一个可观察的泛型树集合






4.58/5 (12投票s)
简单的泛型数据结构,用于维护分层对象
引言
在本技巧中,我将描述一个基于 ObservableCollection
的基本 Tree
结构,其中每个元素都可以包含同类型的子元素。嵌套级别数量不受限制。当您需要存储或操作分层数据时,此类集合可能非常有用。由于该集合内部基于 ObservableCollection
类,因此该树非常适合 WPF 绑定场景。例如,在附加的示例项目中,我使用它通过 HierarchicalDataTemplate
在 TreeView
中显示数据。由于 **WPF 数据绑定**,原始对象图的任何修改都会自动反映在 UI 中。
此 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>>
。这表明树的任何元素都可以拥有自己的子元素。因此,您可以轻松地遍历树元素,而无需担心其中是否存在子元素。在使用 Linq
或 foreach
结构时,它还提供了语法糖。
以下是 ITree<T>
的方法和属性
- ?
HasItems
- 如果树节点包含任何子元素,则返回true
。 Add
- 将对象添加到子集合。请注意,该方法接受对象类型作为参数。这是为了支持您可以一次添加多个对象的场景。我将在稍后演示这一点。Data
- 返回与当前节点关联的泛型数据对象。Items
- 将子元素集合公开为IList<T>
。您可以使用此属性来添加、删除、替换、重置子元素。GetParents()
- 一个枚举所有父节点(如果存在)的方法。可以选择性地将其本身包含在结果中。GetChildren()
- 一个枚举所有子节点(如果存在)的方法。可以选择性地将其本身包含在结果中。
实现
Tree
类实现了上面描述的 ITree
和 ITree<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
实例作为子节点的容器。ObservableCollection
的 CollectionChanged?
事件会内部挂接到一个处理程序,该处理程序为被添加或删除的子节点分配或取消分配 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>
特性和功能