Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating WPF Data Templates in Code: The Right Way

0.00/5 (No votes)
29 May 2014 1  
How to properly create WPF Data Tempaltes in code

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

Small screen shot

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.
Sample screen shot

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here