Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

XTree - Part II

4.86/5 (10 votes)
29 May 2006CPOL4 min read 1   607  
A template driven tree control.

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:

Image 1

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.

XML
<?xml version="1.0" encoding="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 TypeControllerApplication Type
SchemaSchemaControllerSchemaDef
TableTableControllerTableDef
FieldFieldControllerFieldDef

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:

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

C#
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:

C#
public abstract class XtreeNodeController : IXtreeNode
{
  protected IXtreeNode parent;
  protected string name;

  /// <summary>
  /// Gets/sets name
  /// </summary>
  public virtual string Name
  {
    get { return name; }
    set { name = value; }
  }

  /// <summary>
  /// Gets/sets parent
  /// </summary>
  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);

    // Make sure indexing is supported by the controller.
    if (oldIdx != -1)
    {
      // If we're moving the node internally to our own parent...
      if (newParent == oldParent)
      {
        // Get the old index.
        AutoDeleteNode(oldParent);

        // If this is before the new insert point,
        // we can delete the old index
        // and insert at idx-1, since everything
        // is shifted back one entry.
        if (oldIdx < idx)
        {
          InsertNode(oldParent, idx - 1);
        }
        else
        {
          // the oldIdx occurs after the new point,
          // so we can delete the old entry 
          // and insert the new one without changing
          // the new index point.
          InsertNode(oldParent, idx);
        }
      }
      else
      {
        // parent is different, so delete our node...
        AutoDeleteNode(oldParent);
        // Insert our field in the new parent.
        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:

C#
// Can only move to another parent of the same type.
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.

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

    /// <summary>
    /// Gets/sets schemaService
    /// </summary>
    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/MethodDescription
NameUsed by the tree to update the node's text. If node editing is enabled, the setter will be called by the tree also.
TableDefUsed by the TableFieldController to reference the TableDef's field collection.
ItemUsed by the node rearranger to get the current item so that its index can be found in the parent.
IndexRequests the index of the child (item) in the collection that the controller is managing.
AddNodeAdds a node to the end of the collection, corresponding to a node in the tree being added at the end.
DeleteNodeDeletes 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.
AutoDeleteNodeThis method is called by the node rearranger, and is a separate method because it should always delete the node from the parent collection.
InsertNodeInserts a node at the specified position in the parent collection. This method is called by the MoveTo base method.
SelectCalled 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:

C#
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.

XML
<?xml version="1.0" encoding="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="&amp;File">
            <DropDownItems>
              <ToolStripMenuItem Text="&amp;New" Click="{App.OnNew}"/>
              <ToolStripSeparator/>
              <ToolStripMenuItem Text="&amp;Open" Click="{App.OnOpen}"/>
              <ToolStripMenuItem Text="&amp;Save" Click="{App.OnSave}"/>
              <ToolStripMenuItem Text="Save &amp;As" Click="{App.OnSaveAs}"/>
              <ToolStripMenuItem Text="E&amp;xit" Click="{App.OnExit}"/>
            </DropDownItems>
          </ToolStripMenuItem>
        </Items>
      </MenuStrip>
    </Controls>
  </Form>
</MyXaml>

License

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