声明式地填充属性网格






4.54/5 (10投票s)
2004年9月23日
6分钟阅读

93711

2674
运行时类生成,用于声明式地填充属性网格。
引言
我一直在研究如何声明式地编写属性网格,主要是因为我不想创建硬编码的类,以便使用属性网格来管理一些可能需要随着项目发展而更改的配置设置。因此,我研究了管理属性网格所涉及的内容。本文演示了以下内容:
- 声明式编程属性网格
- 将容器数据绑定到控件
- 属性更改时触发事件
- 序列化/反序列化容器
本文使用了之前在以下文章中讨论过的代码:
它是如何工作的?
PropertyGrid
提供了一个 SelectedObject
方法,您可以设置一个实例,您希望 PropertyGrid
通过该方法公开公共属性以供编辑。显然,这需要存在一个类以及该类的实例,以便 PropertyGrid
不仅可以确定公共属性,还可以在用户进行更改时更新属性值。以声明式方式处理此问题需要类在运行时被构造、编译和实例化。这个 MxContainer
类正是这样做的——它使用声明式 XML 和 MycroXaml(或 MyXaml)解析器,根据标记中定义的属性来实例化一个类。
PropertyGrid
的一个特性是,您可以用属性装饰属性方法,PropertyGrid
使用这些属性来改变属性在显示时的行为。MxContainer
类提供了 MxProperty
类,其实例被添加到 MxProperties
集合中。MxProperty
类支持以下属性装饰器:
类别
描述
ReadOnly
默认值
DefaultProperty
(类装饰器)
除了处理字符串、DateTime、Font、Color 和 bool 等基本类型外,PropertyGrid
还知道如何显示枚举的成员。MxContainer
类通过向其 MxEnumerations
集合添加 MxEnum
实例来支持枚举。这些会在生成的代码中生成枚举,进而可以作为属性类型应用。
MxContainer
类还为每个 MxProperty
实例实现了一个 OnValueChanged
事件。当此事件被分配给一个处理程序时,它会自动转换为一个特定的 OnXxxChanged
事件,其中“Xxx
”是特定属性的名称。
声明式标记
上面的截图是用以下声明式 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" StartPosition="CenterScreen"
ClientSize="600, 400" Text="Declarative Property Grid Demo">
<mc:MxContainer Name="Container" DefaultProperty="OutputDirectory">
<mc:MxEnumerations>
<mc:MxEnum Name="MyEnum" Values="Executable, Library, Console"/>
</mc:MxEnumerations>
<mc:MxProperties>
<mc:MxProperty Name="Font" Type="Font"
Category="Editor" Description="The text editor font."
InitialValue="new Font("MS Sans Serif", 10)"/>
<mc:MxProperty Name="Color" Type="Color" InitialValue="Color.Green"
Category="Editor" Description="The text editor color."/>
<mc:MxProperty Name="OutputDirectory" Type="string"
InitialValue=""c:\\test""
DefaultValue=""c:\\test""
Category="Project Settings" Description="Output directory."
OnValueChanged="OnOutputDirectoryChanged"/>
<mc:MxProperty Name="Exclude" Type="bool"
Category="Project Settings"
Description="Exclude this project from the build."
InitialValue="true" DefaultValue="true"/>
<mc:MxProperty Name="OutputType" Type="MyEnum"
Category="Project Settings" Description="Project output type."
DefaultValue="MyEnum.Executable"/>
<mc:MxProperty Name="CreatedOn" Type="DateTime"
InitialValue="DateTime.Today" ReadOnly="true"
Category="Project Information" Description="Project start date."/>
<mc:MxProperty Name="LastModifiedOn" Type="DateTime"
InitialValue="DateTime.Today" ReadOnly="true"
Category="Project Information"
Description="Project last changed date."/>
</mc:MxProperties>
</mc:MxContainer>
<wf:Controls>
<wf:PropertyGrid Name="PropertyGrid" Dock="Fill"
SelectedObject="{Container}" HelpBackColor="LightSteelBlue"/>
<wf:TextBox Name="TextBox" Dock="Left" Text="Foobar" Multiline="true"
Width="300" BorderStyle="Fixed3D">
<wf:DataBindings>
<mc:DataBinding PropertyName="ForeColor" DataSource="{Container}"
DataMember="Color"/>
<mc:DataBinding PropertyName="Font" DataSource="{Container}"
DataMember="Font"/>
</wf:DataBindings>
</wf:TextBox>
<wf:Panel Dock="Top" Height="40">
<wf:Controls>
<wf:Button Text="Serialize" Location="10, 10" Size="80, 25"
FlatStyle="System" Click="OnSerialize"/>
<wf:Button Text="Deserialize" Location="100, 10" Size="80, 25"
FlatStyle="System" Click="OnDeserialize"/>
</wf:Controls>
</wf:Panel>
</wf:Controls>
</wf:Form>
</MycroXaml>
上述标记的显著特点是:
- 支持枚举
- 构造运行时容器
- 为构造的类属性定义属性装饰器
- 使用“内联”代码初始化值
- 数据绑定以自动更改 TextBox 属性
实现
工作的核心是通过构造一个运行时类并实例化它来完成的。实现实际上相当普通——迭代几个集合来创建 C# 代码,然后调用编译器类进行编译。我没有使用 Reflection.Emit
,因为我不熟悉它,并且我喜欢直观地检查构造的代码是否存在错误。
MxContainer
MxContainer
根据 XML 定义构造一个类。请注意以下代码:
sb.Append("\tpublic class "+className+" : MxDataContainer\r\n");
请注意此类是如何派生自 MxDataContainer
的。这是为了让应用程序可以通过 Set
/GetValue
方法与容器交互,因为在编译时,应用程序没有关于运行时构造类的类型信息。这些方法和其他方法支持数据从控件到容器以及从容器到控件的自动传输。此类中的一些代码可能比我做得更好,可以利用数据绑定,但此实现确实有效。
运行时编译器
一旦类构造完毕,就会进行编译:
RunTimeCompiler rtc=new RunTimeCompiler();
ArrayList refs=new ArrayList();
refs.Add("System.dll");
refs.Add("System.Data.dll");
refs.Add("System.Drawing.dll");
refs.Add("MycroXaml.MxContainer.dll");
Assembly assembly=rtc.Compile(refs, "C#", sb.ToString(), String.Empty);
Trace.Assert(assembly != null, "Compiler errors");
refObj=(MxDataContainer)
Activator.CreateInstance(assembly.GetModules(false)[0].GetTypes()[0]);
这里的问题是预测程序员可能使用的属性类型。理想情况下,应该将程序集集合添加到容器中,以便可以声明式地定义引用的程序集,而不是硬编码。然而,我发现对于所有实际目的,上述实现都可以正常工作,所以我从未更改过它!
OnValueChanged 事件
在标记中,每个 MxProperty
支持一个 OnValueChanged
事件,可以将其连接到事件处理程序。但是,您不能为构造类中的每个属性使用相同的 OnValueChanged
事件。相反,每个属性都需要自己的 OnXxxChanged
事件处理程序,并且 MxProperty
标记中分配的事件必须重新分配给构造的属性事件处理程序。这是通过以下方式完成的:
EventInfo ei=refObj.GetType().GetEvent("On"+prop.Name+"Changed");
// there can be only one delegate
Delegate srcDlgt=prop.ValueChangedDelegates[0];
Delegate dlgt=Delegate.CreateDelegate(ei.EventHandlerType, srcDlgt.Target,
srcDlgt.Method.Name);
ei.AddEventHandler(refObj, dlgt);
其中,“prop
”是 MxProperty
实例,ValueChangedDelegates
是 MxProperty
的一个属性,返回 OnValueChanged
事件的调用列表。
public Delegate[] ValueChangedDelegates
{
get {return OnValueChanged.GetInvocationList();}
}
我们只对调用列表的第一个实例感兴趣,因为声明式地只能分配一个实例。因此,上面的代码演示了如何将事件处理程序从一个事件复制到另一个事件。
DataBinding
由于 Binding
类不支持默认构造函数,因此必须使用辅助类来完成数据绑定。因此,DataBinding
辅助类如下所示:
public class DataBinding : ISupportInitialize, MycroXaml.Parser.IMycroXaml
{
protected string propertyName;
protected object dataSource;
protected string dataMember;
protected ControlBindingsCollection cbc;
protected Binding binding;
public String PropertyName
{
get{return propertyName;}
set {propertyName=value;}
}
public object DataSource
{
get {return dataSource;}
set {dataSource=value;}
}
public String DataMember
{
get {return dataMember;}
set {dataMember=value;}
}
public void Initialize(object parent)
{
cbc=parent as ControlBindingsCollection;
}
public object ReturnedObject
{
get {return binding;}
}
public void BeginInit()
{
}
public virtual void EndInit()
{
try
{
binding=new Binding(propertyName, dataSource, dataMember);
if (dataSource is MycroXaml.MxContainer.MxDataContainer)
{
((MycroXaml.MxContainer.MxDataContainer)dataSource).Add(cbc.Control, binding);
}
// do manually, as the Add method is declared as a "new" method,
// causing confusion on reflection.
cbc.Add(binding);
}
catch(Exception e)
{
Trace.Fail(e.Message);
}
}
}
请注意此类如何使用 ISupportInitialize
在所有属性值分配后实例化 Binding
类,并且它还手动将生成的 Binding
实例添加到 ControlBindingsCollection
。这不能在解析器中完成,因为 Add
方法是用“new
”声明的,覆盖了基类实现。由于方法定义相同,使用反射获取 Add
方法会导致方法歧义。这是解析器无法轻易处理的问题。
序列化/反序列化
一个配置类,无论是运行时生成还是非运行时生成,除非您可以保存配置并在以后恢复它,否则几乎是无用的。正如我在关于简单序列化的文章中所讨论的那样,运行时构造的类无法利用 XmlSerializer
或 BinaryFormatter
类,因为在编译时不知道构造类的类型信息。因此,序列化是通过我之前编写的简单序列化器/反序列化器完成的。反序列化后,必须手动更新绑定了容器属性的控件(至少,我还没有弄清楚如何自动更新,因为这可能与不支持正确的 OnXxxChanged
结构有关——需要进一步调查)。
propertyGrid.Refresh();
container.BeginEdit(); // rebind controls with the new values
这些行会更新 PropertyGrid
的新值,并更新所有数据绑定控件。(BeginEdit
可能不是这个方法最好的名称,但目的是确保在用户开始编辑这些值之前,所有控件都已更新为最新的容器值。)
将它们整合在一起
那么,实际构造上述 UI 的代码是什么样的?它们是如何组合在一起的?
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using Mtc.SimpleSerializer;
using MycroXaml.Parser;
using MycroXaml.MxContainer;
namespace PropertyGridDemo
{
public class Startup
{
protected MxDataContainer container;
protected PropertyGrid propertyGrid;
[STAThread]
static void Main()
{
new Startup();
}
public Startup()
{
Parser mp=new Parser();
StreamReader sr;
string text;
XmlDocument doc;
sr=new StreamReader("propertyGrid.xml");
text=sr.ReadToEnd();
sr.Close();
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);
container=(MxDataContainer)mp.GetInstance("Container");
propertyGrid=(PropertyGrid)mp.GetInstance("PropertyGrid");
form.ShowDialog();
}
public void OnSerialize(object sender, EventArgs e)
{
Serializer s=new Serializer();
s.Start();
s.Serialize(container);
string text=s.Finish();
StreamWriter sw=new StreamWriter("data.xml");
sw.Write(text);
sw.Close();
}
public void OnDeserialize(object sender, EventArgs e)
{
StreamReader sr=new StreamReader("data.xml");
string text=sr.ReadToEnd();
sr.Close();
Deserializer d=new Deserializer();
d.Start(text);
d.Deserialize(container, 0);
propertyGrid.Refresh();
container.BeginEdit(); // rebind controls with the new values
}
public void OnOutputDirectoryChanged(object sender,
ContainerEventArgs cea)
{
// some event handler activity
}
}
}
就是这样!MycroXaml
解析器负责实例化对象图,MycroXaml.MxContainer
程序集负责运行时构造容器,SimpleSerializer
程序集负责容器的序列化/反序列化。UI 和属性网格容器已迁移到声明式代码,因此命令式代码需要做的就是启动它们并处理事件。
结论
我发现使用声明式方法构造适合使用 PropertyGrid
进行操作的自定义容器是一种简单而灵活的方法。结合轻松设置数据绑定、事件处理和序列化,本文提供的类应通过提供通用且灵活的解决方案来节省时间。我个人认为,将声明式程序元素与命令式元素分开是一种非常强大的机制,可以应对软件工程的现实——设计时间不足、实现过程中功能请求/更改,以及维护和增强的未知领域(!)。