Background
While writing large composite MVVM applications I found that I often create data templates that have only
a single visual. These data templates establish a connection a view model type and a view type as follows:
<DataTemplate DataType="{x:Type vm:MyViewModelType}">
<views:MyViewType />
</DataTemplate>
In other words this means "whenever you see an object of type MyViewModel
render it using MyView
.
After creating three or four data templates like that, I naturally wanted to automate that task, but it proved to be
not so easy.
The Wrong Way - Don't Do It At Home
There appears to be a simple way to create DataTemplate
s in code: just create a DataTemplate
object
and assign some properties:
DataTemplate CreateTemplateObsolete(Type viewModelType, Type viewType)
{
return new DataTemplate()
{
DataType = viewModelType,
VisualTree = new FrameworkElementFactory(viewType)
};
}
This code is very simple and it sort of works. Until it doesn't. MSDN help for FrameworkElementFactory
class warns:
"This class is a deprecated way to programmatically create templates...;
not all of the template functionality is available when you create a template using this class."
What is this mysterious functionality that is "not available"? I don't know for sure, but I did find a case when this method of creating templates does not work well.
This is the case when your view has bindings to UI elements defined after the binding itself. Consider this XAML:
<TextBlock Text="{Binding ActualWidth, ElementName=SomeControl}" />
<ListBox Name="SomeControl" />
Here the binding on the TextBlock
references a ListBox
named SomeControl
that is defined in XAML after the binding.
The binding will fail if you put this kind of XAML in a data template created via FrameworkElementFactory
. I found that the hard way, and I do have
a sample to prove it.
But let's see first what is the right way to create data templates.
The Right Way
The MSDN article for FrameworkElementFactory
then goes on to say:
The recommended way to programmatically create a template is to load XAML from a string
or a memory stream using the Load method of the XamlReader class.
The trouble is, the XAML parser they give you in .NET framework is not quite the same as XAML parser that comes with VIsual Studio. In particular, you need
to apply some tricks in order to deal with C# namespaces. The resulting code looks as follows:
DataTemplate CreateTemplate(Type viewModelType, Type viewType)
{
const string xamlTemplate = "<DataTemplate DataType=\"{{x:Type vm:{0}}}\"><v:{1} /></DataTemplate>";
var xaml = String.Format(xamlTemplate, viewModelType.Name, viewType.Name, viewModelType.Namespace, viewType.Namespace);
var context = new ParserContext();
context.XamlTypeMapper = new XamlTypeMapper(new string[0]);
context.XamlTypeMapper.AddMappingProcessingInstruction("vm", viewModelType.Namespace, viewModelType.Assembly.FullName);
context.XamlTypeMapper.AddMappingProcessingInstruction("v", viewType.Namespace, viewType.Assembly.FullName);
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
context.XmlnsDictionary.Add("vm", "vm");
context.XmlnsDictionary.Add("v", "v");
var template = (DataTemplate)XamlReader.Parse(xaml, context);
return template;
}
The bad news is that this code is much more verbose and awkward then the naive code. The good news is that this code works better. In particular, it has
no problem with forward bindings.
Another bad thing about the new way of creating templates is that both view and view model classes must be public in .NET 3.5. If they are not, you'll get a
runtime exception when parsing the XAML that says they should be. .NET 4 does not have this limitation: all classes may be internal.
Registering Data Template with the Application
In order to create visual objects using your data tempalte, WPF must somehow know about it. You make your template globally available
by adding it to the application resources:
var key = template.DataTemplateKey;
Application.Current.Resources.Add(key, template);
Note that you need a special resource key that is retrieved from the template itself. It would seem natural to key the templates using
their data type, but this option is already taken by styles. Therefore, Microsoft had to come up with a different kind of key for data templates.
DataTemplateManager Class
Two steps above: creating the template and registering it in the application resources, are encapsulated in the DataTemplateManager
class. You register your templates as follows:
using IKriv.Wpf;
var manager = new DataTemplateManager();
manager.RegisterDataTemplate<ViewModelA, ViewA>();
manager.RegisterDataTemplate<ViewModelB, ViewB>();
Sample Code
In the sample I have a view called TextView
and a view model called TextViewModel
. The view model defines only a single property
called Text
. The view displays the text string and also its actual width, which is a forward binding.
<UserControl x:Class="DataTemplateCreation.TextView">
<DockPanel>
<TextBlock
DockPanel.Dock="Top"
Margin="5"
Text="{Binding ActualWidth, ElementName=TextControl,
StringFormat='Text width is \{0\}', FallbackValue='Binding failed!'}" />
<Grid>
<TextBlock Name="TextControl" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Text}" />
</Grid>
</DockPanel>
</UserControl>
I then instantiate this view in three ways:
- As a direct child of the main window, no data templating involved.
- Via data template created using the right technique.
- Via data template created using the naive not really working technique.
I then create two data templates in App.xaml.cs
.
var manager = new DataTemplateManager();
manager.RegisterDataTemplate<TextViewModel, TextView>();
manager.RegisterObsoleteDataTemplate<TextViewModelObsolete, TextView>();
The second line of the code above means that content of type TextViewModel
will be displayed as TextView
. The third line
means that content of type TextViewModelObsolete
will also be displayed as TextView
, but this data template is created
using a naive not really working obsolete technique not recommended by MSDN. The main window XAML looks as follows:
<UniformGrid Rows="3" Columns="1">
<local:TextView Background="Red" Foreground="White">
<local:TextView.DataContext>
<local:TextViewModel Text="Direct child" />
</local:TextView.DataContext>
</local:TextView>
<Border Background="Yellow">
<ContentPresenter>
<ContentPresenter.Content>
<local:TextViewModel Text="New Data Template" />
</ContentPresenter.Content>
</ContentPresenter>
</Border>
<Border Background="LightGray">
<ContentPresenter>
<ContentPresenter.Content>
<local:TextViewModelObsolete Text="Obsolete Data Template" />
</ContentPresenter.Content>
</ContentPresenter>
</Border>
</UniformGrid>
It has three horizontal bands:
- A
TextView
explicitly created in XAML, no data templating involved.
- A
ContentControl
with content of type TextViewModel
, which triggers new data template.
- A
ContentControl
with content of type TextViewModelObsolete
, which triggers obsolete data template.
As you can clearly see on the screen shot above, the third band does not look so good, as the forward binding has failed.
IKriv.Windows Library
DataTemplateManager
class is now available as part of IKriv.Windows
library I published on NuGet. Use Tools->Library Package Manager->Manage NuGet Packages for Solution dialog in Visual Studio to easily add IKriv.Windows to your solution.
Conclusion
Although MSDN documentation for programmatically creating DataTemplate
s is vague and hard to find, they do know what they are talking about.
Instantiating DataTemplate
class in code won't work well and you need to use XAML parser as shown above.
I do feel, however, that this is too complicated. It should be possible to construct the template's visual tree manually,
just like when there is no template involved. Crafting XAML strings from element types just to feed them back to XAML parser is
awkward and inefficient. Also, the XAML parser you get in code is subtly different from the XAML parser Visual Studio uses, which aggravates the annoyance.
Fortunately, for the typical case the problem should be solved only once, and then you can just call the RegisterDataTemplate
method.
Viva la encapsulation!
PS. Possibility of Support for Generics
A number of people complained that provided solution does not support generic view models. Unfortunately, supporting data templates for generic view models is not possible. WPF uses XAML 2006, that does not support generics. XAML version 2009 introduces x:TypeArguments
attribute as described in this MSDN topic, but WPF XAML compiler does not fully support it, even in .NET 4.5, and probably never will, given the fact that Microsoft moved on from WPF and will not make any major changes to it.
Another way to squeeze the generics in would be to create a markup extension similar to x:Type
, and use it like this:
<DataTemplate DataType={x:GenericType vm:MyViewModel(coll:List(sys:String))}">
Unfortunately, this route is also blocked. I tried to create such an extension and received the following error when placing this DataTemplate
into a resource dictionary:
A key for a dictionary cannot be of type 'GenericInXaml.GenericType'.
Only String, TypeExtension, and StaticExtension are supported.
In other words, a resource key can only be a string, an {x:Type}
or an {x:Static}
, and none of those supports generics.
So, the bottom line is: sorry, no generics.