MycroXaml






4.72/5 (21投票s)
不到 300 行代码的声明式 XML 解析器
引言
那么,我为什么要(又一次?)写这个?主要是因为我想要一个轻量级的声明式解析器。MyXaml 并不是我认为的轻量级,所以我面临一个困境:如何在不要求读者下载和安装整个 MyXaml 包的情况下,编写关于声明式编程的 Applet 和文章?我需要一些简单、不影响 Applet/文章本身焦点的东西,因此 "MycroXaml" 应运而生。
MycroXaml 做什么?
MycroXaml 解析 XmlDocument
,在运行时实例化类并为属性赋值。MycroXaml 具有以下功能:
- 实现真正的类-属性-类架构
- 自动收集命名实例
- 允许引用这些实例
- 支持
ISupportInitialize
接口 - 为执行自定义解析的类提供自定义
IMycroXaml
接口 - 支持
IList
和ICollection
接口 - 代码行数不到 300 行
- 严格的错误检查
- 事件绑定到指定的事件接收器
- DataBinding
它不做什么?
以下功能不支持(但 MyXaml 支持)
- 后期绑定
- "ref:" 结构,用于重用现有实例而不是实例化新实例
- 资源和位图
- 样式
- Include
- 子窗体
- 内联和代码隐藏运行时编译
- 为事件绑定指定目标实例
- IExtenderProvider 管理
- 默认命名空间
- 不安装在 GAC 中
- 结构体
- C# 字段的自动初始化
尽管如此,MycroXaml 是一个非常有用的“游乐场”,可以用来探索声明式编程和编写轻量级 Applet。在本文中,我将描述这个微解析器是如何工作的。
演示程序
通过颜色选择器(文章顶部的截图)展示了声明式编程与数据绑定和事件的简单演示,该选择器是通过以下声明式 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>
类通过以下 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);
}
}
}
解析器是如何工作的?
初始化
解析器首先要做的是定位根元素并识别映射到程序集命名空间的 XML 命名空间。完成此操作后,它就可以开始处理根节点的子元素了。假设只有一个子元素,对象图将由此构建。
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;
}
如果一切顺利,顶层实例将返回给调用者。命名空间处理非常简单。稍后在类实例化过程中,命名空间信息将用于生成类的完全限定名称。
protected void ProcessNamespaces(XmlNode node)
{
nsMap=new Hashtable();
foreach(XmlAttribute attr in node.Attributes)
{
if (attr.Prefix=="xmlns")
{
nsMap[attr.LocalName]=attr.Value;
}
}
}
处理对象图
对象图的处理假定为类-属性-类层次结构。夹在类之间的属性通常是集合,但也可以是接口或抽象类的具体实例。主循环检查创建的实例,看它是否实现了 ISupportInitialize
和 IMycroXaml
接口。前者 ISupportInitialize
对于确保 .NET Form 对象在停靠方面正确构造至关重要。
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;
}
处理子节点
类的子节点被假定为该类的属性。允许实例化不关联的类,从而允许您在父对象之外构建稍后引用的独立对象。典型的子节点是管理集合的属性,或者是值类型为接口或抽象类的属性。
集合属性
集合属性包含零个或多个子项,解析器会将这些子项添加到集合中。
接口/抽象属性类型
值类型为接口或抽象类的属性恰好有一个子项。这个子项是一个具体实例,将被分配给父实例的属性。
实现
注意如何测试 "CanWrite" PropertyInfo
值以确定属性是集合还是列表。根据 .NET 指南,值类型为集合/列表的属性应该是只读的。这是有道理的,因为它可以防止集合/列表被覆盖。另一点需要提及的是,解析器假定集合实现了一个 Add
方法,该方法只接受一个参数——正在添加的项。一些第三方工具(例如 DevExpress)实现的 Add 方法接受两个或更多参数。这使得处理此类实现变得非常困难。
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);
}
}
}
}
}
}
}
}
处理属性
处理元素的属性就是将属性值映射到属性或事件。需要类型转换才能将字符串转换为相应的属性类型。解析器实现了一个特殊检查,用于处理用 {} 包围的属性值,这会告诉解析器将字符串值替换为先前定义的实例。任何具有 "Name" 属性的类都会自动添加到实例集合中(但该类在调试模式下必须提供 Name 属性)。代码中最有趣的部分是如何连接事件。
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;
}
结论
就是这样!用不到 300 行代码通过 XML 实现声明式编程!现在我可以为其他漂亮的东西编写简单的 UI,而无需读者下载整个 MyXaml 包。是的,在多年使用 XML 声明式地构造对象图(以及声明式编程)之后,以“微软方式”构造应用程序的想法让我不寒而栗。声明式编程非常灵活,易于自定义,执行速度足够快,而且我发现它和使用设计器并编写 C# 代码一样快(甚至在某些情况下更快)。特别是从整体上看——不可避免的设计变更和新需求可能导致 C# 代码的大量重写和重新编译——在我看来,声明式编程确实 shines。如果您担心标记没有编译时检查,您始终可以使用 MxLint。好的,我现在要退出声明式编程的讲台了!