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>
-->
<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
{
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 RenderTransform
s of list items without altering the original data templates for the items.
The second (and more interesting) trick is to use invisible FrameworkElement
s 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.