Click here to Skip to main content
Click here to Skip to main content

WPF PropertyGrid - MVVM techniques

, 11 Mar 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
How to build a multicolumn ListView that selects cell templates based on the row data type; and how to create a ViewModel on the fly for each cell template.

Introduction

Two common problems in WPF are selecting a data template for a ListView column based on the data type, and implementing the glue logic that formats the data into something the UI can bind to.

This article presents methods for solving both problems; however, what is important is not the solution, but the thinking behind it. The WPF paradigm is radically different, and hopefully this example will encourage more innovative thinking in UI design.

Knowledge of the Model-View-ViewModel (MVVM) pattern is assumed; if you haven't encountered this pattern before, I highly recommend reading the Wikipedia article on MVVM or John Gossman's original blog post on Model-View-ViewModel.

Background: multicolumn ListViews

A multicolumn ListView has its View property set to an instance of a GridView, as in the following code:

<ListView.View>
    <GridView>
        <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
        <GridViewColumn Header="Value" Width="200" />
    </GridView>
</ListView.View>

Since we're building a property grid, we want the second column to display different sorts of controls based on the type of the property.

GridViewColumn has two properties of interest: CellTemplate and CellTemplateSelector. However, both seem to present obstacles:

  • CellTemplate only allows you to specify a single template, which is used for all rows, and
  • CellTemplateSelector requires you to write a template selector class.

Writing a template selector seems particularly unnecessary, since WPF already has a great mechanism for choosing data templates based on the data type.

Selecting a cell template based on the data

The solution to this problem is to realize that the CellTemplate does not have to present the content of the cell itself. The trick is to use the CellTemplate to host a ContentPresenter, which is what actually selects the appropriate DataTemplate!

<GridViewColumn Header="Value" Width="200">
    <GridViewColumn.CellTemplate>
        <DataTemplate>
            <!-- The cell template is a ContentPresenter, hence the actual
                 template will be selected by type. -->
            <ContentPresenter Content="{Binding}"/>
        </DataTemplate>
    </GridViewColumn.CellTemplate>
</GridViewColumn>

Notice that the Content property of the ContentPresenter is bound to the data context of the CellTemplate. So the CellTemplate is instantiated with its DataContext set to the row being presented, and it simply passes the row on to a ContentPresenter, which selects the appropriate DataTemplate.

Provided there is a DataTemplate defined for each row type, the Value column will now generate content specific to that row. So, let's define two data templates, one for a TextItem and one for a ColorItem:

<DataTemplate DataType="{x:Type clr:TextItem}">
    <TextBox Text="{Binding Text}"/>
</DataTemplate>
            
<DataTemplate DataType="{x:Type clr:ColorItem}">
    <ComboBox />
</DataTemplate>

I'd like to have the ComboBox show a drop down of color values, but unfortunately, my ColorItem class looks like this:

[Serializable]
public class ColorItem : ItemBase
{
    // Inherited from the base class:
    // public string Name { get; set; }
    public float Red { get; set; }
    public float Green { get; set; }
    public float Blue { get; set; }
}

I could use an implementation of IValueConverter to convert the ColorItem to a System.Windows.Media.Color structure. But, when the selection changes, how do I convert back to a ColorItem? In particular, I can't store the Name property in a Color structure, so I don't have enough information to perform the back-conversion.

ViewModel applies

What would be ideal is to wrap up each ColorItem with a ViewModel, which could then do intelligent things like converting from RGB values to a Color and back. There are two snags:

  • Not every item in the list is a ColorItem, and
  • We would prefer to avoid modifying the original list.

What's needed is logic along the lines of: "If the item is a ColorItem, then create a ColorItemViewModel and bind the DataTemplate to that." So, how do we manufacture a ColorItemViewModel for each ColorItem data template?

It may not be obvious, but it's possible to create an instance of an arbitrary class in a ResourceDictionary:

<DataTemplate.Resources>
    <clr:ColorItemViewModel x:Key="ViewModel" />
</DataTemplate.Resources>

That's half the problem solved. Every time the DataTemplate is instantiated, an instance of ColorItemViewModel will be created in its resource dictionary. Now, it's a question of wiring it up to the actual data. Taking a wild stab in the dark:

<DataTemplate.Resources>
    <clr:ColorItemViewModel x:Key="ViewModel" Item="{Binding}" />
</DataTemplate.Resources>

Can it be that simple? At first I thought it was - but I was wrong. Using a binding the way I did requires a DataContext. Even if my ViewModel had a DataContext, a ResourceDictionary doesn't, so there's no inheritance chain for the ViewModel to get the correct context. But all is not lost! There's no reason the ViewModel can't be an (invisible) FrameworkElement. Here's the code:

<DataTemplate DataType="{x:Type clr:ColorItem}">
    <Grid>
        <clr:ColorItemViewModel x:Name="Persona" Item="{Binding}"/>
        <ComboBox DataContext="{Binding ElementName=Persona}"
                  ItemsSource="{Binding AvailableColors}"
                  SelectedItem="{Binding Color}" />
    </Grid>
</DataTemplate>

The grid is used as a placeholder to contain the invisible ViewModel and the ComboBox. Provided the ViewModel derives from FrameworkElement, this works very well.

Notice that we've swapped out the DataContext of the ComboBox, giving it the ViewModel instead of the original ColorItem. Now the ItemsSource binding will fetch the available colors from the ColorItemViewModel instance.

Points of interest

There are two major concepts here. First, the idea of nesting data templates within other data templates, using a ContentPresenter. This allows all sorts of interesting tricks, such as animating the RenderTransforms of list items without altering the original data templates for the items.

The second (and more interesting) trick is to use invisible FrameworkElements inside a DataTemplate to pull in logic for each instance. As these are created each time the data template is instantiated, you'll have a unique instance of your logic class for each data item. You can then use the binding syntax to wire up the logic to the visual elements.

Conclusion

This is not intended as a definitive example of How To Implement a PropertyGrid. There are many ways to skin the cat in WPF. But it's an elegant solution, and I hope that it will provide inspiration for other elegant solutions.

History

In the first version of this article, I made a major mistake, putting the ViewModel inside the Resources section. Clever idea, but I had neglected to think about how the ViewModel would get a DataContext. Which goes to show that not all good ideas work the way you expect them to.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David K Turner
Software Developer Cyest Corporation
South Africa South Africa
David is a software developer with an obsession for analysis and proper architecture.

Comments and Discussions

 
GeneralNice document Pinmembereric_yu25-Mar-12 4:01 
This is good help for me about create custom data property grid. Laugh | :laugh:
GeneralMy vote of 1 Pinmemberspiritboy25-Sep-11 2:33 
QuestionAy idea how to solve Property Grid Grouping Filtering and Searching? Pinmemberride4sun31-Aug-11 7:55 
GeneralMy vote of 5 PinmemberDavidSanFran29-Apr-11 8:54 
GeneralThinking Out of the Box PinmemberBSalita14-Nov-10 13:49 
GeneralGood! PinmemberWes Aday9-Mar-10 9:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141220.1 | Last Updated 11 Mar 2010
Article Copyright 2010 by David K Turner
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid