65.9K
CodeProject 正在变化。 阅读更多。
Home

MycroXaml

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (21投票s)

2004年9月23日

CPOL

5分钟阅读

viewsIcon

100306

downloadIcon

957

不到 300 行代码的声明式 XML 解析器

Sample Image - colorChooser.png

引言

那么,我为什么要(又一次?)写这个?主要是因为我想要一个轻量级的声明式解析器。MyXaml 并不是我认为的轻量级,所以我面临一个困境:如何在不要求读者下载和安装整个 MyXaml 包的情况下,编写关于声明式编程的 Applet 和文章?我需要一些简单、不影响 Applet/文章本身焦点的东西,因此 "MycroXaml" 应运而生。

MycroXaml 做什么?

MycroXaml 解析 XmlDocument,在运行时实例化类并为属性赋值。MycroXaml 具有以下功能:

  • 实现真正的类-属性-类架构
  • 自动收集命名实例
  • 允许引用这些实例
  • 支持 ISupportInitialize 接口
  • 为执行自定义解析的类提供自定义 IMycroXaml 接口
  • 支持 IListICollection 接口
  • 代码行数不到 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;
    }
  }
}

处理对象图

对象图的处理假定为类-属性-类层次结构。夹在类之间的属性通常是集合,但也可以是接口或抽象类的具体实例。主循环检查创建的实例,看它是否实现了 ISupportInitializeIMycroXaml 接口。前者 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。好的,我现在要退出声明式编程的讲台了!

© . All rights reserved.