Click here to Skip to main content
15,886,518 members
Articles / Programming Languages / C#

MycroXaml

Rate me:
Please Sign up or sign in to vote.
4.72/5 (25 votes)
23 Sep 2004CPOL5 min read 96.9K   955   50  
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)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions