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

11 comments:

  1. Hi,
    I have the same problem. It seems impossible implementing a generic ListViewItem that works both for ListView and for ListView containing GridView. The hack you proposed does not seem to work in all scenarios... Sometimes happens that both ContentPresenter and GridViewRowPresenter are concurrently displayed...

    ReplyDelete
  2. Do you have a specific case where it is not working for you? Its been a while since I've looked at this problem. I don't think I had to make any changes to it for it to work in all the cases I was seeing. If you give me the case it doesn't work in I'm sure I can figure out how modify it so it works.

    ReplyDelete
  3. Hi!
    here you are an example in which your proposed solution doesn't work as expected: https://sites.google.com/site/mynetsamples/Home/ListViewStyle_Wrong.zip?attredirects=0&d=1

    And here a final working solution that is based on your idea but also adds a Trigger in order to show/hide the ContentPresenter:
    http://dotnetlearning.wordpress.com/2010/11/09/highlight-selected-item-listview-gridview-listviewitem/

    ReplyDelete
  4. You are correct. When I posted the solution I didn't add one key line. You need to have Visibility="Collapsed" on the contentPresenter element otherwise you will see shadowing in the case where the grid row presenter actually has a value. I've edited the post to reflect this.

    ReplyDelete
  5. When I came across this post I was a little dissapointed to find this seemed to be the only way. After much digging around I did find a nice little nugget that you may be interested in.

    Check out this slice of XAML, this is your golden ticket.
    < Style x:Key="{x:Static GridView.GridViewItemContainerStyleKey}"
    TargetType="{x:Type ListViewItem}" />

    Cheers

    ReplyDelete