65.9K
CodeProject 正在变化。 阅读更多。
Home

一个可观察的泛型树集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (12投票s)

2015年11月11日

CPOL

4分钟阅读

viewsIcon

17661

downloadIcon

1166

简单的泛型数据结构,用于维护分层对象

引言

在本技巧中,我将描述一个基于 ObservableCollection 的基本 Tree 结构,其中每个元素都可以包含同类型的子元素。嵌套级别数量不受限制。当您需要存储或操作分层数据时,此类集合可能非常有用。由于该集合内部基于 ObservableCollection 类,因此该树非常适合 WPF 绑定场景。例如,在附加的示例项目中,我使用它通过 HierarchicalDataTemplateTreeView 中显示数据。由于 **WPF 数据绑定**,原始对象图的任何修改都会自动反映在 UI 中。

Demo App

Tree 实现的另一个重要方面是,我的目标是使代码尽可能紧凑,并尽可能依赖于 ObservableCollection 的原始实现。因此,所有对树的操作,如添加、删除、替换节点等,都应通过标准的 IList<T> 的常用方法和属性来执行。

接口

Tree 类实现了以下接口

public interface ITree
{
    bool HasItems { get; }
    void Add(object data);
}

public interface ITree<T> : ITree, IEnumerable<ITree<T>>
{
    T Data { get; }
    IList<ITree<T>> Items { get; }
    ITree<T> Parent { get; }
    IEnumerable<ITree<T>> GetParents(bool includingThis);
    IEnumerable<ITree<T>> GetChildren(bool includingThis);
}

ITree 是非泛型基接口。泛型接口 ITree<T> 重写了 ITree,并公开了涉及泛型类型的附加属性和方法。将接口分为非泛型和泛型的原因是为了能够在 Add 方法中轻松识别与 ITree 元素兼容的实例。

请注意,ITree<T> 也派生自 IEnumerable<ITree<T>>。这表明树的任何元素都可以拥有自己的子元素。因此,您可以轻松地遍历树元素,而无需担心其中是否存在子元素。在使用 Linqforeach 结构时,它还提供了语法糖。

以下是 ITree<T> 的方法和属性

  • ?HasItems - 如果树节点包含任何子元素,则返回 true
  • Add - 将对象添加到子集合。请注意,该方法接受对象类型作为参数。这是为了支持您可以一次添加多个对象的场景。我将在稍后演示这一点。
  • Data - 返回与当前节点关联的泛型数据对象。
  • Items - 将子元素集合公开为 IList<T>。您可以使用此属性来添加、删除、替换、重置子元素。
  • GetParents() - 一个枚举所有父节点(如果存在)的方法。可以选择性地将其本身包含在结果中。
  • GetChildren() - 一个枚举所有子节点(如果存在)的方法。可以选择性地将其本身包含在结果中。

实现

Tree 类实现了上面描述的 ITreeITree<T> 接口。在构造函数中,您可以传递泛型 Data 对象以及可选的子节点。当传递子节点时,它们会使用以下 Add 方法添加

public void Add(object data)
{
    if (data == null)
        return;

    if (data is T)
    { 
        Items.Add(CloneNode(new Tree<T>((T) data)));
        return;
    }

    var t = data as ITree<T>;
    if (t != null)
    {
        Items.Add(CloneTree(t));
        return;
    }

    var o = data as object[];
    if (o != null)
    {
        foreach (var obj in o)
            Add(obj);
        return;
    }

    var e = data as IEnumerable;
    if (e != null && !(data is ITree))
    {
        foreach (var obj in e)
            Add(obj);
        return;
    }

    throw new InvalidOperationException("Cannot add unknown content type.");
}

如您所见,每个子元素在添加到集合之前都会被克隆。这是为了避免可能导致不可预测行为的循环引用。因此,如果您想用自己的版本覆盖 Tree 类(例如,您可能想创建一个隐藏泛型类型的类,如:class PersonTree : Tree<Person>),您将需要覆盖虚拟 CloneNode 方法,返回您的类型的适当实例。

Add 方法的另一个方面是,只要其元素类型为 ITree<T> 或简单地为 T,它就可以处理任何类型的 IEnumerable 或其派生类。以下是一个使用各种方法构造的 ITree<string> 示例

var tree = Tree.Create("My Soccer Leagues",
    Tree.Create("League A",
        Tree.Create("Division A", 
            "Team 1", 
            "Team 2", 
            "Team 3"),
        Tree.Create("Division B", new List<string> {
            "Team 4", 
            "Team 5", 
            "Team 6"}),
        Tree.Create("Division C", new List<ITree<string>> {
            Tree.Create("Team 7"), 
            Tree.Create("Team 8")})),
    Tree.Create("League B",
        Tree.Create("Division A", 
            new Tree<string>("Team 9"), 
            new Tree<string>("Team 10"), 
            new Tree<string>("Team 11")),
        Tree.Create("Division B", 
            Tree.Create("Team 12"), 
            Tree.Create("Team 13"), 
            Tree.Create("Team 14"))));

最后是 Parent 属性的实现。此属性对任何分层数据结构都至关重要,因为它允许您向上遍历树,从而访问根节点。默认情况下,节点的 Items 集合未初始化(m_items = null)。一旦您尝试访问 Items 属性或向其添加元素,就会创建一个 ObservableCollection 实例作为子节点的容器。ObservableCollectionCollectionChanged? 事件会内部挂接到一个处理程序,该处理程序为被添加或删除的子节点分配或取消分配 Parent 属性。代码如下所示

public IList<ITree<T>> Items
{
    get
    {
        if (m_items == null)
        {
            m_items = new ObservableCollection<ITree<T>>();
            m_items.CollectionChanged += ItemsOnCollectionChanged;
        }
        return m_items;
    }
}

private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
    if (args.Action == NotifyCollectionChangedAction.Add && args.NewItems != null)
    {
        foreach (var item in args.NewItems.Cast<Tree<T>>())
        {
            item.Parent = this;
        }
    }
    else if (args.Action != NotifyCollectionChangedAction.Move && args.OldItems != null)
    {
        foreach (var item in args.OldItems.Cast<Tree<T>>())
        {
            item.Parent = null;
            item.ResetOnCollectionChangedEvent();
        }
    }
}

private void ResetOnCollectionChangedEvent()
{
    if (m_items != null)
        m_items.CollectionChanged -= ItemsOnCollectionChanged;
}

辅助 Tree 类

辅助 static Tree 类提供了创建树节点的语法糖。例如,而不是编写

var tree = new Tree<Person>(new Person("Root"),
    new Tree<Person>(new Person("Child #1")),
    new Tree<Person>(new Person("Child #2")),
    new Tree<Person>(new Person("Child #3")));

您可以以稍微更简洁的方式表达相同的代码

var tree = Tree.Create(new Person("Root"),
    Tree.Create(new Person("Child #1")),
    Tree.Create(new Person("Child #2")),
    Tree.Create(new Person("Child #3")));

此外,还有一个辅助 Visit 方法。它接受一个树节点 ITree<T> 和一个 Action<ITree<T>>,递归地遍历树节点并为每个节点调用该操作

public static void Visit<T>(ITree<T> tree, Action<ITree<T>> action)
{
    action(tree);
    if (tree.HasItems)
        foreach (var item in tree)
            Visit(item, action);
}

例如,您可以使用此方法打印一棵树

public static string DumpElement(ITree<string> element)
{
    var sb = new StringBuilder();
    var offset = element.GetParents(false).Count();
    Tree.Visit(element, x =>
    {
        sb.Append(new string(' ', x.GetParents(false).Count() - offset));
        sb.AppendLine(x.ToString());
    });
    return sb.ToString();
}

摘要

Tree<T> 集合是一个简单的分层数据结构,具有以下特点

  • 支持泛型数据类型
  • 自动维护到 Parent 节点的引用
  • 对 WPF 绑定友好
  • 构造和访问元素的流畅语法
  • 利用标准的 .NET IList<T> 特性和功能
© . All rights reserved.