Introduction
This is a .NET 2.0 article (I wonder how long it will be before that becomes unnecessary to say).
Whenever I use a TreeView
, I get frustrated with the amount of application specific coding that I have to do. I've been mulling about an XML-based template that defines the master tree:
- defining node "types",
- specifying the root node type,
- given a node type, what are the allowable child nodes,
- auto-generating a context menu from the text in the XML defined in the node type,
- the icon file specified in the template for the node type,
- specifying the required child nodes when a node is added,
- automatically adding/removing nodes from the tree based on the XML attributes for the context menu items,
- having the entire process work without the backing objects to provide application specific node implementation,
- specifying whether a node can be edited or not,
- serializing the tree,
- allowing recursive node types, like folder-folder-folder...,
- automatically instantiate the backing object for a node type.
In this first cut, I've managed to put together everything except the last bullet item. That will be discussed in a separate article, as it can get complicated.
Of course, with any general purpose solution, customization of the look and function becomes more difficult. I haven't explored where all the hooks need to go to make this class truly customizable. I'll be looking at that later. For now, I thought I'll publish this as is and get feedback from you on the kind of features this should have, etc. As with many things I publish, this is a work in progress! I'm looking at peterchen's generic tree to see how his work might be used to map concrete, application-specific objects that back the actual nodes and thus also separate the view from the model (not to mention, I probably ought to have a separate controller as well). As it stands right now, the model, view, and the controller are all integrated into a single class.
How It Works
The XTree
class is derived from the TreeView
.
Parsing the template
When the Initialize
method is called with an XmlDocument
template file, the object graph defining the tree template is instantiated. I'm using my MycroXaml parser to instantiate the object graph (the MycroXaml
parser in this source code is much more elaborate than the one presented in the original article). Once the object graph is instantiated, the XTree
instance initializes the root of the tree:
public void Initialize(XmlDocument xdoc)
{
nodeList = new Dictionary();
ImageList = new ImageList();
MycroParser mp = new MycroParser();/>
mp.Load(xdoc, null, null);
mp.NamespaceMap[""] = "Clifton.Windows.Forms.XmlTree,
Clifton.Windows.Forms";
rootNode=(Node)mp.Process();
nodeList[rootNode.Name] = rootNode;
BuildFlatNodeList(rootNode);
TreeNode tn=CreateNode(rootNode); Nodes.Add(tn);
}
Populating a Context Menu
Now, when you right-click on the root (or any node), the object inspects the popup items defined in the selected node's template definition and adds them to the context menu. Next, it inspects all the child nodes and adds the menu items that are found in the ParentPopupItems
collection for that child node. Menu separators are placed between each grouping:
public ContextMenu BuildContextMenu()
{
ContextMenu cm = new ContextMenu();
TreeNode tn = GetNodeAt(mousePos);
Node n=(Node)tn.Tag;
bool first = true;
if (n.PopupItems.Count != 0)
{
foreach (Popup popup in n.PopupItems)
{
NodeMenuItem nmi = new NodeMenuItem(popup.Text,
tn, n, null, popup);
nmi.Click += new EventHandler(OnContextItem);
cm.MenuItems.Add(nmi);
}
first = false;
}
foreach (Node child in n.Nodes)
{
Node refNode = child;
if (child.IsRef)
{
if (!nodeList.ContainsKey(child.RefName))
{
throw new ApplicationException(
"referenced node does not exist.");
}
refNode = nodeList[child.RefName];
}
if (refNode.ParentPopupItems.Count != 0)
{
if (!first)
{
cm.MenuItems.Add("-");
}
first = false;
foreach (Popup popup in refNode.ParentPopupItems)
{
NodeMenuItem nmi = new NodeMenuItem(popup.Text,
tn, n, refNode, popup);
nmi.Click += new EventHandler(OnContextItem);
cm.MenuItems.Add(nmi);
}
}
}
return cm;
}
You will note that if a node is a "reference", then it implements a recursive node and the referenced node is used for the template.
Adding and Removing Nodes
A default click handler is assigned. In the XML template, there are attributes indicating whether the menu item adds the node or removes it:
private void OnContextItem(object sender, EventArgs e)
{
NodeMenuItem nmi = (NodeMenuItem)sender;
if (nmi.PopupInfo.IsAdd)
{
TreeNode tn = CreateNode(nmi.ChildNode);
nmi.TreeNode.Nodes.Add(tn);
tn.Parent.Expand();
tn.TreeView.SelectedNode = tn;
}
else if (nmi.PopupInfo.IsRemove)
{
nmi.TreeNode.Remove();
}
}
Now, of course, in a real application, this would be backed by real functionality. In my prototype though, it simply wants the node added or removed. The node being added is of the correct node type, and therefore its context menu is specific to it.
The XML Template
The template that defines the tree consists of Node
tags, backed by the Node
class. A node can have three different collections:
ParentPopupItems
- a collection of one or more popup tags that define the menu items for the parent node. PopupItems
- a collection of one or more popup tags that define the menu items for the current node. Nodes
- a collection of one or more child nodes.
The root node does not have a ParentPopupItems
collection.
A Node
tag can have the following attributes (backed by properties in the Node
class):
Name
- the name of the node Text
- the text displayed in the tree node IsRequired
- indicates that this node is always created IconFilename
- the icon filename for this node type RefName
- indicates that this node is actually a reference to a node defined higher up in the hierarchy. This allows for recursive, infinite, tree depth. IsReadOnly
- indicates whether the node text can be edited
A Popup
tag, backed by the Popup
class, can have the following attributes:
Text
- the menu item text IsAdd
- true
, if selecting this menu item, adds the associated node template IsRemove
- true
, if selecting this menu item, removes the associated node instance
Given the XML supplied in the demo, the Node
and Popup
classes were 90% auto-generated using my XML To Class generator.
XML Serialization
The caveat to XML serialization is that the template node name is being used as the XML tag, so it has to comply with the XML requirements - no white spaces, etc. The question really is though, should the tree be responsible for serialization? The answer is "sort of". If we look at this from an MVC perspective, the tree embodies both the view and the model. What we really want is a tree in which the model, the node hierarchy, is separated from the view. However, since the TreeView
does not accept a data source (ironically, the ASP.NET TreeView
in .NET 2.0 does support a data source), we're stuck with implementing a proper view-model separation ourselves. Later. And yes, the filename is hard-coded as well - this is a concept piece, after all.
To serialize, the tree is traversed:
public void Serialize(string fn)
{
XmlDocument xdoc = new XmlDocument();
XmlDeclaration xmlDeclaration =
xdoc.CreateXmlDeclaration("1.0", "utf-8", null);
xdoc.InsertBefore(xmlDeclaration, xdoc.DocumentElement);
XmlNode xnode = xdoc.CreateElement("XTree");
xdoc.AppendChild(xnode);
foreach (TreeNode tn in Nodes)
{
WriteNode(xdoc, xnode, tn);
}
xdoc.Save("tree.xml");
}
protected void WriteNode(XmlDocument xdoc,
XmlNode xnode, TreeNode tn)
{
XmlNode xn = xdoc.CreateElement(((Node)tn.Tag).Name);
xn.Attributes.Append(xdoc.CreateAttribute("Text"));
xn.Attributes.Append(xdoc.CreateAttribute("IsExpanded"));
xn.Attributes["Text"].Value = tn.Text;
xn.Attributes["IsExpanded"].Value =
tn.IsExpanded.ToString();
xnode.AppendChild(xn);
foreach (TreeNode child in tn.Nodes)
{
WriteNode(xdoc, xn, child);
}
}
Deserialization is the opposite process. Read in the XML nodes and construct the tree:
public void Deserialize(string fn)
{
Nodes.Clear();
XmlDocument xdoc = new XmlDocument();
xdoc.Load("tree.xml");
XmlNode node = xdoc.DocumentElement;
ReadNode(xdoc, node, Nodes);
}
protected void ReadNode(XmlDocument xdoc, XmlNode node,
TreeNodeCollection nodes)
{
foreach (XmlNode xn in node.ChildNodes)
{
TreeNode tn = new TreeNode();
tn.Text = xn.Attributes["Text"].Value;
tn.Tag = nodeList[xn.Name];
nodes.Add(tn);
ReadNode(xdoc, xn, tn.Nodes);
if (Convert.ToBoolean(xn.Attributes["IsExpanded"].Value))
{
tn.Expand();
}
}
}
A typical XML file (for example, the screenshot at the beginning of the article) looks like this:
="1.0"="utf-8"
<XTree>
<Solution Text="Solution '$Name: $'" IsExpanded="True">
<Project Text="$Name: $" IsExpanded="True">
<Properties Text="Properties" IsExpanded="False" />
<References Text="References" IsExpanded="False" />
<Folder Text="$Folder$" IsExpanded="False" />
</Project>
<Project Text="$Name: $" IsExpanded="True">
<Properties Text="Properties" IsExpanded="False" />
<References Text="References" IsExpanded="False" />
<File Text="$File$" IsExpanded="False" />
<File Text="$File$" IsExpanded="False" />
</Project>
</Solution>
</XTree>
The Demo
I have a simple DemoForm
class that demonstrates its usage (I'm a minimalist when it comes to form classes):
public DemoForm()
{
Text = "General Tree Demo";
gTree = new XTree();
gTree.Dock = DockStyle.Left;
gTree.Width = 200;
Controls.Add(gTree);
Size=new Size(300, 400);
XmlDocument xdoc = new XmlDocument();
xdoc.Load("idetree.xml");
gTree.Initialize(xdoc);
}
A Sample XML Tree Template
The sample XML creates a simple solution tree similar to VS2005. I selected the icons from the common icons rather than using the VS2005 icons (don't want to get sued, you know!). So, given this tree template:
="1.0"="utf-8"
<Node Name="Solution" Text="Solution '$Name: $'" IsRequired="true"
IconFilename="solution.ico">
<Nodes>
<Node Name="Project" Text="$Name: $" IsReadOnly="true"
IconFilename="project.ico">
<ParentPopupItems>
<Popup Text="Add New Project" IsAdd="true"/>
<Popup Text="Add Existing Project" IsAdd="true"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete Project" IsRemove="true"/>
<Popup Text="Remove Project" IsRemove="true"/>
</PopupItems>
<Nodes>
<Node Name="File" Text="$File$" IconFilename="file.ico">
<ParentPopupItems>
<Popup Text="Add New File" IsAdd="true"/>
<Popup Text="Add Existing File" IsAdd="true"/>
<Popup Text="Link To Existing File" IsAdd="true"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Delete File" IsRemove="true"/>
<Popup Text="Remove File" IsRemove="true"/>
<Popup Text="Exclude File" IsRemove="true"/>
</PopupItems>
</Node>
<Node Name="Folder" Text="$Folder$" IconFilename="folder.ico">
<ParentPopupItems>
<Popup Text="Add New Folder" IsAdd="true"/>
<Popup Text="Add Existing Folder" IsAdd="true"/>
</ParentPopupItems>
<PopupItems Separator="true">
<Popup Text="Delete Folder" IsRemove="true"/>
<Popup Text="Remove Folder" IsRemove="true"/>
</PopupItems>
<Nodes>
<Node Name="refFile" RefName="File"/>
<Node Name="refFolder" RefName="Folder"/>
</Nodes>
</Node>
<Node Name="Properties" Text="Properties" IsReadOnly="true"
IsRequired="true" IconFilename="properties.ico"/>
<Node Name="References" Text="References" IsReadOnly="true"
IsRequired="true" IconFilename="references.ico">
<Nodes>
<Node Name="Assembly" Text="$Assembly$" IsReadOnly="true"
IconFilename="reference.ico">
<ParentPopupItems>
<Popup Text="Add Reference" IsAdd="true"/>
</ParentPopupItems>
<PopupItems>
<Popup Text="Remove Reference" IsRemove="true"/>
</PopupItems>
</Node>
</Nodes>
</Node>
</Nodes>
</Node>
</Nodes>
</Node>
If you follow on in the XML, you'll see the following.
The root node is the solution:
and right clicking on it, I get:
If I add a project, the result is:
Notice how the properties and references nodes are automatically created, because the IsRequired
attribute is set to true
.
From here, I can add folders, files, remove files and folders, etc. And of course, I can add multiple projects and so forth:
Conclusion
This class isn't necessarily that useful by itself. But I wanted in this article to introduce the concept of defining a tree hierarchy using an XML template. I hope this gives you some food for thought. I'd like to hear what you think about this concept and what features you think it needs to have to become a useful class. I have my own ideas, but I'd like to hear from the community!
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.