元数据驱动的用户界面
查看原文
John deVadoss
微软公司
2005.7
用于:
企业体系结构
用户界面
概述:本文解释了如何由元数据动态创建Windows窗体,以及当考虑到客户端逻辑定制时如何执行窗体。
目录
介绍
阅读前提
设计要素
设计时实现
运行时实现
结论
介绍
因为客户之间、用户之间需求的变化,很多业务应用程序需要用户界面可护展,用户界面的客户端业务逻辑也需要根据个别用户的需要定制。一个用户与另一个用的屏幕布局可能不同,这可能包括控件位置,可见性等等,或者可能因为设备不同而界面不同,就象智能电话,平板设备或个人数字助理。客户端业务逻辑定制也包括:定制有效性规则,更改控件属性,以及其他修正。例如:为了删除及移动文件,经理比下属拥有不同的特权。
有很多技术能够实现业务应用程序的可护展、可定制,大多数应用程序通过将可定制项,如用户界面布局及客户端业务逻辑作为元数据存储在仓储中解决。元数据因而可由运行时引擎解释,从而为用户显示屏幕,以及当用户在屏幕完成动作时执行客户端业务逻辑。
这种方式的优点是双重的:第一,当表示层相关组件在中心仓储完成定制时,不需要重新部署;第二,仅需要非常轻的客户端安装,也就是仅需将运行时引擎部署到客户端机器上。
本文描述了利用微软.NET Framework实现的此技术,为此,本文解释了如何由元数据创建Windows窗体,以及考虑到客户端逻辑定制时如何运行窗体。
阅读前提
l
了解关于编写Visual
Studio Add-Ins的知识
l
DTE对象模型的用途
设计要素
实现元数据驱动的用户界面,本质上有三个设计要素。
l
首先是设计元数据架构并决定仓储机制。仓储可以是关系数据库,如SQL Server或任何其他存储,如XML。本文我们使用XML文件作为元数据仓储。
l
当我们确定元数据架构后,我们实现一个设计时环境,在此环境中用户(通常为开发者或定制人)能创建及修改特定屏幕。本文将跨在微软Visual Studio Windows Forms Designer(窗体设计器)之上来实现设计时环境。
l
最后,我们设计并实现运行时引擎。
仓储结构
首先,我们要决定能以元数据存储的内容。如上述所提,元数据应该包含所有可定制的项目。例如,如果客户想定制屏幕布局及客户端验证逻辑,那么元数据应该包含有关界面布局信息,包括放置在屏幕的控件,他们的属性,放置位置,元数据也应该包括客户端业务逻辑。
以下是一个包括界面信息的XML文件例子。
<?xml
version="1.0" encoding="utf-8"?>
<form
xmlns:xsd=http://www.w3.org/2001/XMLSchema
xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance"
name="Form1" xmlns="http://tempuri.org/
MetadataForm.xsd">
<controls>
<control type="System.Windows.Forms.Form">
<position>
<height>224</height>
<width>300</width>
<top>13</top>
<left>13</left>
</position>
<controls>
<control type="System.Windows.Forms.Button">
<position>
<height>23</height>
<width>75</width>
<top>144</top>
<left>160</left>
</position>
<controls />
<name>button2</name>
<text>button2</text>
<tabIndex>2</tabIndex>
<event-handlers />
</control>
<control type="System.Windows.Forms.Button">
<position>
<height>23</height>
<width>75</width>
<top>144</top>
<left>64</left>
</position>
<controls />
<name>button1</name>
<text>button1</text>
<tabIndex>1</tabIndex>
<event-handlers>
<eventInfo eventName=
"Click"
eventHandler="button1_Click" />
<eventInfo eventName=
"EnabledChanged"
eventHandler="button1_
EnabledChanged"
/>
</event-handlers>
</control>
<control type="System.Windows.Forms.TabControl">
<position>
<height>100</height>
<width>200</width>
<top>24</top>
<left>48</left>
</position>
<controls>
<control type="System.Windows.Forms.TabPage">
<position>
<height>74</height>
<width>192</width>
<top>22</top>
<left>4</left>
</position>
<controls>
<control type="System.Windows.Forms.Button">
<position>
<height>23</height>
<width>56</width>
<top>24</top>
<left>112</left>
</position>
<controls />
<name>button3</name>
<text>button3</text>
<tabIndex>2</tabIndex>
<event-handlers>
<eventInfo eventName=
"Click"
eventHandler="button3_Click" />
</event-handlers>
</control>
<control type="System.Windows.Forms.TextBox">
<position>
<height>20</height>
<width>64</width>
<top>40</top>
<left>16</left>
</position>
<controls />
<name>textBox2</name>
<text>textBox2</text>
<tabIndex>1</tabIndex>
<event-handlers />
</control>
<control type="System.Windows.Forms.TextBox">
<position>
<height>20</height>
<width>64</width>
<top>8</top>
<left>16</left>
</position>
<controls />
<name>textBox1</name>
<text>textBox1</text>
<tabIndex>0</tabIndex>
<event-handlers />
</control>
</controls>
<name>tabPage1</name>
<text>tabPage1</text>
<tabIndex>0</tabIndex>
<event-handlers />
</control>
<control type="System.Windows.Forms.TabPage">
<position>
<height>74</height>
<width>192</width>
<top>22</top>
<left>4</left>
</position>
<controls>
<control type="System.Windows.Forms.LinkLabel">
<position>
<height>23</height>
<width>120</width>
<top>24</top>
<left>24</left>
</position>
<controls />
<name>linkLabel1</name>
<text>linkLabel1</text>
<visible>false</visible>
<tabIndex>0</tabIndex>
<event-handlers>
<eventInfo
eventName="LinkClicked"
eventHandler=
"linkLabel1_LinkClicked"
/>
</event-handlers>
</control>
</controls>
<name>tabPage2</name>
<text>tabPage2</text>
<visible>false</visible>
<tabIndex>1</tabIndex>
<event-handlers />
</control>
</controls>
<name>tabControl1</name>
<text />
<tabIndex>0</tabIndex>
<event-handlers />
</control>
</controls>
<name>Form1</name>
<text>Form1</text>
<tabIndex>0</tabIndex>
<event-handlers>
<eventInfo eventName="Load" eventHandler="Form1_Load"
/>
</event-handlers>
</control>
</controls>
<script>
<events eventHandler="linkLabel1_LinkClicked">
<content> private void
linkLabel1_LinkClicked
(object sender,
System.Windows.Forms.
LinkLabelLinkClickedEventArgs
e)
{
}</content>
<type>text</type>
</events>
<events eventHandler="Form1_Load">
<content> private void Form1_Load(object
sender, System.EventArgs e)
{
textBox1.Text = " " ;
textBox2.Text = " " ;
}</content>
<type>text</type>
</events>
<events eventHandler="button1_Click">
<content> private void button1_Click(object
sender, System.EventArgs e)
{
textBox1.Text = "Button 1 Clicked" ;
}</content>
<type>text</type>
</events>
<events eventHandler="button1_EnabledChanged">
<content> private void button1_Enabled
Changed(object
sender, System.EventArgs e)
{
}</content>
<type>text</type>
</events>
<events eventHandler="button3_Click">
<content> private void button3_Click(object
sender, System.EventArgs e)
{
Button3.Enabled = true ;
textBox1.Text = "Button 3 Clicked" ;
}</content>
<type>text</type>
</events>
</script>
</form>
让我们看看XML文件的内容。Form根标记意味着元数据包含同屏幕或对话框有关的信息,在form元素下面是controls及script元素。
Controls元素包含有关内嵌到窗体的控件的信息,它是包括零个或多个control元素的容器,control元素代表含有它自己属性的控件,如控件名称,控件放置的相对位置,标题文本以及tab顺序。Control元素也有一个屏幕元素事件及事件处理程序的集合。如果控件包括子控件,其信息保存在controls素中。Control元素有一个类型属性,它的值是控件的完全限定CLR(公共语言运行时)类型名称,运行时引擎在运行时建立类型时必需此信息。
Script元素是客户端业务逻辑的容器节点,持有事件处理器程序和它的代码定义,当窗体被再现时,代码将被集成到窗体的代码页里面。
在此例子中,我们将利用XML序列化来转换仓储XML到具体的CLR类型。CLR类型利用xsd.exe工具生成,以下给出仓储的XML架构。
<?xml
version="1.0" encoding="utf-8"
?>
<xs:schema
id="MetadataForm"
targetNamespace=http://tempuri.org/MetadataForm.xsd
elementFormDefault="qualified"
xmlns= http://tempuri.org/
MetadataForm.xsd
xmlns:mstns=
http://tempuri.org/MetadataForm.xsd
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!--
Definition for MetadataForm
-->
<xs:complexType
name="MetadataForm">
<xs:all>
<xs:element name="controls" type=
"MetadataControls"
minOccurs="1"
maxOccurs="1"
/>
<xs:element name="script" type=
"MetadataScript"
minOccurs="1"
maxOccurs="1"
/>
</xs:all>
<xs:attribute name="name" type="xs:string"
use="required" />
</xs:complexType>
<!-- Definition for MetadataControls
-->
<xs:complexType name="MetadataControls">
<xs:sequence>
<xs:element name="control" type=
"MetadataControl"
minOccurs="0"
maxOccurs="unbounded"></xs:element>
</xs:sequence>
</xs:complexType>
<!-- Definition for MetadataControl -->
<xs:complexType name=
"MetadataControl">
<xs:all>
<xs:element name="position" type="Position"
/>
<xs:element name="controls" type="MetadataControls"
/>
<xs:element name="name" type="xs:string"
/>
<xs:element name="text" type="xs:string"
/>
<xs:element name="visible" type="xs:boolean"
default="true" />
<xs:element name="tabIndex" type="xs:int"
/>
<xs:element name="event-handlers" type="AllEvents"
/>
</xs:all>
<xs:attribute name="type" type="xs:string"
/>
</xs:complexType>
<!--
Definition for Events -->
<xs:complexType name=
"AllEvents">
<xs:sequence>
<xs:element name="eventInfo" type=
"EventInfo"
minOccurs ="0" maxOccurs =
"unbounded"
></xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType
name="EventInfo">
<xs:attribute name="eventName"
type="xs:string"></xs:attribute>
<xs:attribute name="eventHandler"
type="xs:string"></xs:attribute>
</xs:complexType>
<!--
Definition for Position
-->
<xs:complexType
name="Position">
<xs:sequence>
<xs:element name="height"
type="xs:int"></xs:element>
<xs:element name="width"
type="xs:int"></xs:element>
<xs:element name="top"
type="xs:int"></xs:element>
<xs:element name="left"
type="xs:int"></xs:element>
</xs:sequence>
</xs:complexType>
<!-- Definition for MetadataScript -->
<xs:complexType name=
"MetadataScript">
<xs:sequence>
<xs:element name="events" type=
"eventItem"
minOccurs="0" maxOccurs=
"unbounded"
/>
</xs:sequence>
</xs:complexType>
<!--
Definition for contentType
-->
<xs:simpleType
name="contentType">
<xs:restriction
base="xs:string">
<xs:enumeration value="text"
/>
<xs:enumeration value="binary"
/>
</xs:restriction>
</xs:simpleType>
<!--
Definition for eventItem -->
<xs:complexType name=
"eventItem">
<xs:all>
<xs:element name="content" type="xs:string"
/>
<xs:element name="type" type="contentType"
/>
</xs:all>
<xs:attribute name="eventHandler" type="xs:string"
/>
</xs:complexType>
<xs:element name="form" type="MetadataForm"
/>
</xs:schema>
当我们在以上.xsd文件上运行命令XSD.exe,将创建称为MetadataForm的CLR类型,此类型的源文件位置为:$SOLUTIONDIR\MetadataLibrary\MetadataForm.xsd,目标文件位置为:$SOLUTIONDIR\MetadataLibrary\MetadataForm.cs。
这是MetadataForm类看起来的样子:
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]
[System.Xml.Serialization.XmlRootAttribute
("form",
Namespace="http://tempuri.org/MetadataForm.xsd",
IsNullable=false)]
public class
MetadataForm
{
///
<remarks/>
[System.Xml.Serialization.XmlArrayItemAttribute
("control",
IsNullable=false)]
public MetadataControl[]
controls;
///
<remarks/>
[System.Xml.Serialization.XmlArrayItemAttribute
("events",
IsNullable=false)]
public eventItem[]
script;
///
<remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string
name;
public MetadataForm
()
{
controls =
null;
script =
null;
name =
null;
}
public MetadataForm(ArrayList ctrlList,
ArrayList
evtList, string formName)
{
name =
formName;
controls = new
MetadataControl[ctrlList.Count];
script = new
eventItem[evtList.Count];
int cnt =
0;
if(ctrlList.Count >
0)
{
foreach(object ctrl in
ctrlList)
{
controls[cnt] =
(MetadataControl)ctrl;
cnt++;
}
}
if(evtList.Count >
0)
{
cnt =
0;
foreach(object evt in
evtList)
{
script[cnt] =
(eventItem)evt;
cnt++;
}
}
}
}
MetadataForm类包含一个MetadataControl数组对象,以及一个eventItem数组对象。
以下是eventItem类的定义:
///
<remarks/>
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]
public class
eventItem
{
///
<remarks/>
public string
content;
///
<remarks/>
public contentType
type;
///
<remarks/>
[System.Xml.Serialization.Xml
AttributeAttribute()]
public string
eventHandler;
public
eventItem()
{
eventHandler
= content =
null;
type = contentType.text
;
}
public
eventItem(string eventHandler, string content,
contentType
cType )
{
this.eventHandler =
eventHandler;
this.content =
content;
this.type =
cType;
}
}
每个eventItem对象包含存储代码的事件处理程序名称,代码本身,以文本或二进制的形式(由contentType枚举定义)保持在content元素中,当前代码仅支持以文本形式存储内容,然而,此方案可以扩展来支持以二进制形式存储代码。
///
<remarks/>
public enum contentType {
///
<remarks/>
text,
/// <remarks/>
binary,
}
MetadataControl类型定义为:
///
<remarks/>
[System.Xml.Serialization.XmlTypeAttribute
(Namespace=http://tempuri.org/MetadataForm.xsd
]
public class
MetadataControl
{
///
<remarks/>
public Position
position;
///
<remarks/>
[System.Xml.Serialization.Xml
ArrayItemAttribute("control",
IsNullable=false)]
public MetadataControl[]
controls;
///
<remarks/>
public string
name;
///
<remarks/>
public string
text;
///
<remarks/>
[System.ComponentModel.DefaultValueAttribute(true)]
public bool visible =
true;
///
<remarks/>
public int tabIndex;
///
<remarks/>
[System.Xml.Serialization.XmlArrayItemAttribute
("eventInfo",
IsNullable=false)]
public EventInfo[]
event-handlers;
///
<remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string
type;
public
MetadataControl()
{
position = new Position
();
tabIndex =
0;
type = name = text =
null;
visible =
true;
event-handlers =
null;
controls = null; public MetadataControl(string
name,
string
text, bool visible, int tabIndex,
string type, int ht, int wd, int top,
int left
,ArrayList evts
)
{
this.name = name
;
this.text = text
;
this.visible =
visible;
this.tabIndex =
tabIndex;
this.type =
type;
position = new Position
(ht,wd,top,left);
event-handlers = new
EventInfo[evts.Count];
int cnt =
0;
if(evts.Count >
0)
{
foreach(object evt in
evts)
{
event-handlers[cnt] =
(EventInfo)evt;
cnt++;
}
}
}
}
最后,Position类型定义为:
///
<remarks/>
[System.Xml.Serialization.XmlTypeAttribute
(Namespace=http://tempuri.org/MetadataForm.xsd
)]
public class
Position
{
///
<remarks/>
public int
height;
/// <remarks/>
public int
width;
///
<remarks/>
public int
top;
///
<remarks/>
public int left;
public Position()
{
height = width = top = left =
0;
}
public Position( int ht, int wd, int top, int
left)
{
height =
ht;
width =
wd;
this.top =
top;
this.left =
left;
}
}
MetadataControl类拥有设计窗体内的所有控件的控件信息。窗体的属性包容在根元素,并且其他的所有控件是它controls数组的一部分。每个控件的名字,文本,tab顺序,可见性,位置及类型被收集,同样,如果一个控件是一些其他控件的父控件,子控件的信息被保持在controls数组。每个控件,它们事件的触发及事件处理程序被保持在event-handlers数组,这是EventInfo数组对象。
EventInfo类型定义为:
///
<remarks/>
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://tempuri.org/MetadataForm.xsd")]
public class
EventInfo
{
///
<remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string
eventName;
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string
eventHandler;
public
EventInfo()
{
eventName =
null;
eventHandler = null
;
}
public EventInfo(string eventName, string
eventHandler)
{
this.eventName =
eventName;
this.eventHandler =
eventHandler;
}
}
设计时实现
我们把设计时环境作为Visual Studio .Net的附加组件实现,关于如可开发附加件的更多信息,请参考文章“定制附加件帮您最大化Visual
Studio .NET的生产力”。
为什么要设计时?
您需要设计时工具提供图形界面来操纵原始的XML仓储存储,此工具将在两个阶段内用到:
l
在屏幕的最初开发期间;
l
在定制化阶级期间;
此工具应提供图形设计器来操纵用户界面布局,就象在屏幕上添加控件,操纵控件的属性,重定屏幕大小等等。它应该能为控件添加事件及事件处理程序,从用户的角度来看,此工具抽象元数据结构,元数据存在哪里及如何存储。
在这里,我们不深究Visual Studio附加组件及客户端脚本,仅指望您清楚理解Visual Studio附加组件如何工作。示例源代码演示如何实现我们的Visual Studio附加组件。
设计器实现
使用Windows窗体设计器来设计窗体布局并为控件挂接事件,Visual Studio附件组件编程模型可让我们访问Visual Studio环境中当前开放的Window对象,通过检查Window对象是否是从称为IDesignerHost的接口继承的,来让我们查询window是否是设计窗口。
一旦安装附加组件,Tools菜单下将会有一个菜单项。

图1. 在Tools菜单下的附加组件
初始化后,设计时附加组件在Tools菜单下添加一组菜单项。现在,Tools菜单看起来如下:

图2. 附加组件初始化后展开的Tools菜单
表1. 附件菜单项摘要
|
菜单项
|
行为
|
|
Attach
|
从活动文档获取IDesignerHost接口指针。活动文档应为Windows窗体设计器。
在某私有变量中保存IDesignerHost指针,以备将来使用。
|
|
New
|
新建一个元数据文档。
|
|
Open
|
从仓储中打开一个已存在的元数据文档。显示“打开文件”对话框,允许用户选择适当的元数据文档。
|
|
Show Form
|
转为XML文件形式。
|
|
Save
|
保存元数据文档。
|
|
Detach
|
从Windows窗体设计器分离。
|
Attach菜单项
打开一个新的C#语言的Windows窗体项目,为活动窗口获取IDesignerHost句柄,并保存在一个内部变量中。这也使其他菜单项允许。
每个设计器表面提供特定服务以监视它发生的动作,在当前情况下,我们获得IComponentChangeService,用来获取有关设计器表面发生的变化信息。我们为设计器编写了适当的事件处理器。
internal void
AttachDesignerToForm()
{
if
(applicationObject.ActiveDocument != null)
{
foreach (Window W in applicationObject.ActiveDocument.Windows)
{
designerHost = W.Object as IDesignerHost;
}
}
AttachDesignerEvent-handlers();
}
private void
AttachDesignerEvent-handlers()
{
//Component change events.
IComponentChangeService ccs =
(IComponentChangeService)designerHost.GetService
(typeof(IComponentChangeService));
if
(ccs != null)
{
ccs.ComponentChanged += new
ComponentChangedEventHandler(this.OnComponentChanged);
}
}
Detach菜单项
将designerHost对象置为空,并解开设计器上的所有事件处理程序,也要禁止除Attach外的所有附加组件菜单项。
public void
DetachFromDesigner()
{
DetachDesignerEvent-handlers();
this.designerHost = null;
}
private void
DetachDesignerEvent-handlers()
{
IComponentChangeService ccs =
(IComponentChangeService)designerHost.GetService
(typeof(IComponentChangeService));
if
(ccs != null)
{
ccs.ComponentChanged -= new
ComponentChangedEventHandler(this.OnComponentChanged);
}
}
New菜单项
为当前项目添加一个空的窗体,并为窗体获取设计器对象,您可通过拖放开始窗体设计,添加事件处理器,设置控件属性,及实现事件处理器。
设计器仅需捕获的事件是ComponentChanged事件,这用来记录控件触发的事件及相应的事件处理器名称。
private void
OnComponentChanged(object sender, ComponentChangedEventArgs cea)
{
if
(cea.Member.GetType().Name.Equals("EventPropertyDescriptor"))
{
string ctrlName = ((Control)cea.Component).Name;
EventInfo ei = new EventInfo(cea.Member.Name,cea.NewValue.ToString());
ArrayList evtInfoList = new ArrayList();
if (!ctrlEvtList.ContainsKey(ctrlName))
{
evtInfoList.Add(ei);
}
else
{
evtInfoList = (ArrayList)ctrlEvtList[ctrlName];
if(!evtInfoList.Contains(ei))
{
evtInfoList.Add(ei);
}
ctrlEvtList.Remove(ctrlName);
}
ctrlEvtList.Add(ctrlName,evtInfoList);
}
}
Save菜单项
开始以下几步:
1.
提示您为XML文件提供要保存的位置及名称;
2.
递归处理当前设计器对象,收集其中的所有控件信息;
l
为每个在设计器内的对象创建一个MetadataControl对象,保存属性,就象name,tabIndex,location,size,type及visibility。
l
如果控件是其他控件的父控件,子控件的信息收集到父控件的数组成员MetadataControl中。
private MetadataControl GetTheControl(Control formCtrl)
{
MetadataControl newCtrl = new MetadataControl();
newCtrl.name = formCtrl.Name;
newCtrl.text = formCtrl.Text;
newCtrl.position.height = formCtrl.Height;
newCtrl.position.width = formCtrl.Width;
newCtrl.position.top = formCtrl.Top;
newCtrl.position.left = formCtrl.Left;
newCtrl.tabIndex = formCtrl.TabIndex;
newCtrl.type = formCtrl.GetType().ToString();
newCtrl.visible = formCtrl.Visible;
newCtrl.controls = new
MetadataControl[formCtrl.Controls.Count];
newCtrl.event-handlers = null;
if(newCtrl.controls.Length > 0)
{
int cnt = 0;
foreach (Control childCtrl in
formCtrl.Controls)
{
newCtrl.controls[cnt] = GetTheControl(childCtrl);
cnt++;
}
}
return newCtrl;
}
3.
控件触发的事件及相应的处理程序名称存在event-handlers(类型为EventInfo)成员中,这是更新窗体的MetadataControls成员。
private void IntegrateCtrlWithEvents(MetadataControl[] arr)
{
foreach(MetadataControl ctrl in arr)
{
if(ctrlEvtList.ContainsKey(ctrl.name))
{
ArrayList ei =
(ArrayList)ctrlEvtList[ctrl.name];
ctrl.event-handlers
= new EventInfo[ei.Count];
ei.CopyTo(ctrl.event-handlers);
}
else
{
ctrl.event-handlers
= new EventInfo[0];
}
if (ctrl.controls.Length > 0)
{
IntegrateCtrlWithEvents(ctrl.controls);
}
}
}
4.
解析窗体代码文件以得到事件处理器代码。这是添加MetadataForm的脚本元素。
private ArrayList GetEventCode()
{
ArrayList evtCode =
null;
try
{
ProjectItem pi = applicationObject.ActiveDocument.ProjectItem;
MetadataCodeSpace codespace = new MetadataCodeSpace(pi);
string[] evtHdlrNames = GetOnlyEvtHandlers(ctrlEvtList.Values);
evtCode = codespace.ExtractEventCode(evtHdlrNames);
}
catch(Exception e)
{
MessageBox.Show(e.Message,"Error !!!");
return null;
}
return evtCode;
}
public ArrayList ExtractEventCode(string[] evtHandlers)
{
eventItem tempEvt;
// Parse code file for event code
try
{
CodeElement celt = projectItem.FileCodeModel.CodeElements.Item(1);
CodeNamespace cNamespace =
(CodeNamespace)celt;
CodeClass cClass =
(CodeClass)(cNamespace.Members.Item(1));
foreach (CodeElement ce in
cClass.Members)
{
switch (ce.Kind)
{
case EnvDTE.vsCMElement.vsCMElementFunction:
CodeFunction cf = (CodeFunction)ce;
//Chk whether its an eventhandler
if (foundEvtHandler(evtHandlers,cf.Name))
{
TextPoint startPt = cf.StartPoint;
TextPoint endPt = cf.EndPoint;
EditPoint editPt = startPt.CreateEditPoint();
tempEvt = new eventItem(cf.Name,editPt.GetLines(startPt.Line,endPt.Line+1),contentType.text);
evtCodeList.Add(tempEvt);
}
tempEvt = null;
break;
}
}
}
catch(Exception e)
{
string s = e.Message;
}
return evtCodeList;
}
5.
一旦收集到所有窗体信息,新的MetadataForm对象被创建并序列化成XML,此XML按步骤1中的文件名保存。
// Add all the controls to the form object
activeForm = new
MetadataForm(ctrls,evts,GetActiveFormName());
//activeFileName contains name of the file user has
chosen
StreamWriter sw = new StreamWriter(activeFileName);
XmlSerializer writer = new
XmlSerializer(typeof(MetadataForm));
//Serialize the activeForm member variable
writer.Serialize(sw, activeForm);
sw.Close();
运行时实现
Open菜单项
提示您提供在设计器中想打开的XML文件路径,当指定名字后,将发生由XML文件重现窗体的过程。
以下是此过程内的有关步骤:
l
反序列化XML文件到CLR类型MetadataForm;
l
创建空的窗体并基于MetadataForm对象内的form控件设置它的属性;
l
创建控件并在窗体上适当定位;
l
挂接窗体上控件的事件处理程序
l
插入从XML文件内读取的事件处理程序代码到后台代码文件内。
反序列化
象我们以前看到的,第一步是反序列化仓储文件为MetadataForm类型并将创建的类型存在一个称为activeFom的成员变量中,这里是完成这个的代码片断。
//Create the
serializer
XmlSerializer
serializer = new XmlSerializer(typeof(MetadataForm));
FileStream
stream = null;
try {
//Open the file
Stream
= new
FileStream(filename, FileMode.Open,
FileAccess.Read);
//De-serialize the XML into CLR type
activeForm =
(MetadataForm)serializer.Deserialize(stream);
stream.Close();
}
catch(Exception
e) {
MessageBox.Show(e.Message);
}
创建新窗体
为当前项目添加新的空窗体并用MetadataForm对象内的窗体属性初始化。
// Adding a new
form to project
Project proj =
applicationObject.Solution.Projects.Item(1);
ProjectItem pi =
proj.ProjectItems.AddFromTemplate
(@"C:\Program
Files\Microsoft Visual Studio .NET
2003\VC#\CSharpProjectItems\CSharpAddWinFormWiz.vsz",
formName+".cs");
applicationObject.Windows.Item(formName+".cs
[Design]").Activate();
// Setting
properties of the form
private void
SetNewFormProperties(MetadataControl ctrl)
{
((Form)designerHost.RootComponent).Name=ctrl.name;
((Form)designerHost.RootComponent).Text=ctrl.text;
((Form)designerHost.RootComponent).Height=ctrl.position.height;
((Form)designerHost.RootComponent).Width=ctrl.position.width;
}
运行时创建控件
反射的概念用来在运行时创建控件,就象我们在仓储讨论中看到的,仓储结构包含关于窗体内嵌控件的信息。
<control
type="System.Windows.Forms.Button">
<position>
<height>23</height>
<width>75</width>
<top>192</top>
<left>192</left>
</position>
<controls />
<name>button1</name>
<text>Submit</text>
<tabIndex>0</tabIndex>
</control>
如果看一下XML片断,control标记包含type特性,该特性含有控件的完全限定CLR类型,此刻,我们需要做的是利用反射创建控件,我们用Activator类来创建控件实例,完成这个的代码片断如下所示。此函数接受父控件(Winodws.Forms.Control)及子控件集合(MetadataControl)作为参数。
private void
AddControlsToActiveForm(MetadataControl[] ctrl,Control parent)
{
try
{
foreach (MetadataControl child in ctrl)
{
System.Type controltype = Type.GetType(child.type);
//unknown control type. Handle this as an error
if (controltype == null) continue;
//Create the control
Control control = (Control)Activator.CreateInstance(controltype);
control.Name = child.name;
control.Text = child.text;
control.Top = child.position.top;
control.Left = child.position.left;
control.Height = child.position.height;
control.Width = child.position.width;
control.Visible = true;
control.TabIndex = child.tabIndex;
parent.Container.Add(control);
PropertyDescriptor parentProp =
TypeDescriptor.GetProperties(control).Find
("Parent",true);
parentProp.SetValue(control, parent );
if (child.controls.Length > 0)
AddControlsToActiveForm(child.controls,control);
}
}
catch (Exception e)
{
MessageBox.Show(e.Message );
}
}
1.
首先我们使用Type.GetType得到给定类型的CLR类型,它接受一个包含完全限定CLR类型名称的字符串作为参数;
2.
然后,我们调用Activator类的CreateInstance方法创建控件实例并转换控件类型为典型控件,即所有Windows窗体控件的基类;
3.
然后我们设置控件的适当属性;
4.
当创建控件后,我们把它添加到父控件的Controls集合,如果子控件还有子控件,我们传递适当参数递归调用该函数。
为控件附加事件处理程序
每一个在仓储内的MetadataControl维护一个触发的事件及相应事件处理程序的集合,此信息用来绑定运行时创建的控件与指定的事件处理程序。designerHost的IEventBindingService服务可用于此。每个控件有一个可触发事件列表,利用控件的属性列表绑定事件与相应的事件处理程序。
IEventBindingService
eventservice =
IEventBindingService)designerHost.GetService
(typeof(IEventBindingService));
if( eventservice
!= null )
{
if((child.event-handlers != null) || (child.event-handlers.Length > 0))
{
EventInfo[] eiArr = child.event-handlers;
foreach( EventInfo ei in eiArr)
{
EventDescriptor ed =
TypeDescriptor.GetEvents(control).Find(ei.eventName,true);
if( ed == null )
break;
PropertyDescriptor pd = eventservice.GetEventProperty(ed);
if( pd == null )
break;
pd.SetValue(control, ei.eventHandler);
}
}
}
插入事件处理程序代码
最后,事件处理程序代码被插入到窗体代码文件,窗体的所有事件处理程序存于MetadataForm对象的Script元素中,利用CodeDom名称空间中的类,我们解析整个文件代码并适当插入代码处理程序。
private void
AddEventHandlerCode()
{
eventItem[] eiArr = activeForm.script;
ProjectItem pi = applicationObject.ActiveDocument.ProjectItem;
CodeElement celt = pi.FileCodeModel.CodeElements.Item(1);
CodeNamespace cNamespace = (CodeNamespace)celt;
CodeClass cClass = (CodeClass)(cNamespace.Members.Item(1));
foreach (CodeElement ce in cClass.Members)
{
switch (ce.Kind)
{
case EnvDTE.vsCMElement.vsCMElementFunction:
CodeFunction cf = (CodeFunction)ce;
foreach(eventItem ei in eiArr)
{
if (cf.Name.Equals(ei.eventHandler))
{
EditPoint ep =
cf.GetStartPoint(vsCMPart.vsCMPartBody)
.CreateEditPoint();
ep.Insert(justCodeBlock(ei.content));
}
}
break;
}
}
}
private string
justCodeBlock(string fnCode)
{
string tmpcode = fnCode.Substring(fnCode.IndexOf("{")+1);
return tmpcode.Substring(0,tmpcode.Length-1);
}
Show Fom菜单项
XML文件窗体可通过附加组件从其菜单调用Show Fom来运,这是运行一个在设计中已经打开的已存在的XML文件,或者另外提示您提供XML文件路径,这会打开当前项目中的XML文件(与Open 菜单过程一样),通过生成方案,由附加组件代码来运行。
public void
ShowForm()
{
try
{
if( applicationObject.Solution.Projects.Item(1).ProjectItems.Count == 0 )
{
OnOpen();
}
BuildNRunSolution();
}
catch(Exception e)
{
MessageBox.Show(e.Message);
}
}
private void
BuildNRunSolution()
{
applicationObject.Solution.SolutionBuild.Build(true);
applicationObject.Solution.SolutionBuild.Run();
}
结论
在本文当中,我讨论了利用.NET Framework实现仓储或元数据驱动的用户接口方法,我们也解释了如何利用.NET Framework类建立可定制化窗体,以XML存储窗体,加载XML文件到设计器,修改XML文件以及加载并运行窗体。