XTree Part I
Introduction
In Part I, I introduced the idea of using a template to describe a tree structure. The primary missing feature was automatic instantiation of the backing object for the node. In this update, that issue has been taken care of, and a couple additional features have been implemented:
- Implemented node rearrange
- Nodes can be recursive
The node rearrange code is slightly modified from Gabe Anguiano's TreeView rearrange article.
To illustrate the new features, I've included a simple schema editor demo program:
Under the Hood
The following discusses how the XTree
definition and the controller works.
The Tree Template
The following XML illustrates the tree template for the above screenshot. Please review the first article for a description of the template file.
="1.0"="utf-8"
<RootNode Name="Root">
<Nodes>
<NodeDef Name="Schema" Text="Schema" IsReadOnly="true"
IsRequired="true" IconFilename="Schema.ico"
TypeName="XTreeDemo.SchemaController, XTreeIIDemo">
<Nodes>
<NodeDef Name="Table" Text="Table" IsReadOnly="true"
IconFilename="Table.ico"
TypeName="XTreeDemo.TableController, XTreeIIDemo">
<ParentPopupItems>
<Popup Text="Add New Table" IsAdd="true" Tag="Add"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete Table" IsRemove="true"/>
</PopupItems>
<Nodes>
<NodeDef Name="TableField" Text="Field" IsReadOnly="true"
IconFilename="Field.ico"
TypeName="XTreeDemo.TableFieldController, XTreeIIDemo">
<ParentPopupItems>
<Popup Text="Add New Field" IsAdd="true" Tag="Add"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete Field" IsRemove="true"/>
</PopupItems>
</NodeDef>
</Nodes>
</NodeDef>
</Nodes>
</NodeDef>
</Nodes>
</RootNode>
Each node now defines a controller that is responsible for managing the application specific object associated with the node. The controller acts as an intermediary between the tree and the application specific content. So, in the schema editor, the controllers mediate the following:
Node Type | Controller | Application Type |
Schema | SchemaController | SchemaDef |
Table | TableController | TableDef |
Field | FieldController | FieldDef |
Recursive Nodes
One of the new features is the ability to support recursive nodes, for example, if you wanted to use the XTree
for designing menus. An example of that kind of node definition is:
<NodeDef Name="Menu Item" Text="Menu Item" IsReadOnly="true"
IsRequired="false" Recurse="true"
TypeName="MenuItemController, MenuEditor">
Note the Recurse="true"
attribute, which tells the XTree to recurse the popup menu items for that node.
The Controllers
Each controller implements the XtreeNodeController
class, which in turn implements the IXtreeNode
interface. The interface has a few new methods in it, primarily to support rearranging of the nodes:
public interface IXtreeNode
{
string Name {get; set;}
IXtreeNode Parent {get; set;}
int IconIndex { get;}
int SelectedIconIndex { get;}
bool AddNode(IXtreeNode parentInstance, string tag);
bool DeleteNode(IXtreeNode parentInstance);
void AutoDeleteNode(IXtreeNode parentInstance);
void Select(TreeNode tn);
bool IsEnabled(string tag, bool defaultState);
void MoveTo(IXtreeNode newParent, IXtreeNode oldParent, int idx);
int Index(object obj);
}
The XtreeNodeController
class is an abstract class, but it provides some default implementations, especially with regards to the MoveTo
method, which is called by the tree when moving a node:
public abstract class XtreeNodeController : IXtreeNode
{
protected IXtreeNode parent;
protected string name;
public virtual string Name
{
get { return name; }
set { name = value; }
}
public IXtreeNode Parent
{
get { return parent; }
set { parent = value; }
}
public virtual int IconIndex
{
get { return 0; }
}
public virtual int SelectedIconIndex
{
get { return 0; }
}
public abstract object Item
{
get;
}
public virtual bool IsEnabled(string tag, bool defaultValue)
{
return defaultValue;
}
public virtual void MoveTo(IXtreeNode newParent,
IXtreeNode oldParent, int idx)
{
int oldIdx = oldParent.Index(this);
if (oldIdx != -1)
{
if (newParent == oldParent)
{
AutoDeleteNode(oldParent);
if (oldIdx < idx)
{
InsertNode(oldParent, idx - 1);
}
else
{
InsertNode(oldParent, idx);
}
}
else
{
AutoDeleteNode(oldParent);
InsertNode(newParent, idx);
}
}
}
public abstract int Index(object item);
public abstract void InsertNode(IXtreeNode parentInstance, int idx);
public abstract bool AddNode(IXtreeNode parentInstance, string tag);
public abstract bool DeleteNode(IXtreeNode parentInstance);
public abstract void AutoDeleteNode(IXtreeNode parentInstance);
public abstract void Select(TreeNode tn);
}
Moving Nodes
A node can have its position changed within the same parent, or it can move between parents implementing the same controller type. This test is done in the OnXTreeDragDrop
event:
if (parentInst.GetType() == movingNodeParentInst.GetType())
Root Controller
The root controller doesn't need to implement a lot, and in fact, several of the methods throw an exception if called. The following code illustrates the schema node, which is the root for the demo. Note how the schema controller implements a property for the application specific SchemaDef
instance.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Xml;
using Clifton.Windows.Forms.XmlTree;
namespace XTreeDemo
{
public class SchemaController : XtreeNodeController
{
protected SchemaDef schemaDef;
public SchemaDef SchemaDef
{
get { return schemaDef; }
set { schemaDef = value; }
}
public override string Name
{
get { return "Schema"; }
set { ;}
}
public override object Item
{
get { return schemaDef; }
}
public SchemaController()
{
}
public SchemaController(SchemaDef schemaDef)
{
this.schemaDef = schemaDef;
}
public override int Index(object item)
{
throw new XmlTreeException("Calling Index for SchemaController
is not permitted.");
}
public override bool AddNode(IXtreeNode parentInstance, string tag)
{
return false;
}
public override bool DeleteNode(IXtreeNode parentInstance)
{
return false;
}
public override void AutoDeleteNode(IXtreeNode parentInstance)
{
throw new XmlTreeException("Calling AutoDeleteNode for
SchemaController is not permitted.");
}
public override void InsertNode(IXtreeNode parentInstance, int idx)
{
throw new XmlTreeException("Calling InsertNode for SchemaController
is not permitted.");
}
public override void Select(TreeNode tn)
{
Program.Properties.SelectedObject = schemaDef;
}
public override void MoveTo(IXtreeNode newParent,
IXtreeNode oldParent, int idx)
{
}
}
}
Child Controller
Now, compare the above code with the TableController
, which manages inserting itself in the parent (the schema) and also manages a collection of fields. This controller implements the abstract/virtual methods you would usually implement for a controller that is a child, and also manages child nodes itself:
Property/Method | Description |
Name | Used by the tree to update the node's text. If node editing is enabled, the setter will be called by the tree also. |
TableDef | Used by the TableFieldController to reference the TableDef 's field collection. |
Item | Used by the node rearranger to get the current item so that its index can be found in the parent. |
Index | Requests the index of the child (item) in the collection that the controller is managing. |
AddNode | Adds a node to the end of the collection, corresponding to a node in the tree being added at the end. |
DeleteNode | Deletes the node from the parent instance collection. This method returns false if deletion is cancelled, so you can use this method with a confirmation dialog. |
AutoDeleteNode | This method is called by the node rearranger, and is a separate method because it should always delete the node from the parent collection. |
InsertNode | Inserts a node at the specified position in the parent collection. This method is called by the MoveTo base method. |
Select | Called by the XTree control, telling the control which control has been selected. The implementation here selects the node into the property grid. |
Note how the child controller implements insertion and deletion. Doing it this way, the child knows what collection in the parent needs to be adjusted, which is important because the parent might be managing several different collections.
Regarding the Index
method, there is only one collection that the TableController
manages. If there were more than one collection, you would have to test the item
against the controllers that manage the different collections, in order to determine the appropriate collection from which to get the index.
Here's the table controller:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Xml;
using Clifton.Tools.Strings;
using Clifton.Windows.Forms.XmlTree;
namespace XTreeDemo
{
public class TableController : XtreeNodeController
{
protected TableDef tableDef;
protected SchemaDef schemaDef;
public override string Name
{
get { return tableDef.Name; }
set {tableDef.Name=value;}
}
public TableDef TableDef
{
get { return tableDef; }
}
public override object Item
{
get { return tableDef; }
}
public TableController()
{
}
public TableController(TableDef tableDef)
{
this.tableDef = tableDef;
}
public override int Index(object item)
{
return tableDef.Fields.IndexOf(
((TableFieldController)item).TableFieldDef);
}
public override bool AddNode(IXtreeNode parentInstance, string tag)
{
schemaDef = ((SchemaController)parentInstance).SchemaDef;
tableDef = new TableDef();
schemaDef.Tables.Add(tableDef);
return true;
}
public override bool DeleteNode(IXtreeNode parentInstance)
{
schemaDef = ((SchemaController)parentInstance).SchemaDef;
schemaDef.Tables.Remove(tableDef);
return true;
}
public override void AutoDeleteNode(IXtreeNode parentInstance)
{
schemaDef = ((SchemaController)parentInstance).SchemaDef;
schemaDef.Tables.Remove(tableDef);
}
public override void InsertNode(IXtreeNode parentInstance, int idx)
{
schemaDef = ((SchemaController)parentInstance).SchemaDef;
schemaDef.Tables.Insert(idx, tableDef);
}
public override void Select(TreeNode tn)
{
Program.Properties.SelectedObject = tableDef;
}
}
}
The FieldController
is very similar, except it throws an exception if the Index
method is called, because it does not have any child collections.
Conclusion
I've found the XTree
control to be very useful in a variety of applications where an object graph needs to be managed. Here are some example applications:
- schema editor
- simple report editor (managing data sets, tables, relationships, etc.)
- workflow management
- state graphs
- menus and menu-like things
- form designers and control object graphs
The XTree
automates the user interface with regards to the construction of the object graph. In MVC parlance, the controllers provide a clean interface between the view (the tree controller) and the model (the application specific objects).
The User Interface
One more thing--the user interface for the demo is coded declaratively, for those interested in how declarative programming works. Note how easy it is to use the new MenuStrip
in .NET 2.0.
="1.0"="utf-8"
<MyXaml xmlns="System.Windows.Forms, System.Windows.Forms,
Version=2.0.0000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
xmlns:app="Clifton.Windows.Forms, Clifton.Windows.Forms"
xmlns:def="Definition"
xmlns:ref="Reference">
<Form Name="SchemaEditor"
Text="XTree Demo"
ClientSize="400, 450"
StartPosition="CenterScreen">
<Controls>
<PropertyGrid def:Name="pgProperties" Dock="Fill"
PropertyValueChanged="{App.OnPropertyValueChanged}"/>
<Splitter Dock="Left" MinSize="150" Width="5"/>
<Panel Dock="Left" Width="150" BackColor="Red" BorderStyle="Fixed3D">
<Controls>
<app:XTree def:Name="sdTree" Dock="Fill" Width="150"
TreeDefinitionFileName="sdtree.xml" FullRowSelect="true"
HideSelection="false"/>
</Controls>
</Panel>
<MenuStrip Dock="Top">
<Items>
<ToolStripMenuItem def:Name="menuFile" Text="&File">
<DropDownItems>
<ToolStripMenuItem Text="&New" Click="{App.OnNew}"/>
<ToolStripSeparator/>
<ToolStripMenuItem Text="&Open" Click="{App.OnOpen}"/>
<ToolStripMenuItem Text="&Save" Click="{App.OnSave}"/>
<ToolStripMenuItem Text="Save &As" Click="{App.OnSaveAs}"/>
<ToolStripMenuItem Text="E&xit" Click="{App.OnExit}"/>
</DropDownItems>
</ToolStripMenuItem>
</Items>
</MenuStrip>
</Controls>
</Form>
</MyXaml>