Search code examples
c#wpfxamldata-binding

How to bind hierarchical list<T> to WPF TreeView


I have a hierarchical type Category, that I want to put into TreeView. Nested level count is unlimited. Data is stored in DB with hierarchyid field.

Class Definition

public class Category
{
    public Category()
    {
        NestedCategories = new List<Category>();
    }

    public string CategoryParentID { get; set; }
    public string CategoryHID { get; set; }
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string CategoryValueType { get; set; }
    public DateTime LastTimeUsed { get; set; }

    public List<Category> NestedCategories
    {
        get; set;
    }

    public void AddChild(Category cat)
    {
        NestedCategories.Add(cat);
    }

    public void RemoveChild(Category cat)
    {
        NestedCategories.Remove(cat);
    }

    public List<Category> GetAllChild()
    {
        return NestedCategories;
    }
}

Firstly I took all data from table and Put it to structured list. I checked result in debugger, and it's really contains all categories donwn by levels.

public CategorySelector()
{
    InitializeComponent();
    catctrl = new CategoryController();

    Categories = CategoriesExpanded();

    DataContext = this;
}

private readonly CategoryController catctrl;

public List<Category> Categories { get; set; }

private List<Category> CategoriesExpanded()
{
    List <Category> list = catctrl.GetAllCategories();
    foreach (Category cvo in GetAllCat(list))
    {
        foreach (Category newparent in GetAllCat(list))
        {
            if (newparent.CategoryHID.ToString().Equals(cvo.CategoryParentID.ToString()))
            {
                list.Remove(cvo);
                newparent.AddChild(cvo);
                break;
            }
        }
    }
    return list;
}

private List<Category> GetAllCat(List<Category> list)
{
    List<Category> result = new List<Category>();
    foreach (Category child in list)
    {
        result.AddRange(GetNestedCat(child));
    }
    return result;
}

private List<Category> GetNestedCat(Category cat)
{
    List<Category> result = new List<Category>();
    result.Add(cat);
    foreach (Category child in cat.NestedCategories)
    {
        result.AddRange(GetNestedCat(child));
    }
    return result;
}

Then I add binding in XAML and there is the problem. I tried different combinations, but all I got is only first level displayed.

<mah:MetroWindow x:Class="appname.Views.CategorySelector"
        xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:localmodels="clr-namespace:appname.Models"
                 mc:Ignorable="d"
        .....
        <TreeView Name="TV_Categories" Grid.Row="5" FontSize="16" ItemsSource="{Binding Categories}" DisplayMemberPath="CategoryName">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type localmodels:Category}" ItemsSource="{Binding NestedCategories}" >
                    <TextBlock Text="{Binding CategoryName}" />
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>
</mah:MetroWindow>

So what did I do wrong? Thank you.


Solution

  • I think you are using the wrong XAML language version. There are multiple XAML langauge versions. WPF only fully supports the 2006 version. The 2009 version is only partially supported.

    In WPF, you can use XAML 2009 features, but only for XAML that is not WPF markup-compiled. Markup-compiled XAML and the BAML form of XAML do not currently support the XAML 2009 language keywords and features.

    These are the correct 2006 language XML namespaces:

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    

    In your DataType definition, you use xmlns on a property element, which is a 2009 language feature.

    XAML 2009 can support XAML namespace (xmlns) definitions on property elements; however, XAML 2006 only supports xmlns definitions on object elements.

    You cannot use this in WPF, if your project does not meet the constraints above. Instead, you can declare your local XML namespace on an object element, e.g. your top level element (here Window) and use the x:Type markup extension or simply DataType="localmodels:Category", e.g.:

    <Window x:Class="YourApp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:localmodels="clr-namespace:YourApp"
    
    <HierarchicalDataTemplate DataType="{x:Type localmodels:Category}" ItemsSource="{Binding NestedCategories}" >
       <TextBlock Text="{Binding CategoryName}" />
    </HierarchicalDataTemplate>
    

    Update, I found the root cause. If you set the DisplayMemberPath this will cause the TreeView to apply a data template that just shows the ToString() text of the corresponding property value. It does not know about your nested collections of categories.

    Now, if you assign your data template directly to TreeView.ItemTemplate in addition to setting DisplayMemberPath, you will get an exception stating that you cannot use both.

    System.InvalidOperationException: 'Cannot set both "DisplayMemberPath" and "ItemTemplate".'

    However, if you define the data template in the Resources, there is no exception, it fails silently and applies the DisplayMemberPath template and that is why only one level is displayed.

    In order to solve the issue, just remove the DisplayMemberPath and all of these variants work.

    <TreeView Name="TV_Categories" Grid.Row="5" FontSize="16" ItemsSource="{Binding Categories}">
       <TreeView.Resources>
          <HierarchicalDataTemplate DataType="{x:Type local:Category}" ItemsSource="{Binding NestedCategories}" >
             <TextBlock Text="{Binding CategoryName}" />
          </HierarchicalDataTemplate>
       </TreeView.Resources>
    </TreeView>
    
    <TreeView Name="TV_Categories" Grid.Row="5" FontSize="16" ItemsSource="{Binding Categories}">
       <TreeView.ItemTemplate>
          <HierarchicalDataTemplate DataType="{x:Type local:Category}" ItemsSource="{Binding NestedCategories}" >
             <TextBlock Text="{Binding CategoryName}" />
          </HierarchicalDataTemplate>
       </TreeView.ItemTemplate>
    </TreeView>
    
    <TreeView Name="TV_Categories" Grid.Row="5" FontSize="16" ItemsSource="{Binding Categories}">
     <TreeView.ItemTemplate>
          <HierarchicalDataTemplate ItemsSource="{Binding NestedCategories}" >
             <TextBlock Text="{Binding CategoryName}" />
          </HierarchicalDataTemplate>
       </TreeView.ItemTemplate>
    </TreeView>