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
DataSet
s. 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:
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):
[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:
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.
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:
="1.0"="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.
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
{
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)
{
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.
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)
{
foreach (var kvp in collection.Collection)
{
string collectionName = kvp.Key;
var collectionItems = kvp.Value;
NodeDef nodeDef = node.Nodes.Find(t => t.TypeName.Contains(collectionName));
foreach (var item in collectionItems)
{
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();
sdTree.Nodes[0].Nodes.Clear();
NodeDef nodeDef=sdTree.RootNode.Nodes[0];
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:
- I'd like to get feedback from the community as to what "polished" actually means, in terms of interest, requirements, and so forth.
- 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.
- This is actually more of an architecture article than a "use it right out of the box" solution.