Showing posts with label Theme. Show all posts
Showing posts with label Theme. Show all posts

Tuesday, November 16, 2010

WPF Themes - Ensuring Hot Keys Display properly

In going through the themes in the WPF Themes project on CodePlex I've encountered many inconsistencies.  Sometimes these inconsistencies are annoying and other times they require much more complicated fixes.  All these issues are instructive because they teach us things to be aware of when building our own themes or even our own templates.


I've written about some of the more complex issues I've encountered in the past.  Today I'm just going to talk about one of the more simple problems.  Some themes in the WPF Themes do not properly display the access keys.  The underscore used to indicate the key was being displayed instead of removed.  This is a simple problem to fix.  ContentPresenters have a property RecognizesAccessKey.  If this property is not set, it defaults to false and the access keys will not work.  Writing RecognizesAccessKey="True" will make the access keys work properly.


I would have preferred the default for this property to true.  In the general case I want the access keys to be active.  An even better solution would be if there was a global way to specify the default for it.  Using access keys is usually what you want to be application wide and it'd eliminate little presentation bugs to if this was possible. Its easy to forget to set this property on a ContentPresenter which can manifest in hard to find locations.  Its an easy problem to fix, but annoying when you discover it.

Wednesday, March 3, 2010

ContentPresenter, GridViewRowPresenter, and ListViewItems

There are two distinct ways that content can be presented within control templates.  These are the GridViewRowPresenter and the ContentPresenter.  The only place that the GridViewRowPresenter is used is within a GridView to present the cell of data.  This is needed for the binding path to be evaluated properly.  If for example I have the class Foo defined below:

public class Foo{
    public string X {get; set;}
    public string Y {get; set;}
}

If I want to use a ListView presented using a GridView inside it to present the values of both X and Y as columns you would write the following assuming the FooCollectionView is a collection view source or some other collection of Foo's


<ListView ItemsSource={Binding FooCollectionView}">
  <ListView.View>
    <GridView >
      <GridViewColumn Header="X" DisplayMemberBinding="{Binding Path=X}" />
      <GridViewColumn Header="Y" DisplayMemberBinding="{Binding Path=Age}" />
    </GridView>
  </ListView.View>
</ListView>

  
If you want to define a style or control template that applies to this control instead of the normal location, where you'd put a ContentPresenter in the template you put a GridViewRowPresenter.  What happens if you use a ContentPresenter instead?  You get the content presented but its not in a grid and its just the ToString() of the object.  This obviously is not what you want to occur.

Therefore, it seems fairly obvious that you should use a GridViewRowPresenter.  It however, is not that straightforward because what if you want to not use a GridView and instead just want to list them (the same as would be in a ListBox) so you have something like the following?

<ListView ItemsSource={Binding FooCollectionView}">

What happens when you define the style with GridViewRowPresenter to present the content?  The ListView will appear to have no content because there is no GridView to present the content of.  This does not present a huge problem if you are defining a style on a per ListView instance.  However, it is a problem if you want to define a general style like you would in a theme.  This presents a big problem because either you cannot use a GridView or must always use a GridView.  You can get around this by using a ListBox anywhere you don't want to use a GridView.  This strategy would mean letting the style dictate the form of the application which shouldn't be the case.

However, the real problem occurs when defining a reusable theme.  If you want to define a general purpose theme that others can reuse without having to alter thier application such as in the WPFThems project then you need it be able to handle both cases.  On a side-note some of the themes in WPFThemes use the ContentPresenter and some use the GridRowViewPresenter.  This can make it so that the ListBox is shown properly with some themes applied and improperly with others.

The fix to this is a bit of a hack but ultimatley turns out to work.  It basically involves defining both presenters inside the control template.  So where you put the presenters you put code that looks like the following:


  <GridViewRowPresenter x:Name="gridrowPresenter"
                        Content="{TemplateBinding Property=ContentControl.Content}"/>
  <ContentPresenter x:Name="contentPresenter"
                    Content="{TemplateBinding Property=ContentControl.Content}"  Visibility="Collapsed"/>


 To get the content to correctly display doing this.  For the template the following trigger will need to be added:

   <Trigger Property="GridView.ColumnCollection" Value="{x:Null}">
     <Setter TargetName="contentPresenter" Property="Visibility" Value="Visible"/>
  </Trigger>     

This trigger will show the ContentPresenter when the GridViewRowPresenter has no content.  Since there is no GridView the GridViewRowPresenter will not display anything visually.

Obviously, this is a bit of a hack to get around a flaw with how WPF works.  Hopefully, in a future version this will be addressed in how the framework works.

Edit 11/16/10 Added  Visibility="Collapsed" property to the contentPresenter element which was a bug in the implementation

Wednesday, February 10, 2010

SourceName In MultiTriggers in WPF Themes

There is a bug in the WPF code that can cause a null pointer exception.  The null pointer exception occurs on line 5924 of System.Windows.StyleHelper.cs :

object evaluationValue = evaluationNode.GetValue( conditions[i].Property );

The problem has to do when you are defining a MultiTrigger on an element in a theme.  This bug manifested itself when on our project we were refreshing a collection that was bound to a TreeViewer and the TreeViewItem style had a MultiTrigger in it.  The style in question comes from the WPFThemes project and was the ExpressionDark Theme (the problem occurs in about 7 of the other themes in the project). The one Condition on the MultiTrigger in the theme has a SourceName defined for it.  The SourceName was not needed because it was the element right inside the root element anyway.  This bug was especially subtle because it would only manifest itself after other conditions would have happened that would cause the MultiTrigger to fire.

 I suspect, but have not verified, that the precondition for causing this bug is having a currently selected TreeViewItem that is removed upon refreshing the control and a newly generated item in the refresh was being set as the selected item in its stead.  The trigger was checking the condition but the new item hadn't fully been generated so when it was looking for the child element it didn't yet exist.  This makes sense in context  of the code in the StyleHelper class.  The code that returns the element using the GetChild method defined on line 6388.  The comment inside the method says:


 // Notice that if we are requesting a childIndex that hasn't been
 // instantiated yet we return null. This could happen when we are 
 // invalidating the dependents for a property on a TemplateNode and
 // the dependent properties are meant to be on template nodes that
 // haven't been instantiated yet.


This method was returning null because


if (styledChildren == null || childIndex > styledChildren.Count)
was evaluating to true because styledChilderen was null.

The null pointer exception was occurring because line 5924


object evaluationValue = evaluationNode.GetValue( conditions[i].Property );



was assuming evaluationNode as not null.

It was assuming it could do this because of the Debug.Assert on line 5903:


 Debug.Assert(evaluationNode != null, 
                    "Couldn't find the node corresponding to the ID and name given in the trigger.  This should have been caught somewhere upstream, like StyleHelper.SealTemplate()." );


This brings up a point about using Debug.Asserts in code.  People often use them (I see it on my project often) when they don't want to perform the null check overhead or other conditions that they feel should never happen.  The idea is that you should catch any of these cases in testing.  Obviously this is a case that was over looked in Microsoft's testing.  The method that set the value of evaluationNode before the Assert has a perfectly legitimate reason for returning null (see the comments earlier) then its not reasonable to use the Assert here.  More on a future post about using Asserts versus null checks.

The work around for this bug was to remove the SourceName from the MultiTrigger.  This does not change the style in a meaningful way and removes the null pointer exception