TreeView Hierarchical DataTemplate ve WPF


Pokud jste někdy ve WPF pracovali s komponentou TreeView, tak jste asi nebyli nadšeni jejími možnostmi a prací s ní, moc toho totiž neumí a pro jednotlivé “nody” je třeba využívat TreeViewItem(y)… Pokud se ale chcete vyhnout (polo)ruční práci s TreeViewItem(y), využívat binding datového zdroje a nepřijít o možnost hierarachie, tak je k dispozici HierarchicalDataTemplate.

Já osobně jsem tuto techniku využil poprvé reálně teprve nedávno, ale hned mě dostala :) O co jde? HierarchicalDataTemplate je rozšířený klasický DataTemplate, ve kterém se definuje jak má vydat právě jeden “node” a dále, kde vzít podřízené prvky. Takhle to zní celkem jednoduše, ale vlastní implementace je trochu složitější, resp. zaleží na tom, jak si ji představujete a co všechno budete chtít realizovat…

Kód (nejen) v tomto případě mluví za vše a začneme jednoduchým příkladem a nejprve tedy obecným rozhraním, které nám bude držet jednotnou formu a může s využitím a rozšířením do budoucna:


using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace Netcorex
{
  interface ITreeNode : INotifyPropertyChanged
  {
    object Item { get; }
    IList<ITreeNode> Children { get; }
    bool IsExpanded { get; set; }
    bool IsSelected { get; set; }
    bool IsEnabled { get; set; }
    Visibility Visibility { get; set; }
    string NodeTitle { get; }
    bool IsVisible { get; }
    bool IsAvailable { get; }
    int ChildrenCount { get; }
  }
}

Pak přidáme reprezentaci “nodů”:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace Netcorex
{
  public class PersonTreeNode : BindableBase, ITreeNode
  {
    private readonly string m_CustomTitle;
    private readonly PersonModel m_Item;
    private readonly List<PersonTreeNode> m_Children;
    private bool m_IsExpanded = true;
    private bool m_IsSelected;
    private bool m_IsEnabled = true;
    private Visibility m_Visibility = Visibility.Visible;


    /// <summary>
    /// Založení větve se záznamem a případně s podřízenými prvky
    /// </summary>
    public PersonTreeNode(PersonModel item, List<PersonTreeNode> children = null)
    {
      if (item == null)
      {
        throw new ArgumentNullException("item");
      }
      m_Item = item;
      m_Children = children ?? new List<PersonTreeNode>();
    }

    /// <summary>
    /// Založení rodičovské větve s titulkem a případně s podřízenými prvky
    /// </summary>
    public PersonTreeNode(string customTitle, List<PersonTreeNode> children = null)
    {
      m_CustomTitle = customTitle;
      m_Children = children ?? new List<PersonTreeNode>(); ;
    }


    public string CustomTitle
    {
      get { return m_CustomTitle; }
    }
    public PersonModel Item
    {
      get { return m_Item; }
    }
    object ITreeNode.Item
    {
      get { return Item; }
    }
    public List<PersonTreeNode> Children
    {
      get { return m_Children; }
    }
    IList<ITreeNode> ITreeNode.Children
    {
      get
      {
        IList<PersonTreeNode> children = Children;
        return (children == null) ? null : new List<ITreeNode>(children);
      }
    }
    public bool IsExpanded
    {
      get { return m_IsExpanded; }
      set { SetProperty(ref m_IsExpanded, value); }
    }
    public bool IsSelected
    {
      get { return m_IsSelected; }
      set { SetProperty(ref m_IsSelected, value); }
    }
    public bool IsEnabled
    {
      get { return m_IsEnabled; }
      set { SetProperty(ref m_IsEnabled, value); }
    }
    public Visibility Visibility
    {
      get { return m_Visibility; }
      set { SetProperty(ref m_Visibility, value); }
    }
    public string NodeTitle
    {
      get
      {
        PersonModel item = Item;
        if (item == null)
        {
          string customTitle = CustomTitle;
          if (string.IsNullOrEmpty(customTitle))
          {
            return null;
          }
          return customTitle;
        }
        return item.ToString();
      }
    }
    public bool IsVisible
    {
      get { return (Visibility == Visibility.Visible); }
    }
    public bool IsAvailable
    {
      get { return IsEnabled && IsVisible; }
    }
    public int ChildrenCount
    {
      get
      {
        IList<PersonTreeNode> children = Children;
        if (children == null)
        {
          return 0;
        }
        return children.Count + children.Sum(x => x.ChildrenCount);
      }
    }


    public override string ToString()
    {
      return NodeTitle ?? base.ToString();
    }
  }
}

Všimněte si, že je třída PersonTreeNode odvozena od BindableBase. To je kvůli tomu, abychom mohli definovat property, na které bude moct reagovat GUI. Zde je jinak velký prostor pro úpravy a rozšíření dle vlastních potřeb a představ…

No a dále pokračujeme na definici samotného TreeView v XAMLu:


<TreeView ItemsSource="{Binding Path=Nodes}">
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Path=Children}"
                              DataType="{x:Type netcorex:PersonTreeNode}">
      <TextBlock Text="{Binding Path=NodeTitle}" />
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsExpanded"
              Value="{Binding Path=IsExpanded}" />
      <Setter Property="IsSelected"
              Value="{Binding Path=IsSelected}" />
      <Setter Property="IsEnabled"
              Value="{Binding Path=IsEnabled}" />
      <Setter Property="Visibility"
              Value="{Binding Path=Visibility}" />
    </Style>
  </TreeView.ItemContainerStyle>
</TreeView>

U definice TreeView je třeba dodat, že v základu stačí jen HierarchicalDataTemplate, pokud ale chcete používat rozšířené možnosti reakcí na GUI, tak je třeba ještě dodat styl ItemContaineru, který v podstatě spáruje vlastnosti jednotlivých PersonTreeNodů.

Potom je třeba možné si udělat jednoduchou extenzi a hromadnou aktualizaci celých větví potažmo celého stromu:


using System;
using System.Collections.Generic;

namespace Netcorex
{
  public static class PersonTreeNodeExtensions
  {
    public static void UpdateBranch(this PersonTreeNode node, Action<PersonTreeNode> updateAction)
    {
      if (node == null)
      {
        throw new ArgumentNullException("node");
      }
      if (updateAction == null)
      {
        throw new ArgumentNullException("updateAction");
      }
      List<PersonTreeNode> children = node.Children;
      if (children != null)
      {
        foreach (PersonTreeNode child in children)
        {
          child.UpdateBranch(updateAction);
        }
      }
      updateAction(node);
    }
  }
}

Hromadnou aktualizaci všech nodů je pak možné vyvolat aplikací extenze a lamda výrazu (akce) na všech kořenových (root) nodech, což zajistí i veškerou další aplikaci do hloubky:


public void Expand()
{
  foreach (PersonTreeNode node in DataContext.Nodes)
  {
    node.UpdateBranch(x => x.IsExpanded = true);
  }
}

public void Collapse()
{
  foreach(PersonTreeNode node in DataContext.Nodes)
  {
    node.UpdateBranch(x => x.IsExpanded = false);
  }
}

Takhle snadno se pak dají provádět operace s nody, takže kromě hromadného rozbalování/zabalování díky IsExpanded klidně i skrývání/odkrývání na základě Visibility nebo zakazování/povolování pomocí IsEnabled atd…

Na závěr už jenom obrázek jako důkaz, že to opravdu funguje, resp. spíš jak to vypadá:

treenodes

… no prostě jako TreeView :)

Dodatek: pro úplnost ještě náhled vzorového DataContextu – PersonsViewModel(u):


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Netcorex
{
  public class PersonsViewModel : BindableBase
  {
    private readonly List<PersonTreeNode> _nodes;


    public PersonsViewModel()
    {
      _nodes = new List<PersonTreeNode>();
      for (int i = 1; i < 4; i++)
      {
        List<PersonTreeNode> children = new List<PersonTreeNode>();
        for (int j = 1; j < 4; j++)
        {
          PersonTreeNode child = new PersonTreeNode(string.Format("Child {0}{1}", i, j));
          children.Add(child);
        }
        PersonTreeNode parent = new PersonTreeNode(string.Format("Parent {0}", i), children);
        _nodes.Add(parent);
      }
    }


    public List<PersonTreeNode> Nodes
    {
      get { return _nodes; }
    }
  }
}

ps: za zde používaný PersonModel si doplňte libovolnou vlastní třídu…

, , , , ,

Komentáře jsou uzavřeny.