Home > C#, MVVM, WPF > WPF, MVVM and the TreeView Control using with different HierarchicalDataTemplates and DataTemplateSelector

WPF, MVVM and the TreeView Control using with different HierarchicalDataTemplates and DataTemplateSelector


I spent some time today getting to grips with the TreeView control whilst trying to maintain an MVVM perspective.

To generalise the situation I have a pretty typical hierarchy of objects, e.g. Customer, Order, Product, each of which has numerous properties.

Although the objects contain the hierarchical nature within themselves, i.e. Customer has a property called Orders, the objects themselves do not contain properties useful for displaying with a TreeView (or other such hierarchical control), such as IsSelected, IsExpanded, etc.

What I want to be able to achieve is to be able to expose the objects via the ViewModel to the View so that each type of object can be displayed differently, e.g. the Customer shows a customer icon and displays the customer number and name, whereas the Order shows an order icon and displays the order number and total value. In addition I need to add the additional properties to each that will be required for use in a hierarchical control, such as the TreeView.

The options at this point are:

  • Subclass each object individually and add in the extra properties
    This means that the implementation is very much tied to this solution and is not very reusable.
  • Create a new set of new objects (one for each of the original objects), containing the additional properties required, that accept the original objects in the constructor and then exposes only the properties required for display.
    This again ties the implementation to the solution, but also limits any future changes to the view as to what can be exposed.
  • Wrap the objects in a class that has the required additional properties suitable for a hierarchy
    This is the most reusable and generic solution, but of course brings with it some additional problems.
  • Implement a pattern, such as the Decorator pattern.
    Sadly since the origin of the classes is out of my control this is not an option. This can happen when importing proxies from various WCF services, utilising classes generated by Entity Framework, etc.

I opted for the third option, creating a wrapper, which makes life a little harder, i.e. is it not a quick and dirty solution, but it has the advantage of reuse.


The Hierarchy Item Wrapper (HierarchyItemViewModel)

The first item we will create here is the wrapper for the objects to be shown in the TreeView. I’ll call this ‘HierarchyItemViewModel’.

In my implementation this inherits from ViewModelBase. ViewModelBase simply sets up some of the key points required in an MVVM solution, such as ObservableObjects. See the MVVM Foundation for a good starting point.

The main points in the code shown below in Listing 1 are:

  • The constructor accepts an object which is the data item being wrapped. This is then exposed to the View via the DataItem property.
  • A CollectionView of Children is exposed as a property as well as Parent.
  • The IsSelected and IsExpanded properties that we needed are created.
  • The properties call the RaisePropertyChanged method on which I have written about previously.
public class HierarchyItemViewModel : ViewModelBase
{
    private CollectionView _children;
    private bool _isExpanded;
    private bool _isSelected;

    public HierarchyItemViewModel(object dataItem)
    {
        DataItem = dataItem;
    }

    public object DataItem { get; private set; }

    public HierarchyItemViewModel Parent { get; set; }

    public bool IsExpanded
    {
        get { return _isExpanded; }
        set
        {
            _isExpanded = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName());
        }
    }

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            _isSelected = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName());
        }
    }

    public System.Windows.Data.CollectionView Children
    {
        get { return _children; }
        set { _children = value; }
    }

}

Listing 1 – HierarchyItemViewModel

The Hierarchy View Model (HierarchyViewModel)

Now we have our wrapper for the items we need to create our nested set of items ready to expose to the actual TreeView.

Listing 2, below, shows the code that builds the hierarchy taking in a list of Customer objects in the constructor. We are assuming that we have already obtained from elsewhere the actual data we want to display. The code is a bit long winded to keep it simple. For a more complex but more generic version see this post.

So here are we are going to wrap each item in a HierarchyItemViewModel object and create the parent and child relationships.

In addition, since we actually want to select a certain item from the list and make sure that it is visible we look out for our chosen item, identified in the constructor, which in this case I have called selectedEntity.

public class HierarchyViewModel : ViewModelBase
{
    public CollectionView Customers { get; private set; }
    private HierarchyItemViewModel _selectedItem;

    public HierarchyViewModel(List customers, object selectedEntity)
    {
        // create the top level collectionview for the customers
        var customerHierarchyItemsList = new List();

        foreach (Customer c in customers)
        {
            // create the hierarchy item and add to the list
            var customerHierarchyItem = new HierarchyItemViewModel(c);
            customerHierarchyItemsList.Add(customerHierarchyItem);

            // check if this is the selected item
            if (selectedEntity != null && selectedEntity.GetType() == typeof(Customer) && (selectedEntity as Customer).Equals(c))
            {
                _selectedItem = customerHierarchyItem;
            }

            // if there are any orders in customerHierarchyItem
            if (c.Orders.Count != 0)
            {
                // create a new list of HierarchyItems
                var orderHierarchyItemsList = new List();
                // loop through the orders and add them
                foreach (Order o in c.Orders)
                {
                    // create the hierarchy item and add to the list
                    var orderHierarchyItem = new HierarchyItemViewModel(o);
                    orderHierarchyItem.Parent = customerHierarchyItem;
                    orderHierarchyItemsList.Add(orderHierarchyItem);

                    // check if this is the selected item
                    if (selectedEntity != null && selectedEntity.GetType() == typeof(Order) && (selectedEntity as Order).Equals(o))
                    {
                        _selectedItem = orderHierarchyItem;
                    }

                    // if there are any products in orderHierarchyItem
                    if (o.Products.Count != 0)
                    {
                        // create a new list of HierarchyItems
                        var productHierarchyItemsList = new List();
                        // loop through the sites and add them
                        foreach (Product p in o.Products)
                        {
                            // create the hierarchy item and add to the list
                            var productHierarchyItem = new HierarchyItemViewModel(p);
                            productHierarchyItem.Parent = orderHierarchyItem;
                            productHierarchyItemsList.Add(productHierarchyItem);

                            // check if this is the selected item
                            if (selectedEntity != null && selectedEntity.GetType() == typeof(Product) && (selectedEntity as Product).Equals(p))
                            {
                                _selectedItem = productHierarchyItem;
                            }
                        }

                        // create the children of the order
                        orderHierarchyItem.Children = new CollectionView(productHierarchyItemsList);
                    }
                }
                // create the children of the customer
                customerHierarchyItem.Children = new CollectionView(orderHierarchyItemsList);
            }
        }

        this.Customers = new CollectionView(customerHierarchyItemsList);

        // select the selected item and expand it's parents
        if (_selectedItem != null)
        {
            _selectedItem.IsSelected = true;
            HierarchyItemViewModel current = _selectedItem.Parent;

            while (current != null)
            {
                current.IsExpanded = true;
                current = current.Parent;
            }
        }
    }
}

Listing 2 – HierarchyViewModel

The TreeView XAML

The next step is to create our TreeView in XAML and bind it to our HierarchyViewModel. For the simplicity of this example I’ve added the TreeView class straight to the MainWindowView. This could of course be wrapped up into its own UserControl. The ViewModel is attached to the DataContext of the View on creation. I won’t go into the code for how to bind the ViewModel to the View as this is covered in depth elsewhere. Important points to note in the code in Listing 3 are:

  • The TreeView tag (line 11) where the DataContext is set for the TreeView, which identifies from where the data for the TreeView will be sourced.
  • The three HierarchicalDataTemplate items, one for each of Customer, Order and Product. Note that each binds to different properties within the DataItem and each could have its own styling. This is the key point of this example.
  • The Window.Resources (line 8) where the HierarchyDataTemplateSelector is identified. This essentially provides a key which can be used in the XAML to a method that selects which HierarchicalDataTemplate will be used for each item in the TreeView. I’ll cover this in more depth below,
  • Again, in the TreeView tag (line 11) the ItemTemplateSelector which references the hierarchyDataTemplateSelector mentioned above.
<Window x:Class="WilberBeast.TreeView.Demo.Views.MainWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ViewModels="clr-namespace:WilberBeast.TreeView.Demo.ViewModels"
        xmlns:View.Helper="clr-namespace:WilberBeast.TreeView.Demo.Views.Helper"
        Title="MainWindowView" Height="350" Width="525">
    <Window.Resources>
        HierarchyDataTemplateSelector x:Key="hierarchyDataTemplateSelector" />
    </Window.Resources>
    <Grid>
        <TreeView Name="HierarchyTreeView" DataContext="{Binding Path=HierarchyViewModel}" ItemTemplateSelector="{StaticResource hierarchyDataTemplateSelector}" ItemsSource="{Binding Customers}">
            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
                    <Setter Property="IsSelected" Value="{Binding IsSelected}" />
                    <Setter Property="FontWeight" Value="Normal" />
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="FontWeight" Value="Bold" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </TreeView.ItemContainerStyle>
            <TreeView.Resources>
                <HierarchicalDataTemplate x:Key="CustomerTemplate" DataType="{x:Type ViewModels:HierarchyItemViewModel}" ItemsSource="{Binding Path=Children}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=DataItem.Name}" Margin="0 0 5 0"/>
                        <TextBlock Text="{Binding Path=DataItem.Number}"/>
                    <!--<span class="hiddenSpellError" pre=""-->StackPanel>
                <!--<span class="hiddenSpellError" pre=""-->HierarchicalDataTemplate>
                OrderTemplate" DataType="{x:Type ViewModels:HierarchyItemViewModel}" ItemsSource="{Binding Path=Children}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=DataItem.Number}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
                ProductTemplate" DataType="{x:Type ViewModels:HierarchyItemViewModel}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=DataItem.Name}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>
</Window>

Listing 3 – TreeView XAML

The DataTemplateSelector

Since to the TreeView class the all the objects within the TreeView appear to be of the same type, due to each being wrapped up in the HierarchyItemViewModel class, in order to display the Customer, Order and Product objects with different styles and content we have to have some code to help the TreeView identify which HierarchicalDataTemplate to use in each case. This is where the TemplateSelector comes in. This is nothing new or fancy, just one of the those hidden gems of WPF. The code for the HierarchyDataTemplateSelector is shown in Listing 4below. The code is fairly self explanatory. Basically the class inherits from the base class DataTemplateSelector and overrides the virtual method SelectTemplate. The TreeView passes the items one at a time to this method and the return value is simply the name of the HierarchicalDataTemplate to use.

public class HierarchyDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        DataTemplate retval = null;
        FrameworkElement element = container as FrameworkElement;

        if (element != null && item != null && item is HierarchyItemViewModel)
        {
            HierarchyItemViewModel hierarchyItem = item as HierarchyItemViewModel;
            if (hierarchyItem.DataItem != null)
            {

                if (hierarchyItem.DataItem.GetType() == typeof(Customer))
                {
                    retval = element.FindResource("CustomerTemplate") as DataTemplate;
                }

                else if (hierarchyItem.DataItem.GetType() == typeof(Order))
                {
                    retval = element.FindResource("OrderTemplate") as DataTemplate;
                }

                else if (hierarchyItem.DataItem.GetType() == typeof(Product))
                {
                    retval = element.FindResource("ProductTemplate") as DataTemplate;
                }
            }
        }

        return retval;
    }
}

Listing 4 – HierarchyDataTemplateSelector

All the code used in this example has been pulled together into one downloadable project that demonstrates the principle. The links are below.

Please do send me your comments as they are always welcome.

Happy coding…

WilberBeast.TreeView.Demo.zip (100.70 kb)
WilberBeast.TreeView.Demo.doc (100.70 kb)
Same as the zip version, just renamed to .doc to help with downloading.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: