Using the composite pattern with serialization in order to create a flexible navigation framework
Introduction
We all use navigation structures in our websites, some more useful than others. In a CMS system you will normally add a lot of meta data to the navigation elements, seeing that the whole point of the system is to manipulate these items and their content.
What I want to accomplish is to write a basic framework that will allow for a base navigation element that can be extended for different scenarios. In one site we would possibly only want to have description and heading properties related to the navigation objects. In another site we may want both the description and heading properties, but also an additional property, called URL.
Requirements
In order to simplify what we want to accomplish here, I'll try to draw up a list of requirements:
- We want our objects to be related to each other in a tree like structure - for this we'll use the composite pattern (With a bit of tweaking)
- We want all the nav objects to be able to manipulate this tree, by way of adding children and siblings to them
- We want to be able to publish the whole structure (or part of it) to an XML file for XSL transformation
- We want to be able to extend the base navigation easily to add our own properties to persist to our xml stores
Overview of proposed solution
- I'll use the composite design pattern to represent the tree structure
- I'll use serialization in order to "automate" the representation of the object in XML
- For now, I'm not going to provide the objects with data methods - maybe for a future article
Code
First we'll write our base navigation class. In this class we'll have the composite logic. We'll also insert the common logic to manipulate the tree structure
using System;
using System.Xml.Serialization;
using System.Collections;
namespace serialization
{
///
/// This object will be used as the base class for other navigation elements
/// The basic structure modifier methods will be housed here
///
public class NavObject
{
///
/// The private variables
///
private string description;
private string heading;
private ArrayList childNavs = new ArrayList();
private NavObject parent;
///
/// The properties
///
public string Description
{
get{return description;}
set{description = value;}
}
public string Heading
{
get{return heading;}
set{heading = value;}
}
///
/// This is ignored - because if the serializer will attempt to ///parse the housing element's
/// properties - it will cause an infinite loop
///
[XmlIgnore]
public NavObject Parent
{
set{parent = value;}
}
///
/// Declare the XmlArrayItem attribute
/// Note that all the different possible object types must be listed in here
///
/// The XmlArray attribute - specifies that we want the next item to ///be serialized as an xmlarray
///
[XmlArrayItem(ElementName="NavObject",Type = typeof(object)), XmlArrayItem(ElementName="AdminNavObject", Type = typeof(AdminNavObject)), XmlArrayItem(ElementName = "SiteNavObject", Type= typeof(SiteNavObject))]
[XmlArray(ElementName="Children")]
public ArrayList ChildNavs
{
get{return childNavs;}
}
///
/// Adds a child to the item to the end of the child collection
///
/// The item to add to the collection
public void AddChild(object navElement)
{
((NavObject)navElement).Parent = this;
this.childNavs.Add(navElement);
}
///
/// Removes a child from the collection
///
/// The object to remove from the collection
public void RemoveChild(object navElement)
{
this.childNavs.Remove(navElement);
}
///
/// Inserts a child in the parent's collection at this items index - ///and shift all the items down after it
///
///
public void InsertPrevSibling(object navElement)
{
((NavObject)navElement).Parent = this.parent;
this.parent.ChildNavs.Insert(this.parent.ChildNavs.IndexOf(this), navElement);
}
///
/// Inserts a child in the parent's collection after this item's index
///
///
public void InsertAfterSibling(object navElement)
{
((NavObject)navElement).Parent = this.parent;
int insertIndex = this.parent.ChildNavs.IndexOf(this) + 1;
if(insertIndex > this.parent.ChildNavs.Count)
{
this.parent.AddChild(navElement);
}
else
{
this.parent.ChildNavs.Insert(insertIndex, navElement);
}
}
public NavObject()
{
}
}
}
|
Now we'll derive a class from the base class called AdminNavObject. This class will contain extra properties which will then be persisted to the resulting xml.
using System;
using System.Xml.Serialization;
namespace serialization
{
public class AdminNavObject: NavObject
{
///
/// We just add our extra properties
/// If it's populated at the time of serialization, it will be /// serialized - otherwise it will be omitted
/// We add [XmlIgnore] attributes to the properties which we want to omit ////from the results
///
private string username;
private string password;
///
/// We dont want the password property to be serialized, so we use the /// XmlIgnore attribute to exclude it
///
[XmlIgnore]
public string PassWord{get{return password;} set{password = value;}}
public string UserName{get{return username;} set{username = value;}}
public AdminNavObject()
{
}
}
}
|
And just for fun, we'll create another derivative of the base object, with it's own additional properties which will be persisted to xml.
using System;
using System.Xml.Serialization;
namespace serialization
{
public class SiteNavObject: NavObject
{
///
/// We just add our extra properties
/// If it's populated at the time of serialization, it will be serialized - ///otherwise it will be omitted
/// We add [XmlIgnore] attributes to the properties which we want /// to omit from the results
///
private string url;
private string target;
public string URL{get{return url;} set{url = value;}}
public string Target{get{return target;} set{target = value;}}
public SiteNavObject()
{
}
}
} |
Let's look at an example that will build a tree and persist it to xml. Normally, you would get the data from a data store of some kind, and populate the tree from that.
using System;
using System.Xml.Serialization;
using System.IO;
using System.Collections;
namespace serialization
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
//we create a navobject base class - this can be any navobject //(AdminNavObject or SiteNavObject or NavObject)
NavObject o = new NavObject();
o.Description = "this is item number a - navobject";
o.Heading ="item number a";
//We add a child to the item above
AdminNavObject a1 = new AdminNavObject();
a1.Description = "this is item number b - adminobject";
a1.Heading ="item number b";
o.AddChild(a1);
//We add a child to the item above
AdminNavObject a2 = new AdminNavObject();
a2.Description = "this is item number c - adminobject";
a2.Heading ="item number c";
a2.UserName = "HendrikSwanepoel";
a2.PassWord="MyPassword";
a1.AddChild(a2);
//We add a child to the item above
AdminNavObject a3= new AdminNavObject();
a3.Description = "this is item number d - adminobject";
a3.Heading ="item number d";
a2.AddChild(a3);
//We add a child to the item above
AdminNavObject a4= new AdminNavObject();
a4.Description = "this is item number e - adminobject";
a4.Heading ="item number e";
a2.AddChild(a4);
//We insert an element after item a3
AdminNavObject a5= new AdminNavObject();
a5.Description = "this is item number f - adminobject - will be inserted between element d and e";
a5.Heading ="item number f";
a3.InsertAfterSibling(a5);
//we insert an element before item a4 - which is now the last element
AdminNavObject a6= new AdminNavObject();
a6.Description = "this is item number g - will be inserted before item e";
a6.Heading ="item number g";
a4.InsertPrevSibling(a6);
//can add a SiteNavObject too - we add this to the root node
SiteNavObject s1 = new SiteNavObject();
s1.Description = "A site nav object";
s1.Heading = "The site nav's heading";
s1.Target="_blank";
s1.URL = "http://dotnet.org.za/hendrik";
o.AddChild(s1);
//We instantiate the serializer
XmlSerializer x = new XmlSerializer(typeof(NavObject));
//We instantiate a stream to write the results to
string fileName = Directory.GetCurrentDirectory() + "\\thexml.xml";
FileStream f = new FileStream(fileName, FileMode.Create);
try
{
//let's serialize it
x.Serialize(f, o);
}
finally
{
//Whatever happens we have to close the stream
f.Close();
Console.WriteLine(fileName);
Console.ReadLine();
}
}
}
}
|
The resulting xml file will look like this:
xml version="1.0" ?>
- <NavObject xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Description>this is item number a - navobject< SPAN>Description>
<Heading>item number a< SPAN>Heading>
- <Children>
- <AdminNavObject>
<Description>this is item number b - adminobject< SPAN>Description>
<Heading>item number b< SPAN>Heading>
- <Children>
- <AdminNavObject>
<Description>this is item number c - adminobject< SPAN>Description>
<Heading>item number c< SPAN>Heading>
- <Children>
- <AdminNavObject>
<Description>this is item number d - adminobject< SPAN>Description>
<Heading>item number d< SPAN>Heading>
<Children />
< SPAN>AdminNavObject>
- <AdminNavObject>
<Description>this is item number f - adminobject - will be inserted between element d and e< SPAN>Description>
<Heading>item number f< SPAN>Heading>
<Children />
< SPAN>AdminNavObject>
- <AdminNavObject>
<Description>this is item number g - will be inserted before item e< SPAN>Description>
<Heading>item number g< SPAN>Heading>
<Children />
< SPAN>AdminNavObject>
- <AdminNavObject>
<Description>this is item number e - adminobject< SPAN>Description>
<Heading>item number e< SPAN>Heading>
<Children />
< SPAN>AdminNavObject>
< SPAN>Children>
<UserName>HendrikSwanepoel< SPAN>UserName>
< SPAN>AdminNavObject>
< SPAN>Children>
< SPAN>AdminNavObject>
- <SiteNavObject>
<Description>A site nav object< SPAN>Description>
<Heading>The site nav's heading< SPAN>Heading>
<Children />
<URL>http://dotnet.org.za/hendrik< SPAN>URL>
<Target>_blank< SPAN>Target>
< SPAN>SiteNavObject>
< SPAN>Children>
< SPAN>NavObject> |