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:
="1.0" ="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)
{
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);
}
if (ret is ISupportInitialize)
{
((ISupportInitialize)ret).BeginInit();
}
if (ret is IMycroXaml)
{
((IMycroXaml)ret).Initialize(parent);
}
ProcessChildProperties(node, ret);
string refName=ProcessAttributes(node, ret, t);
if (ret is ISupportInitialize)
{
((ISupportInitialize)ret).EndInit();
}
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();
foreach(XmlNode child in node.ChildNodes)
{
if (child is XmlElement)
{
string pname=child.LocalName;
PropertyInfo pi=t.GetProperty(pname);
if (pi==null)
{
ProcessNode(child, null);
continue;
}
foreach(XmlNode grandChild in child.ChildNodes)
{
if (grandChild is XmlElement)
{
object propObject=pi.GetValue(parent, null);
object obj=ProcessNode(grandChild, propObject);
if (obj != null)
{
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
{
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;
foreach(XmlAttribute attr in node.Attributes)
{
string pname=attr.Name;
string pvalue=attr.Value;
PropertyInfo pi=t.GetProperty(pname);
EventInfo ei=t.GetEvent(pname);
if (pi != null)
{
if ( pvalue.StartsWith("{") && pvalue.EndsWith("}") )
{
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
{
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);
}
}
}
if (pname=="Name")
{
refName=pvalue;
AddInstance(pvalue, ret);
}
}
else if (ei != null)
{
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
{
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!