Creating DataTemplates in code using LINQ to XML

In the wonderful world of WPF development there are some things that are easier to do code and some things better left to XAML. For instance, in my previous webcast Charting 101, most of the work was done in C# code but the DataTemplates were done in XAML. I prefer to do most of my work in code so that it's all in one place, but creating a DataTemplate is something that is much easier to do in XAML. Plus, they can be loaded from code and assigned to other dynamic elements at runtime without much problem. But you might find yourself in a scenario where you need to completely generate a DataTemplate programmatically. How can you do it?

There are two common approaches for runtime DataTemplate creation: using the FrameworkElementFactory class or using the XamlReader class with a string created with LINQ to XML objects. See below for sample code using both approaches as we create some dynamic templates to use as data labels and tooltips for the C1Chart control.

FrameworkElementFactory

The original way to programmatically create DataTemplates is to use the FrameworkElementFactory class. This approach is straightforward, but can be tricky to create complex templates with many UI Elements. In the code snippet below we create a DataTemplate with a TextBlock as the root element bound to our chart Values.


DataTemplate GetDataTemplate()  
{  
    FrameworkElementFactory fef = new FrameworkElementFactory(typeof(TextBlock));  
    //setup binding for textblock to chart values  
    Binding textBinding = new Binding("Value");  
    textBinding.Converter = new C1.WPF.C1Chart.FormatConverter();  
    textBinding.ConverterParameter = "n2";  

    fef.SetBinding(TextBlock.TextProperty, textBinding);  
    DataTemplate dt = new DataTemplate();  
    dt.VisualTree = fef;  
    return dt;  
}  

To use this template at runtime on C1Chart, we call this method on the ChartDataSeries' PointLabelTemplate or PointTooltipTemplate properties. Each data label or tooltip will display the Y-value on each data point.


ds.PointLabelTemplate = GetDataTemplate();  

This approach works, however; according to Microsoft the FrameworkElementFactory class is depracated. The recommended way to create a DataTemplate now is to load XAML from a string using XamlReader. This approach is detailed next.

XamlReader and LINQ to XML

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. To build a string of XAML we can use LINQ to XML to help. As a LINQ-enabled, in memory XML programming interface, LINQ to XML enables us to modify XML trees easily. This approach, called functional construction, takes advantage of the powerful XElement and XAttribute objects to create and modify XML trees, such as a XAML DataTemplate. These classes are found in the System.XML.Linq namespace.

The following method builds a more complex DataTemplate programmatically using this approach. This template is designed as a data label for a C1Chart, so we have also included the necessary chart converter in the markup.


DataTemplate GetDataTemplate()  
{  
    XNamespace ns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";  
    XElement xDataTemplate =  
        new XElement(ns   "DataTemplate",  
        new XAttribute(XNamespace.Xmlns   "x", "http://schemas.microsoft.com/winfx/2006/xaml"),  
            new XAttribute(XNamespace.Xmlns   "c1", "http://schemas.componentone.com/xaml/c1chart"),  
            new XElement(ns   "Border",  
                new XAttribute("BorderBrush", "Black"),  
                new XElement(ns   "Grid",  
                    new XElement(ns   "Grid.ColumnDefinitions",  
                        new XElement(ns   "ColumnDefinition"),  
                        new XElement(ns   "ColumnDefinition")),  
                    new XElement(ns   "Grid.RowDefinitions",  
                        new XElement(ns   "RowDefinition"),  
                        new XElement(ns   "RowDefinition")),  
                    new XElement(ns   "TextBlock",  
                        new XAttribute("Text", "X=")),  
                    new XElement(ns   "TextBlock",  
                        new XAttribute("Text", "{Binding Path=[XValues], Converter={x:Static c1:Converters.Format}, ConverterParameter=n2}"),  
                        new XAttribute("Grid.Column", "1")),  
                    new XElement(ns   "TextBlock",  
                        new XAttribute("Text", "Y="),  
                        new XAttribute("Grid.Row", "1")),  
        new XElement(ns   "TextBlock",  
            new XAttribute("Text", "{Binding Path= [Values], Converter={x:Static c1:Converters.Format}, ConverterParameter=n2}"),  
            new XAttribute("Grid.Column", "1"),  
            new XAttribute("Grid.Row", "1"))  
                        )));  


    StringReader sr = new StringReader(xDataTemplate.ToString());  
    XmlReader xr = XmlReader.Create(sr);  
    DataTemplate dataTemplateObect = System.Windows.Markup.XamlReader.Load(xr) as DataTemplate;  
    return dataTemplateObect;  
}  

XElement and XAttribute objects are used to quickly add UI Elements and set properties in our XAML string. We use XNamespace for our namespace which is combined with each element using the override of the addition operator. With this approach it's much easier to add many elements to our template because we're essentially just building a string. We have not only some bound TextBlocks, but they are nested inside Grid and Border elements. The root XElement object above is basically creating the following XAML markup:


<DataTemplate xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml xmlns:c1=http://schemas.componentone.com/xaml/c1chart xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">   
  <Border BorderBrush="Black">   
    <Grid>   
      <Grid.ColumnDefinitions>   
        <ColumnDefinition />   
        <ColumnDefinition />   
      </Grid.ColumnDefinitions>   
      <Grid.RowDefinitions>   
        <RowDefinition />   
        <RowDefinition />   
      </Grid.RowDefinitions>   
      <TextBlock Text="X=" />   
      <TextBlock Text="{Binding Path=[XValues], Converter={x:Static c1:Converters.Format}, ConverterParameter=n2}" Grid.Column="1" />   
      <TextBlock Text="Y=" Grid.Row="1" />   
      <TextBlock Text="{Binding Path= [Values], Converter={x:Static c1:Converters.Format}, ConverterParameter=n2}" Grid.Column="1" Grid.Row="1" />   
    </Grid>   
  </Border>   
</DataTemplate>  

Finally, since this is WPF we use an XMLReader to pass our string into XamlReader, and this can be directly cast to a DataTemplate object.

In Silverlight, the XamlReader Load method accepts different arguments, allowing us to skip the StringReader and XmlReader and just pass the XAML string. You could rewrite the last 4 lines as the following 2 lines in Silverlight.


...  
//Silverlight code  
DataTemplate dataTemplateObect = XamlReader.Load(xDataTemplate.ToString()) as DataTemplate;  
return dataTemplateObect;  

Creating templates in code with LINQ to XML gives us the power to modify the XML tree dynamically.

Greg Lutz

comments powered by Disqus