Tuesday, August 4, 2009

Redefining DataTemplates for an Inheritance Hierarchy in WPF

I'm working on a project using WPF and MVVM. Given that, my app is very DataTemplate-heavy.

My app is following this pattern:

Every ViewModel class has a default View (CustomControl).

Normally, the template looks like this:

<DataTemplate DataType="{x:Type viewModel:MyViewModel}">
<view:MyView />
</DataTemplate>

This way, there's always a default view (in this case, "MyView"). This works great. So, when you want to change the template in a specific situation, you can just change the DataTemplate for the type "MyViewModel".

Well, I ran across a bit of a problem when I was creating a pluggable architecture which included a set of ICommand-derived objects that would render as a menu. In order to allow naming and separators, I created 2 classes, PlugInAction and PlugInActionSeparator.

So, I started with my templates:

<DataTemplate DataType="{x:Type plugin:PlugInAction}">
<MenuItem Header="{Binding Name}" Command = "{Binding Command}"/>
</DataTemplate>

<DataTemplate DataType="{x:Type plugin:PlugInActionSeparator}">
<Separator />
</DataTemplate>

This way, I bound my actions to a root MenuItem.

<MenuItem
x:Name="actionsMenu"
Header="Actions"
ItemsSource="{Binding Actions}" />

So far, so good. Without anything explicit, the actions will render as menu items or separators based on their data type. Cool. Now, what happens when I want to completely change the way this hierarchy of objects renders? Say... as TextBlocks?

Well, I defined new (non-default) DataTemplates...

<DataTemplate
DataType="{x:Type plugin:PlugInAction}"
x:Key="ActionAsTextTemplate">
<TextBlock Text="{Binding Name}" />
</DataTemplate>

<DataTemplate
DataType="{x:Type plugin:PlugInActionSeparator}"
x:Key="SeparatorAsTextTemplate">
<TextBlock Text="--" />
</DataTemplate>


Now, I want do display my list again... So, I start with an ItemsControl.

If I try:

<ItemsControl
x:Name="actionsControl"
ItemsSource="{Binding Actions}"/>

I get a list of MenuItems and Separators. I need to change the templates.. so, what about this?

<ItemsControl
x:Name="actionsControl"
ItemsSource="{Binding Actions}"
ItemTemplate={..... oh, wait... I can only have one of these.... hmmm...
/>

Code? I guess I could create a DataTemplateSelector class and specify that in my XAML, but I think that's pretty lame. I prefer to leave as much in the XAML as I can for my UI...

The problem is that I want ONE DataTemplate that will handle all types in the hierarchy. I could use DataTriggers based on the Type of the object, but that just makes me want to beat myself with an OO text book.

After thinking long and hard, I figured it out: Nested DataTemplates!

I realized that, through their Resources, DataTemplates can redefine the scope of "default" templates. This means that I can nest new defaults inside a single Default DataTemplate, like this:

<!-- DEFAULT template for ALL Actions -->
<DataTemplate DataType="{x:Type plugin:PlugInAction}" >
<DataTemplate.Resources>
<!-- NEW Default Action Template in the DataTemplate.Resources -->
<DataTemplate DataType="{x:Type plugin:PlugInAction}">
<MenuItem Header="{Binding Name}" Command = "{Binding Command}"/>
</DataTemplate>

<!-- NEW Default Separator Template in the DataTemplate.Resources -->
<DataTemplate DataType="{x:Type plugin:PlugInActionSeparator}">
<Separator />
</DataTemplate>

</DataTemplate.Resources>

<!-- Actual Content Presentation Here -->
<ContentPresenter Content="{Binding}" />
</DataTemplate>

THAT WORKS! So now, my new Text-based template looks like this:

<!-- Text template for ALL Actions -->
<DataTemplate
DataType="{x:Type plugin:PlugInAction}"
x:Key="pluginActionTextTemplateSet" >

<DataTemplate.Resources>
<DataTemplate DataType="{x:Type plugin:PlugInAction}">
<TextBlock Text="{Binding Name}" />
</DataTemplate>

<DataTemplate DataType="{x:Type plugin:PlugInActionSeparator}" >
<TextBlock Text="--" />
</DataTemplate>
</DataTemplate.Resources>

<!-- Content Presentation Here -->
<ContentPresenter Content="{Binding}" />
</DataTemplate>

So now, for the text version, all you'd have to do is:

<ItemsControl
x:Name="actionsAsText"
ItemsSource="{Binding Actions}"
ItemTemplate="{StaticResource pluginActionTextTemplateSet}" />

And there you have it! It redefines a whole hierarchy of DataTemplates.

Ahhh Learning...