Creating WPF Data Templates in Code
The Right Way
Download DataTemplateManager.cs
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) { // WARNING: NOT WORKING CODE! DO NOT USE 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 they 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 typeTextViewModel
, which triggers new data template. - A
ContentControl
with content of typeTextViewModelObsolete
, 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.
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!
Feedback
Questions? Comments? Feel free to
Leave feedback
Copyright (c) Ivan Krivyakov. Last updated: August 21, 2012