利用反射实现简单 UI 自动化
周融
(C) 2007 All Rights Reserved.
在实现分布式系统设计的过程中,在 UI 设计层往往会遇到这样的问题,我们希望建立一个用户界面工厂,其他开发人员和第三方软件公司的开发人员可以通过这个工厂很容易的产生界面扩充加载项。或者希望不需要写复杂的注册代码,就能够使某一种元数据的继承子类(如某个特定控件所对应的单击命令)自动全部注册到工厂中。本文通过介绍反射技术并通过一些实例让开发人员了解一种通过反射实现这种简单 UI 自动化的原理和方法。
在本文中:
1、问题
2、简单的处理方法
3、什么是反射
4、利用反射实现 UI 自动化
5、结论
问题
假设这样一个开发场景:在一个 Ribbon 界面应用程序中,所有的界面按钮元素都关联到特定的单击事件上,而单击事件处理程序又可以根据界面按钮的特性(如按钮名称)分辨出需要执行哪种动作,实现目标是尽量分散 UI 设计人员和逻辑编码人员之间的资源耦合。UI 设计人员只负责勾画 UI 并为每个按钮元素取名;逻辑编码人员则仅仅编写与该按钮对应的执行过程,至于如何关联它们,则由 UI 自动化自动完成。
这个例子看似简单,但分析起来并不是那么容易。首先,对于每一个截面按钮元素,到底是采用一个统一的单击事件入口,还是各自定义;其次,如何将每个按钮的单击事件中所需要执行的过程抽象出来;再次,如何建立按钮元素和这个抽象执行对象之间的关系;最后,如何解决自动关联按钮元素和执行对象。这一系列的问题,都不好解决。
利用建模工具和一些经验,发现必须描述一个统一的单击事件处理入口以方便 UI 自动化;另外,可以构建一个可继承的 Cmmand 类用来描述每个执行命令。我们发现,这样抽象出来原形后,需要解决如下问题。
1、如何寻找应用程序主窗体中所有的 Ribbon 按钮;
2、如何统一设定这些按钮的单击事件;
3、如何设计 Command 类;
4、如何使用 UI 自动化,将 Command 类的继承类和 Ribbon 按钮的单击事件处理程序自动结合起来。
只有解决了以上的问题,才能实现本文开头所提出的设计目标。
简单的处里方法
对于问题 1、2,寻找 Ribbon 按钮并自动增加单击事件并不难,让我们通过 Linq 来实现。请参考下面的代码:

/**//// <summary>加载菜单项目和命令。</summary>
private static void LoadMenuItems()

...{
// 图标
Program.ApplicationMainForm.Ribbon.ApplicationIcon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath).ToBitmap();
Program.ApplicationMainForm.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath);

// 对于每一个命令按钮,设置其默认的 Click 事件。
Command.Initialize();
var buttons = from item in Program.ApplicationMainForm.Ribbon.Items.OfType<DevExpress.XtraBars.BarItem>()
where item is DevExpress.XtraBars.BarButtonItem
select item;

foreach (var button in buttons)

...{
// 将每一个按钮对象关联到特定的命令对象上。
// 设置单击事件。
button.ItemClick += (object sender, DevExpress.XtraBars.ItemClickEventArgs e) =>

...{
Command cmd = Command.GetCommandByName(e.Item.Name);
if (cmd != null)
cmd.ExecuteCommand(new object(), new EventArgs());
};
}
}


这段代码为每一个 Ribbon 按钮设置了默认的 Click 事件。并且可以看到,如果 Click 事件通过 Command.GetCommandByName() 方法得到名称和按钮名称相同的 Command 实例。如果存在关联实例,则执行,否则不执行。这样我们就可以利用 Command 对象的 Add 和 Remove 方法动态增加/删除命令了。
我们可以把定义 Command 对象的类放在另一个程序集中,这样做的好处是可以供第三方开发人员调用并扩展 Command 的派生实例。并创建加载项。完整的 Command 定义如下:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;

namespace Heading.LeafPsp.UI

...{

/**//// <summary>表示执行一个命令的事件处理委托。</summary>
/// <param name="sender">System.Object 的实例。</param>
/// <param name="e">System.EventArgs 的实例。</param>
public delegate void ExecuteCommandEventHandler(object sender, EventArgs e);


/**//// <summary>
/// 表示一个应用程序界面元素传递给 Windows 的一个命令。例如文件打开、应用程序退出等。
/// 此类的所有继承类都将自动被注册到应用程序,并与名称和该命令名称相同的界面元素发生单击事件关联。
/// </summary>
public class Command

...{
private static List<Command> commands = new List<Command>();
private static bool isInitialized = false;


/**//// <summary>
/// 创建 Command 类的新实例。
/// </summary>

public Command() : base() ...{ }


/**//// <summary>
/// 创建 Command 类的新实例。
/// </summary>
/// <param name="commandName">
/// 命令的名称,该名称如果与某个 System.Windows.Forms.Control 名称相同,则将被自动关联到
/// 与此 System.Windows.Forms.Control 实例对应的单击事件上。
/// </param>

public Command(string commandName) : this() ...{ this.Name = commandName; }


/**//// <summary>
/// 创建 Command 类的新实例。
/// </summary>
/// <param name="commandName">
/// 命令的名称,该名称如果与某个 System.Windows.Forms.Control 名称相同,则将被自动关联到
/// 与此 System.Windows.Forms.Control 实例对应的单击事件上。
/// </param>
/// <param name="handler">指定额外绑定到这个命令对象上的执行处理程序。</param>

public Command(string commandName, ExecuteCommandEventHandler handler) : this(commandName) ...{ this.Execute += handler; }


/**//// <summary>
/// 将一个 Command 的实例注册到应用程序全局命令列表中。除非您编写加载项,否则不需要显式调用此方法。
/// </summary>
/// <param name="command">需要注册的 Command 的实例。</param>
public static void RegisterCommand(Command command)

...{
var count = (from item in commands where item.Name == command.Name select item).Count();
if (count != 0)
throw new Exception(string.Format("名称为 {0} 的命令已经被注册。", command.Name));
commands.Add(command);
}


/**//// <summary>
/// 释放已经注册到应用程序全局命令列表中的一个命令。
/// </summary>
/// <param name="command">需要释放的 Command 的实例。</param>
public static void ReleaseCommand(Command command)

...{
commands.Remove(command);
}


/**//// <summary>
/// 获取已经注册到应用程序全局命令列表中的命令集合。
/// </summary>
/// <returns>包含 Command 列表的 Command 数组。</returns>

public static Command[] GetRegisteredCommands() ...{ return commands.ToArray(); }


/**//// <summary>
/// 释放已经注册到应用程序全局命令列表中的所有命令实例。
/// </summary>

public static void ReleaseAllCommands() ...{ commands.Clear(); }


/**//// <summary>
/// 获取指定的已注册到应用程序全局命令列表中的命令。
/// </summary>
/// <param name="commandName">命令的名称。</param>
/// <returns>与此名称相同的 Command 实例。如果没有发现匹配项,则返回 null。</returns>
public static Command GetCommandByName(string commandName)

...{
var q = from item in commands where item.Name.ToUpper() == commandName.ToUpper() select item;
if (q.Count() != 0)
return q.First();
else
return null;
}


/**//// <summary>
/// 执行该命令。继承类需要重写此方法以便实现特定逻辑。
/// </summary>
/// <param name="sender">System.Object 的实例。</param>
/// <param name="e">System.EventArgs 的实例。</param>

public virtual void ExecuteCommand(object sender, EventArgs e) ...{ if (this.Execute != null) Execute(sender, e); }


/**//// <summary>
/// 获取或设置此命令的名称。
/// </summary>

public string Name ...{ get; set; }


/**//// <summary>命令执行事件。</summary>
public event ExecuteCommandEventHandler Execute;


/**//// <summary>
/// 利用反射自动注册所有该程序集定义的命令对象。这些命令对象从 Command 对象继承而来。
/// 该过程只能被调用一次。
/// </summary>
public static void Initialize()

...{
RegisterCommand(new ExitApplicationCommand());
RegisterCommand(new openDataFileCommand());
}
}


/**//// <summary>
/// 关闭应用程序的命令。
/// </summary>
internal class ExitApplicationCommand : Command

...{

public ExitApplicationCommand() : base("exitButton") ...{ RegisterCommand(this); }

public override void ExecuteCommand(object sender, EventArgs e)

...{
base.ExecuteCommand(sender, e);
System.Windows.Forms.Application.Exit();
}
}

internal class OpenDataFileCommand : Command

...{

public OpenDataFileCommand() : base("openDataFileButton") ...{ RegisterCommand(this); }

public override void ExecuteCommand(object sender, EventArgs e)

...{
base.ExecuteCommand(sender, e);
System.Windows.Forms.OpenFileDialog dialog = new System.Windows.Forms.OpenFileDialog();
dialog.ShowDialog();
}
}
}


让我们关注到 Command.Initialize() 的静态方法上,这里使用传统的手动注册实例的方法为 Command 的派生类注册,这样做显然存在弊端,比如 Command 派生类较多时,该函数会很长;当开发人员新建 Command 派生类时,还需要改写 Initialize() 方法;另外,由于 Initialize() 需要改动,则不能将包含有 Command 类的程序集进行 SDK 分发。这一系列问题说明,使用传统的方法进行 UI 自动化是不成功的。
什么是反射
反射是利用 CLR 检索程序集、类、属性类等运行时信息的有效途径。它能帮助开发人员:
1、获取任何您需要的对象运行时信息,包括程序集、类、域、方法、属性、事件等。
2、动态创建某个类型和类型的实例。
3、动态执行某个方法或设置属性。
4、检索属性类(Attribute Class)。
5、动态加载程序集。
利用 System.Reflection 命名空间,就可以非常容易的使用反射。下面让我们利用反射技术来将程序集中的所有 Command 类的派生类动态注册到 Command 工厂上。
首先,我们需要在 Command.cs 上引用 System.Reflection 命名空间。
然后改写 Command.Initialize() 方法。

/**//// <summary>
/// 利用反射自动注册所有该程序集定义的命令对象。这些命令对象从 Command 对象继承而来。
/// 该过程只能被调用一次。
/// </summary>
public static void Initialize()

...{
if (isInitialized)
throw new Exception("Initialize 过程只能被调用一次。");
isInitialized = true;

// 动态注册所有 Command 类的派生类实例。
Assembly assembly = Assembly.GetExecutingAssembly();
Type[] types = assembly.GetTypes();
var commands = from item in types
where item.IsSubclassOf(typeof(Command))
select item;
foreach (var command in commands)

...{
Command cmd = (Command)assembly.CreateInstance(command.ToString());
}
}


这个改写后的方法找到当前运行程序集中所有 Command 的派生子类,并为每一个 Command 的派生子类创建了新的实例。开发人员在编写 Command 的派生类时,只需要按照类似于下面的结构定义对象,注册的工作就会由 UI 自动化完成。
一个用来打开数据文件的 Command 例程如下。
internal class OpenDataFileCommand : Command

...{

public OpenDataFileCommand() : base("openDataFileButton") ...{ RegisterCommand(this); }

public override void ExecuteCommand(object sender, EventArgs e)

...{
base.ExecuteCommand(sender, e);
System.Windows.Forms.OpenFileDialog dialog = new System.Windows.Forms.OpenFileDialog();
dialog.ShowDialog();
}
}


使用反射实现 UI 自动化
使用 Reflection,很容易实现在经典 Win32 和 COM 开发模型中难以实现的目标,如:
1、加载项。可动态检索特定文件夹下的 DLL 程序集并自动在应用程序启动时调用这些程序集中的某些函数进行注册。
2、可再分发。可以编写为第三方开发人员扩展应用程序功能的可再发行组件。
3、分布式组件开发。可实现类似于 COM 模型的更加高效和托管的分布式组件。
4、优化应用程序结构,使应用程序更加面向服务化(SOA)。
结论
事实上,本文所列举的利用反射实现 UI 自动化的例子仅仅是反射入门的开始。还有很多需要我们研究和深入了解的东西。特别是利用反射实现的 CLR 所特有的基于属性的编程(Attribute)和属性类,掌握这些技术可以为日后编写优秀的应用程序打下基础。