XTree - 通用实现





5.00/5 (11投票s)
重新审视 XTree 实现,使用通用控制器。
引言
大约五年前,我写了一系列关于模板驱动的 XML 树控件的文章
- 第一部分:操作树,但没有后端对象。
- 第二部分:引入了一个 Controller 类来实例化支持树节点的对象,以及一个
PropertyGrid
来编辑节点实例的属性。 - 第三部分:处理树和
DataSet
。与本文关系不大。
多年来,我广泛使用了 Xtree
类,只做了一些小小的修改。但在实际应用中,我发现我为每种后端实例类编写一个 Controller 类。例如,在我写的一个模式设计器中,我有 25 个 Controller 类来处理表、视图、字段、计算字段、XML 字段、矩阵等。为每种后端实例类编写一个 Controller 类非常繁琐,我一直想编写一个通用的 Controller。结果发现有一个重大的实现问题:通用 Controller 如何处理多个子集合?例如,在模式设计器中,一个视图可以有映射到表的字段,也可以有计算字段。
- 由于每个子集合都是强类型的,除非我们将列表类型视为“object”来处理底层类型化集合(这说得通吗?),否则我们无法创建集合列表。
- 接口不起作用,因为
List<SomeType>
无法转换为List<IHasCollection>
。 - 我不想在集合定义中使用接口。我希望后端类中的代码写成
List<TableFields>
而不是List<IHasCollection>
。换句话说,我不想让处理通用树控制器这一事实影响后端类的实现。 - 我不想显式使用反射。
- 不允许使用后端类的基类。只能使用接口,因为我不想限制后端类只能继承一个类来呈现视图。
这些问题既出现在树节点的实例化中,也出现在将对象图反序列化以重建树(视图)时。事实上,反序列化尤其具有挑战性,因为反序列化后的对象图(本质上是模型)必须遍历特定级别的每个条目,并递归遍历每个条目的子项以重建树节点。
实现权衡
我希望对后端类尽可能地少影响。我并不确定我已经实现了这一点,但已经足够接近了。基本思想是后端类必须提供其维护的集合的信息。这可以通过反射公共属性来处理,并查找装饰该属性的属性,该属性指示这是一个可序列化的集合。或者,后端类本身可以创建集合列表,并为访问此集合列表的属性提供支持。我最终采用了这种方法,因为它很简单。
因此,我们有一个简单的接口
public interface IHasCollection
{
string Name { get; set; }
Dictionary<string, dynamic> Collection { get; }
}
该接口要求后端类提供一个 Name
属性(这样树节点中可以填充一些内容,这并不是一个大问题,因为大多数对象图中的内容已经有名称),其次是集合列表。请注意,此列表实际上是作为字典实现的。键字段允许我们访问特定的集合,而值字段(动态类型)允许我们访问集合本身的方法,这是无法实现的,如果字典定义如下:Dictionary<string, object> Collection { get; }
。是的,这隐藏了显式的反射调用,但代码更炫!
实现示例
所以,例如,对于我正在开发的另一篇文章,我想能够管理实体、关系和属性的图。因此,我的顶层模式定义了这些集合的容器(并允许具有特定类型的不同命名容器)
[Browsable(false)]
public List<RelationshipsContainer> Relationships { get; set; }
[Browsable(false)]
public List<AttributesContainer> Attributes { get; set; }
[Browsable(false)]
public List<EntitiesContainer> Entities { get; set; }
由于 Schema
类是树的根级别,它本身就继承自 IHasCollection
,因此实现了接口属性
public class Schema : IHasCollection
{
[XmlAttribute()]
public string Name { get; set; }
[XmlIgnore]
[Browsable(false)]
public Dictionary<string, dynamic> Collection { get; protected set; }
最后,在构造函数中,初始化了 Collection
属性。
public Schema()
{
Relationships = new List<RelationshipsContainer>();
Attributes = new List<AttributesContainer>();
Entities = new List<EntitiesContainer>();
Collection = new Dictionary<string, dynamic>() {
{"EntitiesContainer", Entities},
{"AttributesContainer", Attributes},
{"RelationshipsContainer", Relationships},
};
}
我认为代码的这部分可以做得更好——字符串键必须与集合类型匹配,因此代码容易出错,并且 Collection
可以以更智能的方式初始化以避免此问题。但是,我正在考虑为此的最佳方法,例如,通过扩展方法实现的 Collection
工厂将完全消除此代码,只需要类实现 Collection
属性即可。我乐意接受建议!
树定义
XML 中的树定义需要镜像后端对象图,并在运行时使用反射来实例化实例。这些实例由通用控制器添加到相应的集合中,我将在下面展示,但首先,让我们快速浏览一下定义树结构的 XML。因为我还没有完成我真正想写的文章,所以这个实现很简单,但希望能说明问题。
一个片段,以免 XML overwhelm 你
<?xml version="1.0" encoding="utf-8"?>
<RootNode Name="Root">
<Nodes>
<NodeDef Name="ROP" Text="ROP" IsReadOnly="true" IsRequired="true"
TypeName="XTreeDemo.GenericController`1[[ROPLib.Schema, ROPLib,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], XTreeIIDemo">
<Nodes>
<NodeDef Name="Entities"
Text="Entities"
IsReadOnly="true"
IsRequired="true"
TypeName="XTreeDemo.GenericController`1[[ROPLib.EntitiesContainer,
ROPLib, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]], XTreeIIDemo">
<ParentPopupItems>
<Popup Text="Add New Entity Collection" IsAdd="true" Tag="Add"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete Entity Collection" IsRemove="true"/>
</PopupItems>
<Nodes>
<NodeDef Name="Entity"
Text="Entity"
IsReadOnly="true"
TypeName="XTreeDemo.GenericController`1[[ROPLib.Entity,
ROPLib, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]], XTreeIIDemo">
<ParentPopupItems>
<Popup Text="Add New Entity" IsAdd="true" Tag="Add"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete Entity" IsRemove="true"/>
</PopupItems>
<Nodes/>
</NodeDef>
</Nodes>
</NodeDef>
<NodeDef Name="Attributes"
Text="Attributes"
IsReadOnly="true"
IsRequired="true"
TypeName="XTreeDemo.GenericController`1[[ROPLib.AttributesContainer,
ROPLib, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null]], XTreeIIDemo">
..etc..
</NodeDef>
</Nodes>
</RootNode>
上面 XML 中最相关的是 TypeName
属性,它定义了要实例化的特定后端类类型。仅此一项就将创建一个具有所需节点(注意 IsRequired
属性)的简单树。
Xtree
类处理弹出菜单、相应控制器类型的实例化以及树的初始化。通用控制器处理每个树节点后端类的实际实例化,以及从相应集合中添加和删除项。我们现在来看看通用控制器。
通用控制器
这是通用控制器重要部分的片段。
- 我们有两个构造函数:默认构造函数用于在用户从弹出菜单中选择“添加...”时实例化一个新的后端类实例。带布尔参数的构造函数用于在反序列化现有图后创建控制器,此时后端类实例已经创建。
GenericTypeName
属性解析集合所管理的对象类型,该类型代表与子集合关联的控制器。因此,EntitiesContainer
类有一个Entity
对象集合,当用户添加此控制器类型的节点时,我们可以通过解析类型名称来确定集合实例类型。这也可以通过检查Instance
类型来完成,但我选择处理控制器类型。GenericTypeName
用作索引集合字典的键,以便创建的实例被添加到正确的父集合中。- 由于字典值是
dynamic
类型,我们可以调用强类型的通用实例的 Add/Remove 方法,让System.Core
处理反射的混乱。 - 我不太在意使用
dynamic
关键字的性能影响,因为这段代码仅在节点创建期间(与用户交互)和反序列化(加载图时的单次事件)期间调用。
public class GenericController<T> :
XtreeNodeController, IGenericController
where T : IHasCollection, new()
{
public T Instance { get; set; }
public Dictionary<string, dynamic> Collection { get { return Instance.Collection; } }
public string GenericTypeName
{
get
{
// Example: XTreeDemo.GenericController`1[[ROPLib.Entity, ROPLib,
// Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
string fulltype = this.GetType().FullName;
string typeName = fulltype.RightOf('.').Between('.', ',');
return typeName;
}
}
public GenericController()
{
Instance = new T();
}
public GenericController(bool createInstance)
{
if (createInstance)
{
Instance = new T();
}
}
public override int Index(object item)
{
return Instance.Collection[GenericTypeName].IndexOf((T)item);
}
public override bool AddNode(IXtreeNode parentInstance, string tag)
{
IGenericController ctrl = (IGenericController)parentInstance;
ctrl.Collection[GenericTypeName].Add(Instance);
return true;
}
public override bool DeleteNode(IXtreeNode parentInstance)
{
// TODO: Inject the ability to confirm the delete operation.
IGenericController ctrl = (IGenericController)parentInstance;
ctrl.Collection[GenericTypeName].Remove(Instance);
return true;
}
}
反序列化和填充树
反序列化过程需要填充树(模型-视图-控制器实现中的视图),这是一个使用 dynamic
和 var
关键字的有趣组合,因为该过程根本不知道后端类的实际类型。
- 算法从根节点(模式实例)开始。
- 它遍历集合字典,查找与集合类型匹配的任何节点定义。
- 遍历集合中的项,然后...
- 给定节点定义,它实例化适当的控制器并用反序列化的后端类实例对其进行初始化。
- 最后,对于集合中的每一项,算法都会递归,以便处理子项的集合,从而以树形结构构建对象图。
protected void Load()
{
XmlSerializer xs = new XmlSerializer(typeof(Schema));
StreamReader sr = new StreamReader(schemaFilename);
schemaDef = (Schema)xs.Deserialize(sr);
((GenericController<Schema>)((NodeInstance)rootNode.Tag).Instance).Instance = schemaDef;
sr.Close();
PopulateTree();
}
protected void RecurseCollection(NodeDef node, dynamic collection, TreeNode tnCurrent)
{
if (node.Nodes.Count > 0)
{
// Collection is a Dictionary<string, dynamic> where dynamic is a List<T>
// obj is a KeyValuePair<string, dynamic>
foreach (var kvp in collection.Collection)
{
string collectionName = kvp.Key;
var collectionItems = kvp.Value;
// Doesn't matter what nodeDef we find, this is only
// to get the TypeName and number of child nodes on recursion.
// But it does allow us to separate the serialization
// order from the tree definition order.
NodeDef nodeDef = node.Nodes.Find(t => t.TypeName.Contains(collectionName));
foreach (var item in collectionItems)
{
// Do not create new instances for the items,
// as they have already been created!
IXtreeNode controller =
(IXtreeNode)Activator.CreateInstance(
Type.GetType(nodeDef.TypeName), new object[] {false});
controller.Item = item;
TreeNode tn = sdTree.AddNode(controller, tnCurrent);
string name = ((IHasCollection)item).Name;
tn.Text = (String.IsNullOrWhiteSpace(name) ? tn.Text : name);
RecurseCollection(nodeDef, item, tn);
}
}
}
}
protected void PopulateTree()
{
sdTree.SuspendLayout();
// Remove all existing (such as required) nodes.
sdTree.Nodes[0].Nodes.Clear();
NodeDef nodeDef=sdTree.RootNode.Nodes[0]; // Get the child of the top level node.
RecurseCollection(nodeDef, schemaDef, rootNode);
sdTree.CollapseAll();
sdTree.Nodes[0].Expand();
sdTree.ResumeLayout();
pgProperties.SelectedObject = schemaDef;
sdTree.SelectedNode = sdTree.Nodes[0];
}
其他杂项
- 声明树结构的 XML 使用我的 MycroXaml 解析器进行解析。
- UI 本身使用 MyXaml 实例化。
- 由于使用了
dynamic
关键字,因此需要 Visual Studio 2010 和 .NET 4.0。 - 类名及其与演示文稿和实际
Xtree
控制器的组织方式可以改进。 - 什么是 ROP?这是一个我计划用于“关系导向编程”文章的小型模式,我还没有写?
Xtree
控件实现了移动节点的支持,但通用控制器目前不支持。我正在为此努力。
结论
还需要做更多工作,你可能想知道为什么我提交一篇代码不完全完善的文章。三个原因
- 我想听取社区关于“完善”实际含义的反馈,包括兴趣、需求等方面。
- 这实际上是一个更大项目的先驱。我需要编写一些基础性的东西,比如这段代码,这样最终的目标文章就不会被更好的地方描述的实现细节所混淆。
- 这更像是一篇架构文章,而不是一个“开箱即用”的解决方案。