Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#
Article

MycroXaml

Rate me:
Please Sign up or sign in to vote.
4.72/5 (25 votes)
23 Sep 2004CPOL5 min read 96.5K   955   50   12
A Declarative Xml Parser In Less Than 300 Lines Of Code

Sample Image - colorChooser.png

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:

  • Implements a true class-property-class architecture
  • Automatically collects named instances
  • Allows referencing of those instances
  • Supports the ISupportInitialize interface
  • Provides a custom IMycroXaml interface for classes that perform custom parsing
  • Supports IList and ICollection interfaces
  • Is implemented in less than 300 lines of code
  • Strict error checking
  • Event binding to a specified event sink
  • DataBinding

What Doesn't It Do?

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

  • Late binding
  • The "ref:" construct to re-use an existing instance instead of instantiating a new one
  • Resources and bitmaps
  • Styles
  • Include
  • Sub-forms
  • Inline and code-behind runtime compilation
  • Specifying target instances for event binding
  • IExtenderProvider management
  • Default namespace
  • Does not install in the GAC
  • Structs
  • Auto-initialization of C# fields

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
<?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:

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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!

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

 
QuestionAdd items to combo box Pin
♥…ЯҠ…♥12-Jun-12 21:06
professional♥…ЯҠ…♥12-Jun-12 21:06 
GeneralDefault namespace Pin
[Marc]2-Feb-07 15:19
[Marc]2-Feb-07 15:19 
To enable a default namespace, i did the following:
private void ProcessNamespaces(XmlNode node)
{
    nsMap = new Dictionary<string, string>();
    foreach (XmlAttribute attr in node.Attributes)
    {
        if (attr.Prefix == "xmlns")
        {
            nsMap[attr.LocalName] = attr.Value;
        }
        // added by me
        else if (attr.Name == "xmlns")
        {
            nsMap[""] = attr.Value;
        }
    }
}

By the way, is this whole My(cro)Xaml thing being used/developed in any way nowadays? I like the Mycro thingy (just one class, can be used in close source app) in conjunction with VG.net (needed some tweaking though). Perhaps a bit overshadowed by the whole .Net 3 buzz, but this is still some nice work!


"..Commit yourself to quality from day one..it's better to do nothing at all than to do something badly.."
-- Mark McCormick

|| Fold With Us! || Pensieve || VG.Net ||

QuestionLicense Pin
Daniel Grunwald10-Sep-05 2:36
Daniel Grunwald10-Sep-05 2:36 
AnswerRe: License Pin
Marc Clifton10-Sep-05 2:38
mvaMarc Clifton10-Sep-05 2:38 
GeneralProblem with Type.GetType() Pin
Seredenlo Dmitry20-May-05 3:16
sussSeredenlo Dmitry20-May-05 3:16 
GeneralRe: Problem with Type.GetType() Pin
thebeekeeper17-Mar-06 3:46
thebeekeeper17-Mar-06 3:46 
GeneralCulture Info problem Pin
Väinölä Harri1-Oct-04 0:40
Väinölä Harri1-Oct-04 0:40 
GeneralRe: Culture Info problem Pin
Daniel Grunwald19-Oct-04 0:37
Daniel Grunwald19-Oct-04 0:37 
GeneralRe: Culture Info problem Pin
Marc Clifton19-Oct-04 0:52
mvaMarc Clifton19-Oct-04 0:52 
GeneralRe: Culture Info problem Pin
Väinölä Harri19-Oct-04 1:39
Väinölä Harri19-Oct-04 1:39 
GeneralXMLDocument Pin
SHG28-Sep-04 21:13
SHG28-Sep-04 21:13 
GeneralIs vs As Pin
bobddddd24-Sep-04 3:51
bobddddd24-Sep-04 3:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.