Monday, December 22, 2008

WPF - data templating

Well, I found the weird facts about WPF (at least I think that they are weird) during resolving the first question from the list of the prior post:

How to hide the column headers for WPF list view controls?

I found no way to disable the column headers for WPF ListView control and no way to disallow the resizing of the columns. However I learned WPF feature called "data templating" which is new for me. Data templating allows you to define the appearance of the items in ListBox and ListView control. Data templating allows you to define the relation between the visual components properties and data source. To achieve my needs (show the data using multicolumn grid view but do not show the column headers) I should use ListBox control specifying the data template for the ListBox items. MSDN shows the examples of the data templates using XAML syntax only, i.e.:
<ListBox.ItemTemplate>
 <DataTemplate>
  <StackPanel>
   <TextBlock Text="{Binding Path=Name}" />
   <TextBlock Text="{Binding Path=Value}"/>
  </StackPanel>
 </DataTemplate>
</ListBox.ItemTemplate>


But creating the Data Template programmatically topic looks like somewhat classified. Well, let’s look at this topic closer. ListBox and ListView types have ItemTemplate property which is inherited from ItemsControl class which represents a control that can be used to present a collection of items. The type of ItemTemplate property is DataTemplate class. DataTemplate overview in MSDN also has only XAML syntax bases examples. DataTemplate class has VisualTree property which should be set to define the appearance of the items. Actually this is not that explicitly stated in MSDN. Then the type of VisualTree property of DataTemplate class is FrameworkElementFactory class. The description of this class says:

"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. The recommended way to programmatically create a template is to load XAML from a string or a memory stream"

So, guys in MS believe that creating the data templates programmatically is a deprecated way! It sounds like terrific fact for me. However, I tried to find the limitations that exist for creating the data templates programmatically and instantaneously faced the problem. First of all I tried to create the visual tree for the data template that is shown above (stack panel has 2 child textblocks, the 1st textblock is bound to Name property of data source item, the 2nd textblock is bound to Value property). Operating on FrameWorkElement factory is weird itself. Look at the code:
private static FrameworkElementFactory CreateDataTemplate()
{
   var txtNode = new FrameworkElementFactory(typeof(TextBlock));
   txtNode.SetBinding(TextBlock.TextProperty, new Binding("Name"));
   var valueNode = new FrameworkElementFactory(typeof(TextBlock));
   valueNode.SetBinding(TextBlock.TextProperty, new Binding("Value"));
   var stackPanelNode = new FrameworkElementFactory(typeof (StackPanel));
   stackPanelNode.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
   stackPanelNode.AppendChild(txtNode);
   stackPanelNode.AppendChild(valueNode);
   return stackPanelNode;
}

So, to create the visual tree for the data template programmatically I need to manually replicate the corresponding XAML nodes tree in the code and this way has limitations.
Let’s return to the goal I wanted to achieve:
Show the data using multicolumn grid view but do not show the column headers.
Basically I need to show the data in ListBox control and define the data template with visual tree that is Grid control:
<Grid>
 <Grid.ColumnDefinitions>
  <ColumnDefinition Width="100" />
  <ColumnDefinition Width="50" />
 </Grid.ColumnDefinitions>
 <Grid.RowDefinitions>
  <RowDefinition />
 </Grid.RowDefinitions>
 <TextBlock Text="{Binding Path=Name}" Grid.Column="0" Grid.Row="0" />
 <TextBlock Text="{Binding Path=Value}" Grid.Column="1" Grid.Row="0" />
</Grid>

Well, I tried to create this visual tree programmatically:

private static FrameworkElementFactory CreateDataTemplate()
{
   var txtNode = new FrameworkElementFactory(typeof(TextBlock));
   txtNode.SetBinding(TextBlock.TextProperty, new Binding("Name"));
   txtNode.SetValue(Grid.RowProperty, 0);
   txtNode.SetValue(Grid.ColumnProperty, 0);
   var valueNode = new FrameworkElementFactory(typeof (TextBlock));
   valueNode.SetBinding(TextBlock.TextProperty, new Binding("Value"));
   valueNode.SetValue(Grid.RowProperty, 0);
   valueNode.SetValue(Grid.ColumnProperty, 1);
   var rowDefNode = new FrameworkElementFactory(typeof (RowDefinition));
   var col1DefNode = new FrameworkElementFactory(typeof (ColumnDefinition));
   var col2DefNode = new FrameworkElementFactory(typeof(ColumnDefinition));
   var rowsDefNode = new FrameworkElementFactory(typeof (RowDefinitionCollection));
   rowsDefNode.AppendChild(rowDefNode);
   var colsDefNode = new FrameworkElementFactory(typeof (ColumnDefinitionCollection));
   colsDefNode.AppendChild(col1DefNode);
   colsDefNode.AppendChild(col2DefNode);
   var gridNode = new FrameworkElementFactory(typeof (Grid));
   gridNode.AppendChild(rowsDefNode);
   gridNode.AppendChild(colsDefNode);
   gridNode.AppendChild(txtNode);
   gridNode.AppendChild(valueNode);
}

Executing this code throws the following exception:
"'RowDefinitionCollection' type must derive from FrameworkElement, FrameworkContentElement, or Visual3D."
Yep! You can’t create the visual tree nodes for types which are not derived from the listed types! This is the main limitation of creating data template programmatically.
When I faced that I thought that I made some mistake, I thought – well, probably I can load the data template from pre-defined XAML visual tree that is based on Grid definition and look how VisialTree property value is constructed? Nope. If you load the data template from XAML content, then VisualTree property of DataTemplate instance is null.
So, the only way to construct the data template programmatically is load it from XAML fragments. I think that this is very weird fact.
Indeed I can be asked – why do you need to create the data templates at runtime? Do you know any live use-case of that? Yes, I know.
The first idea that came to my mind when I learned the data templates is we can create the simple reporting engine using WPF capabilities. The reporting engine that seems like ActiveReports would be created very easy. End users would specify the data they want to show and the data item template exactly like ActiveReports designer. Then the engine would construct the data template programmatically and show the data in WPF ListBox control. With the current way of operating on Data Templates programmatically this is not possible.
Well, once again returning to the problem that I tried to resolve – I made it by using Grid based data template that is loaded from XAML resource file. The code is trivial.

2 comments:

  1. Actually, there is way. I would not think that this would work but it does!

    //create grid
    var grid = new FrameworkElementFactory(typeof(Grid));

    // assign template to grid
    CellControlTemplate.VisualTree = grid;

    // define grid's rows
    var r = new FrameworkElementFactory(typeof(RowDefinition));
    grid.AppendChild(r);

    // define grid's columns
    var c = new FrameworkElementFactory(typeof(ColumnDefinition));
    grid.AppendChild(c);

    c = new FrameworkElementFactory(typeof(ColumnDefinition));
    c.SetValue(ColumnDefinition.WidthProperty, GridLength.Auto);
    grid.AppendChild(c);

    c = new FrameworkElementFactory(typeof(ColumnDefinition));
    c.SetValue(ColumnDefinition.WidthProperty, GridLength.Auto);
    grid.AppendChild(c);

    ReplyDelete