Rich Internet for Everyone (RICHIE) Network: United XAML - XUL Alliance - XUL News - XUL Forum - The Richmond Post - RichCon 2005

MycroXaml

By Marc Clifton (September 2004)

A Declarative Xml Parser In Less Than 300 Lines Of Code

Sample Image

Introduction

So why did I write this (again???)?  Mainly because I wanted a lightweight declarative parser.  MyXaml isn't what I would consider lightweight, so I was faced with a dilemma: how do I write applets and articles about declarative programming without requiring that reader to download and install the whole MyXaml package?  I needed something that was simple and didn't detract from the focus of the applet/article itself, hence "MycroXaml" was born.

What Does MycroXaml Do?

MycroXaml parses an XmlDocument, instantiating classes and assigning values to properties at runtime.  MycroXaml has the following features:

What Doesn't It Do?

The following features are not supported (but are in MyXaml):

That said, MycroXaml is a very useful "playground" for exploring declarative programming and writing lightweight applets.  In this article I'm going to describe how the micro-parser works. 

Demonstration Program

A simple demonstration of declarative programming with data binding and events is illustrated with the color picker (screen shot at top of article), created with the following declarative xml:

<?xml version="1.0" encoding="utf-8"?>
<MycroXaml Name="Form"
  xmlns:wf="System.Windows.Forms, System.Windows.Forms,
        Version=1.0.5000.0, Culture=neutral,
        PublicKeyToken=b77a5c561934e089"
  xmlns:mc="MycroXaml.MxContainer, MycroXaml.MxContainer">
  <wf:Form Name="AppMainForm"
    Text="Color Chooser"
    ClientSize="400, 190"
    BackColor="White"
    FormBorderStyle="FixedSingle"
    StartPosition="CenterScreen">

    <wf:Controls>
      <wf:TrackBar Name="RedScroll" Orientation="Vertical"
          TickFrequency="16" TickStyle="BottomRight" Minimum="0"
          Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128"
          Location="10, 30"/>
      <wf:TrackBar Name="GreenScroll" Orientation="Vertical"
          TickFrequency="16" TickStyle="BottomRight" Minimum="0"
          Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128"
          Location="55, 30"/>
      <wf:TrackBar Name="BlueScroll" Orientation="Vertical"
          TickFrequency="16" TickStyle="BottomRight" Minimum="0"
          Maximum="255" Value="128" Scroll="OnScrolled" Size="42, 128"
          Location="100, 30"/>

      <wf:Label Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="10, 10" ForeColor="Red" Text="Red"/>
      <wf:Label Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="55, 10" ForeColor="Green" Text="Green"/>
      <wf:Label Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="100, 10" ForeColor="Blue" Text="Blue"/>

      <wf:Label Name="RedValue" Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="10, 160" ForeColor="Red">
        <wf:DataBindings>
          <mc:DataBinding PropertyName="Text" DataSource="{RedScroll}"
              DataMember="Value"/>
        </wf:DataBindings>
      </wf:Label>

      <wf:Label Name="GreenValue" Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="55, 160" ForeColor="Green">
        <wf:DataBindings>
          <mc:DataBinding PropertyName="Text" DataSource="{GreenScroll}"
              DataMember="Value"/>
        </wf:DataBindings>
      </wf:Label>

      <wf:Label Name="BlueValue" Size="40,15" TextAlign="TopCenter"
          Font="Microsoft Sans Serif, 8.25pt, style= Bold"
          Location="100, 160" ForeColor="Blue">
        <wf:DataBindings>
          <mc:DataBinding PropertyName="Text" DataSource="{BlueScroll}"
              DataMember="Value"/>
        </wf:DataBindings>
      </wf:Label>

      <wf:PictureBox Name="ColorPanel" Location="90, 0" Size="200, 100"
          Dock="Right" BorderStyle="Fixed3D" BackColor="128, 128, 128"/>
    </wf:Controls>
  </wf:Form>
</MycroXaml>

The classes are instantiated and the event handler defined with the following C# code:

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using System.Xml;

using MycroXaml.Parser;

namespace Demo
{
  public class Startup
  {
    protected Parser mp;

    [STAThread]
    static void Main() 
    {
      new Startup();
    }

    public Startup()
    {
      mp=new Parser();
      StreamReader sr=new StreamReader("ColorPicker.mycroxaml");
      string text=sr.ReadToEnd();
      sr.Close();
      XmlDocument doc=new XmlDocument();
      try
      {
        doc.LoadXml(text);
      }
      catch(Exception e)
      {
        Trace.Fail("Malformed xml:\r\n"+e.Message);
      }

      Form form=(Form)mp.Load(doc, "Form", this);
      form.ShowDialog();
    }

    public void OnScrolled(object sender, EventArgs e)
    {
      TrackBar RedScroll = (TrackBar)mp.GetInstance("RedScroll");
      TrackBar GreenScroll = (TrackBar)mp.GetInstance("GreenScroll");
      TrackBar BlueScroll = (TrackBar)mp.GetInstance("BlueScroll");
      PictureBox ColorPanel = (PictureBox)mp.GetInstance("ColorPanel");

      ColorPanel.BackColor = System.Drawing.Color.FromArgb(
         (byte)RedScroll.Value,
         (byte)GreenScroll.Value,
         (byte)BlueScroll.Value);
    }
  }
}

How Does The Parser Work?

Initialization

The first thing the parser has to do is locate the root element and identify the xml namespaces that map to assembly namespaces.  Once this is done, it can begin processing the child element of the root node.  It is assumed that there will only be a single child element from which the object graph is constructed.

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.")
  ProcessNamespaces(node);
  ret=ProcessNode(node.ChildNodes[0], null);
  return ret;
}

If all goes well, the top-level instance is returned to the caller.  The namespace processing is very simple.  Later during class instantiation the namespace information is used to generate a fully qualified name for the class.

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

Processing The Object Graph

The object graph is processed assuming a class-property-class hierarchy.  The property sandwiched between the classes is usually a collection, but can also be a concrete instance of a property whose type is either an interface or an abstract class.  The main loop inspects the created instance to see if it implements the ISupportInitialize and IMycroXaml interfaces.  The first, ISupportInitialize, is critical to ensure that a .NET Form object is properly constructed with regards to docking.

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;
}

Processing Child Nodes

A child node of a class is assumed to be a property of that class.  A special exception is made allowing disassociated classes to be instantiated, allowing you to construct independent objects off of the parent object that are referenced later on.  The typical child node is either a property managing a collection or a property whose value type is an interface or an abstract class.

Collection Properties

A collection property has zero or more child items that the parser adds to the collection.

Interface/Abstract Property Types

A property whose value type is an interface or abstract class has exactly one child item.  This child item is a concrete instance that is assigned to the property of the parent instance.

Implementation

Note how the "CanWrite" PropertyInfo value is tested to determine whether the property is a collection or a list.  According to the .NET guidelines, properties whose value type are a collection/list should be read only.  This makes sense, as it prevents the overwriting of the collection/list.  Another point to bring up is that the parser assumes that the collection implements an Add method that takes only one parameter--the item being added.  Some third party tools (DevExpress comes to mind) implement Add methods in that take two or more parameters.  This makes it very difficult to work with such implementation.

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);
              }
            }
          }
        }
      }
    }  
  }
}

Processing Attributes

Processing the attributes of an element is all about mapping the attribute value to either a property or an event.  Type conversion is necessary to convert the string to the appropriate property type.  The parser implements a special check for attribute values surrounded by {}, which tells the parser to replace the string value with an instance previously defined.  Any class with a "Name" attribute is automatically added to the instance collection (but the class must provide a Name property in the debug mode).  The most interesting aspect of this code is how events are wired up.

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;
}

Conclusion

That's it!  Declarative programming with xml in less than 300 lines of code!  Now I can write simple UI's for other nifty things without requiring the reader to download the entire MyXaml package.  And yes, after using xml to declaratively construct object graphs (and declarative programming in general for years), the thought of constructing an application the "Microsoft Way" gives me shivers.  Declarative programming is very flexible, easily customizable, has sufficiently fast execution time, and I find it to be as fast (if not faster in some cases) than using a designer and writing C# code.  Especially when considering programming holistically--the inevitable design changes and new requirements that can cause considerable rewrites and recompilations of C# code--declarative programming in my opinion really shines.  And if you're concerned about not having compile-time checking of the markup, you can always use MxLint.  OK, I'll get off the declarative programming soapbox now!


Hosted by SourceForge Logo Please send comments on our web pages to our public xaml-talk mailinglist or to a member of our web team. Copyright © 2004, 2005 The United XAML Team