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

XTree - 通用实现

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2011年11月30日

CPOL

8分钟阅读

viewsIcon

59206

downloadIcon

1519

重新审视 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;
  }
}

反序列化和填充树

反序列化过程需要填充树(模型-视图-控制器实现中的视图),这是一个使用 dynamicvar 关键字的有趣组合,因为该过程根本不知道后端类的实际类型。

  • 算法从根节点(模式实例)开始。
  • 它遍历集合字典,查找与集合类型匹配的任何节点定义。
  • 遍历集合中的项,然后...
  • 给定节点定义,它实例化适当的控制器并用反序列化的后端类实例对其进行初始化。
  • 最后,对于集合中的每一项,算法都会递归,以便处理子项的集合,从而以树形结构构建对象图。
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 控件实现了移动节点的支持,但通用控制器目前不支持。我正在为此努力。

结论

还需要做更多工作,你可能想知道为什么我提交一篇代码不完全完善的文章。三个原因

  1. 我想听取社区关于“完善”实际含义的反馈,包括兴趣、需求等方面。
  2. 这实际上是一个更大项目的先驱。我需要编写一些基础性的东西,比如这段代码,这样最终的目标文章就不会被更好的地方描述的实现细节所混淆。
  3. 这更像是一篇架构文章,而不是一个“开箱即用”的解决方案。
© . All rights reserved.