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...