Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version
Go to top

MycroXaml

, 23 Sep 2004
A Declarative Xml Parser In Less Than 300 Lines Of Code
using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Xml;

namespace MycroXaml.Parser
{
	public interface IMycroXaml
	{
		void Initialize(object parent);
		object ReturnedObject
		{
			get;
		}
	}

	public class Parser
	{
		protected Hashtable nsMap;
		protected Hashtable objectCollection;
		protected object eventSink;

		public object Load(XmlDocument doc, string objectName, object eventSink)
		{
			this.eventSink=eventSink;
			objectCollection=new Hashtable();

			object ret=null;
			XmlNode node=doc.SelectSingleNode("//MycroXaml[@Name='"+objectName+"']");
			Trace.Assert(node != null, "Couldn't find MycroXaml element "+objectName);
			Trace.Assert(node.ChildNodes.Count==1, "Only one child of the root is allowed.");
			// The last valid node instantiated is returned.
			// The xml root should only have one child.
			ProcessNamespaces(node);
			ret=ProcessNode(node.ChildNodes[0], null);
			return ret;
		}

		public object GetInstance(string name)
		{
			Trace.Assert(objectCollection.Contains(name), "The object collection does not have an entry for "+name);
			return objectCollection[name];
		}

		public void AddInstance(string name, object obj)
		{
			// We don't care if we overwrite an existing object.
			objectCollection[name]=obj;
		}

		protected void ProcessNamespaces(XmlNode node)
		{
			nsMap=new Hashtable();
			foreach(XmlAttribute attr in node.Attributes)
			{
				if (attr.Prefix=="xmlns")
				{
					nsMap[attr.LocalName]=attr.Value;
				}
			}
		}

		protected object ProcessNode(XmlNode node, object parent)
		{
			object ret=null;
			if (node is XmlElement)
			{
				// instantiate the class
				string ns=node.Prefix;
				string cname=node.LocalName;
				Trace.Assert(nsMap.Contains(ns), "Namespace '"+ns+"' has not been declared.");
				string asyName=(string)nsMap[ns];
				string qname=StringHelpers.LeftOf(asyName, ',')+"."+cname+", "+StringHelpers.RightOf(asyName, ',');
				Type t=Type.GetType(qname, false);
				Trace.Assert(t != null, "Type "+qname+" could not be determined.");
				try
				{
					ret=Activator.CreateInstance(t);
				}
				catch(Exception e)
				{
					Trace.Fail("Type "+qname+" could not be instantiated:\r\n"+e.Message);
				}

				// support the ISupportInitialize interface
				if (ret is ISupportInitialize)
				{
					((ISupportInitialize)ret).BeginInit();
				}

				// If the instance implements the IMicroXaml interface, then it may need 
				// access to the parser.
				if (ret is IMycroXaml)
				{
					((IMycroXaml)ret).Initialize(parent);
				}

				// implements the class-property-class model
				ProcessChildProperties(node, ret);
				string refName=ProcessAttributes(node, ret, t);

				// support the ISupportInitialize interface
				if (ret is ISupportInitialize)
				{
					((ISupportInitialize)ret).EndInit();
				}

				// If the instance implements the IMicroXaml interface, then it has the option
				// to return an object that replaces the instance created by the parser.
				if (ret is IMycroXaml)
				{
					ret=((IMycroXaml)ret).ReturnedObject;
					if ( (ret != null) && (refName != String.Empty) )
					{
						AddInstance(refName, ret);
					}
				}
			}
			return ret;
		}

		protected void ProcessChildProperties(XmlNode node, object parent)
		{
			Type t=parent.GetType();

			// children of a class must always be properties
			foreach(XmlNode child in node.ChildNodes)
			{
				if (child is XmlElement)
				{
					string pname=child.LocalName;
					PropertyInfo pi=t.GetProperty(pname);

					if (pi==null)
					{
						// Special case--we're going to assume that the child is a class instance
						// not associated with the parent object
						ProcessNode(child, null);
						continue;
					}

					// a property can only have one child node unless it's a collection
					foreach(XmlNode grandChild in child.ChildNodes)
					{
						if (grandChild is XmlElement)
						{
							object propObject=pi.GetValue(parent, null);
							object obj=ProcessNode(grandChild, propObject);

							// A null return is valid in cases where a class implementing the IMicroXaml interface
							// might want to take care of managing the instance it creates itself.  See DataBinding
							if (obj != null)
							{

								// support for ICollection objects
								if (!pi.CanWrite)
								{
									if (propObject is ICollection)
									{
										MethodInfo mi=t.GetMethod("Add", new Type[] {obj.GetType()});
										if (mi != null)
										{
											try
											{
												mi.Invoke(obj, new object[] {obj});
											}
											catch(Exception e)
											{
												Trace.Fail("Adding to collection failed:\r\n"+e.Message);
											}
										}
										else if (propObject is IList)
										{
											try
											{
												((IList)propObject).Add(obj);
											}
											catch(Exception e)
											{
												Trace.Fail("List/Collection add failed:\r\n"+e.Message);
											}
										}
									}
									else
									{
										Trace.Fail("Unsupported read-only property: "+pname);
									}
								}
								else
								{
									// direct assignment if not a collection
									try
									{
										pi.SetValue(parent, obj, null);
									}
									catch(Exception e)
									{
										Trace.Fail("Property setter for "+pname+" failed:\r\n"+e.Message);
									}
								}
							}
						}
					}
				}
			}
		}

		protected string ProcessAttributes(XmlNode node, object ret, Type t)
		{
			string refName=String.Empty;

			// process attributes
			foreach(XmlAttribute attr in node.Attributes)
			{
				string pname=attr.Name;
				string pvalue=attr.Value;

				// it's either a property or an event
				PropertyInfo pi=t.GetProperty(pname);
				EventInfo ei=t.GetEvent(pname);

				if (pi != null)
				{
					// it's a property!
					if ( pvalue.StartsWith("{") && pvalue.EndsWith("}") )
					{
						// And the value is a reference to an instance!
						// Get the referenced object.  Late binding is not supported!
						object val=GetInstance(pvalue.Substring(1, pvalue.Length-2));
						try
						{
							pi.SetValue(ret, val, null);
						}
						catch(Exception e)
						{
							Trace.Fail("Couldn't set property "+pname+" to an instance of "+pvalue+":\r\n"+e.Message);
						}
					}
					else
					{
						// it's string, so use a type converter.
						TypeConverter tc=TypeDescriptor.GetConverter(pi.PropertyType);
						if (tc.CanConvertFrom(typeof(string)))
						{
							object val=tc.ConvertFrom(pvalue);
							try
							{
								pi.SetValue(ret, val, null);
							}
							catch(Exception e)
							{
								Trace.Fail("Property setter for "+pname+" failed:\r\n"+e.Message);
							}
						}
					}

					// auto-add to our object collection
					if (pname=="Name")
					{
						refName=pvalue;
						AddInstance(pvalue, ret);
					}
				}
				else if (ei != null)
				{
					// it's an event!
					Delegate dlgt=null;
					try
					{
						MethodInfo mi=eventSink.GetType().GetMethod(pvalue, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
						dlgt=Delegate.CreateDelegate(ei.EventHandlerType, eventSink, mi.Name);
					}
					catch(Exception e)
					{
						Trace.Fail("Couldn't create a delegate for the event "+pvalue+":\r\n"+e.Message);
					}

					try
					{
						ei.AddEventHandler(ret, dlgt);
					}
					catch(Exception e)
					{
						Trace.Fail("Binding to event "+pname+" failed: "+e.Message);
					}
				}
				else
				{
					// who knows what it is???
					Trace.Fail("Failed acquiring property information for "+pname);
				}
			}
			return refName;
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 23 Sep 2004
Article Copyright 2004 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid