Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

XTree - A Generic Implementation

5.00/5 (11 votes)
30 Nov 2011CPOL8 min read 58.2K   1.5K  
Revisiting the XTree implementation, using a generic controller.

Introduction

About five years ago I wrote a series of articles on a template-driven XML tree control:

  • Part I: Manipulating the tree, but no backing objects.
  • Part II: Introduced a Controller class to instantiate objects that back tree nodes, as well as a PropertyGrid to edit properties of node instances.
  • Part III: Working with trees and DataSets. Not really relevant to this article.

Over the years, I've used the Xtree class extensively, making only minor modifications. In actual practice though, I noticed that I was writing a Controller class to handle every backing instance class. So, for example, in a schema designer that I wrote, I have 25 Controller classes for handling tables, views, fields, calculated fields, XML fields, matrices, etc. It gets quite burdensome to write a Controller class for every backing instance class, and I've been wanting to write a generic Controller for a while. Turns out there's a significant implementation issue though: how does a generic controller handle multiple child collections? For example, in the schema designer, a view can have fields that map to a table as well as calculated fields.

  • Because each child collection is strongly typed, we can't create a list of collections unless we treat the list type as "object" for the underlying typed collection (does that make sense?).
  • An interface doesn't work, because List<SomeType> cannot be cast to List<IHasCollection>.
  • I was unwilling to use an interface in the collection definition. I want the code in the backing class to be expressed like List<TableFields> rather than List<IHasCollection>. In other words, I don't want the fact that we're working with a generic tree controller to affect the implementation of the backing class.
  • I didn't want to explicitly use Reflection.
  • Base classes to the backing class are not allowed. Only interfaces, because I don't want to restrict a backing class to being derived from a class solely for the view presentation.

These issues appear in both the instantiation of tree nodes as well as the deserialization of an object graph to reconstruct the tree (the view). In fact, the deserialization is particularly challenging because the object graph (essentially the model), once deserialized, must be iterated for each of the entries at a particular level and recursed through for children of each entry to reconstruct the tree nodes.

Implementation Tradeoff

I wanted as minimal an impact on the backing class as possible. I'm not convinced I've achieved this, but it's close enough. The basic idea is that the backing class must provide information on the collections that it maintains. This could be handled by reflecting through public properties and looking for an attribute decorating the property that indicates that this is a serializable collection. Alternatively, the backing class itself could create the list of collections and provide support for the property to access this list of collections. This is the approach that I ended up taking, because of its simplicity.

Thus, we have a simple interface:

C#
public interface IHasCollection
{
  string Name { get; set; }
  Dictionary<string, dynamic> Collection { get; }
}

which requires that the backing class provides a Name property (so something can be populated in the tree node, this is not a huge issue because most things already have names in these object graphs), and secondly, the list of collections. Note that this list is actually implemented as a dictionary. The key field allows us to access a specific collection, and the value field, being dynamic, allows us to access methods on the collection itself, something you can't do if the dictionary was defined like this: Dictionary<string, object> Collection { get; }. Yes, this hides explicit Reflection calls, but the code is sexier!

Implementation Example

So, for example, for another article that I'm working on, I want to be able to manage a graph of entities, relationships, and attributes. So, my top level schema defines containers for these collections (and allows different named containers of a specific type):

C#
[Browsable(false)]
public List<RelationshipsContainer> Relationships { get; set; }

[Browsable(false)]
public List<AttributesContainer> Attributes { get; set; }

[Browsable(false)]
public List<EntitiesContainer> Entities { get; set; }

Since the Schema class is the root level of the tree, it is itself derived from IHasCollection, and therefore implements the interface properties:

C#
public class Schema : IHasCollection
{
[XmlAttribute()]
public string Name { get; set; }

[XmlIgnore]
[Browsable(false)]
public Dictionary<string, dynamic> Collection { get; protected set; }

And lastly, in the constructor, the Collection property is initialized.

C#
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},
  };
}

This part of the code I think could be handled better--the string key has to match the collection type, so the code is prone to typo's and the Collection could be initialized in a smarter way to avoid this issue. However, I'm considering the best approach for this, for example, a Collection factory implemented through an extension method would remove this code completely, requiring only that the class implement the Collection property. I'm open to suggestions!

The Tree Definition

The definition of the tree, in XML, needs to mirror the backing object graph and uses Reflection to instantiate the instances at runtime. The instances are added to the appropriate collection by the generic controller, which I'll show next, but first, let's take a quick tour of the XML that defines the tree structure. Because I'm not finished with the article that I actually want to write, this implementation is simple, but hopefully illustrative.

A snippet, so as not to overwhelm you with XML:

XML
<?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>

The most relevant part of the above XML is the TypeName attribute, which defines the specific backing class type to instantiate. By itself, this will create a simple tree with the required nodes (note the IsRequired attribute).

The Xtree class handles the popup menus, instantiation of the appropriate controller type, and initialization of the tree. The generic controller handles the instantiation of the actual classes backing each tree node as well as adding and removing items from the appropriate collection. We'll look at the generic controller now.

The Generic Controller

This is a snippet of the salient pieces of the generic controller.

  • We have two constructors: the default constructor is used for instantiating a new backing class instance when the user selects "Add..." something from the popup menu. The constructor taking the bool parameter is used to create the controller after deserializing an existing graph, when the backing class instance has already been created.
  • The property GenericTypeName parses the type being managed by the collection, which represents a controller associated with the child collection. Thus, the EntitiesContainer class has a collection of Entity objects, and when the user adds a node of this controller type, we can determine the collection instance type by parsing the type name. This could also be done by inspecting the type Instance, but I chose to work with the controller type instead.
  • The GenericTypeName is used as the key to index the dictionary of collections so that the instance that was created is added to the correct parent collection.
  • Because the dictionary value is of type dynamic, we can call the Add/Remove methods for strongly typed, generic instances, letting System.Core handle the Reflection messiness.
  • I don't much care about the performance hit of using the dynamic keyword because this code is called only during the creation of nodes (interacting with the user) and during deserialization, a one-time event when loading a graph.
C#
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;
  }
}

Deserialization and Populating the Tree

The deserialization process requires populating the tree (the View in our Model-View-Controller implementation), which is an interesting mix of using the dynamic and var keywords, since the process has no clue as to the actual types of the backing classes.

  • The algorithm starts with the root node, the schema instance.
  • It iterates through the dictionary of collections, finding any node definition that matches the type of the collection.
  • The items in the collection are iterated through, and...
  • Given the node definition, it instantiates the appropriate controller and initializes it with the deserialized backing class instance.
  • Lastly, for each item in the collection of items, the algorithm recurses, so that collections of the child can be processed, thus building the object graph in a tree representation.
C#
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];
}

Other Miscellaneous Things Going On

  • The XML declaring the tree structure is parsed using my MycroXaml parser.
  • The UI itself is instantiated using MyXaml.
  • Because of the use of the dynamic keyword, Visual Studio 2010 with .NET 4.0 is required.
  • The names of classes and their organization with relation to the demo and the actual Xtree controller could be improved.
  • What is ROP? This is a small schema that I want to use for an article on Relationship Oriented Programming, which I haven't written yet?
  • Support for moving nodes around, while implemented by the Xtree control, is not currently supported by the generic controller. I'm working on that.

Conclusion

More work needs to be done, and you might be wondering why I'm submitting an article with code that isn't fully polished. Three reasons:

  1. I'd like to get feedback from the community as to what "polished" actually means, in terms of interest, requirements, and so forth.
  2. This is actually a precursor to a much larger project. I need to get some foundational aspects, such as this code, written so that the article that is the end goal is not muddled with implementation details better described elsewhere.
  3. This is actually more of an architecture article than a "use it right out of the box" solution.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)