很多时候,我们希望可以扩展或者改善应用程序的UI,特别在应用程序已经发布后。通常情况下,这意味着要重新部署整个应用程序,本文探讨了一种插件体系结构,可以在任何时候,实现应用程序UI的改进。
一、概述
当不考虑插件体系架构之前,你的应用程序通常是这种情况:应用程序UI插件之间没有任何交互,这不是说这些UI插件不能共享一个公共的数据结构或者业务对象,而是说这些插件之间不能直接去调用对方。
在我们要讨论的插件体系结构中,所有的UI元素都被设计成包含在一组基于System.Windows.Forms.UserControl类型上的插件集,并且所有的插件都由一个配置文件进行描述,能够在运行时动态地加载。通过创建新的插件,并在配置文件中添加对应的配置,就可以完成应用程序UI的扩展。
这样的插件体系结构,具有很多优点:
· 不同的UI元素可以独立地开发。例如,如果正在开发个人信息管理系统(PIM),可以让一个人去实现“约会/日程安排”的UI,而派另一个人去完成“联系人”的UI
· 更好的系统控制。你可以根据用户的姓名或者用户的角色,甚至基于所购注册码的选项,来限制系统提供给用户的功能
· 你可以在任何时候添加新的UI,例如上述PIM的例子,你可以在应用程序发布之后,添加一个“日记”功能的UI
本插件体系结构包含如下三个方面的内容:
1. 一个shell,用户加载和浏览插件
2. 一个实现shell和插件之间所有通信的类
3. 各自独立的UI插件
二、shell的实现
当应用程序启动时,shell负责读取配置文件中每个UI的名字和位置信息,然后shell通过反射来加载每个插件,在下面的截图中,shell应用包含了一个列表框控件,用于展示插件列表,同时它还包含一个面板控件用于加载插件。
下面是一个加载两个插件后的shell示例,左边的列表框用于展示所有可用选择的插件,当插件被选中后,右边的面板则用来显示插件。
当点击左边列表中的"PlugIn1"后,可以看到"PlugIn1"插件被加载到右边的面板中:
而点击左边列表框中的"PlugIn Number2"后,可以看到右边的面板加载了"PlugIn Number2":
在标签风格的Shell中,可以看到另外一种加载插件的界面:
下图是选中"PlugIn Number2"后的界面:
三、Shell如何发现插件的?
插件通过config.xml文件来配置,并在运行时被载入:
<?xml version="1.0" encoding="utf-8" ?>
<PlugIns>
<PlugIn Location="E:\ExtensibleUI\OurControls\bin\Debug\OurControls.dll"
Name="OurControls.PlugIn1"></PlugIn>
<PlugIn Location="E:\ExtensibleUI\OurControls\bin\Debug\OurControls.dll"
Name="OurControls.PlugIn2"></PlugIn>
</PlugIns>
当Shell应用的主窗体load时,程序通过DataSet对象的ReadXml方法加载config.xml配置。然后通过AddPlugIn方法遍历配置文件中PlugIns内的每一行来加载插件的"Location"和"Name"。
{
DataSet ds = new DataSet();
ds.ReadXml("Config.xml");
foreach(DataRow dr in ds.Tables["Plug-In"].Rows)
{
AddPlugIn(dr["Location"].ToString(),
dr["Name"].ToString());
}
}
四、两个示例插件的代码:
AddPlugIn方法加载包含插件的程序集,并创建插件对象实例,然后将插件的名字添加到左边的列表框中,当列表框中的一个插件被选中后,我们就隐藏当前的插件,显示被选中的插件。
// Also set the list box to nav1gate between plug-ins.
private void AddPlugIn(string Location, string ControlName)
{
Assembly ControlLib;
PlugIn NewPlugIn;
// Load the assembly.
ControlLib = Assembly.LoadFrom(Location);
// Now create the plugin.
NewPlugIn = (PlugIn)ControlLib.CreateInstance(ControlName);
NewPlugIn.Location = new System.Drawing.Point(0, 0);
NewPlugIn.Dock = DockStyle.Fill;
NewPlugIn.Visible = false;
// Add it to the panel, note that its Visible property is false.
panel1.Controls.Add(NewPlugIn);
// Set up the ClickHandler
NewPlugIn.Clicked += new PlugInLib.ClickHandler(Control_Clicked);
// Add the plugin to the listBox, listBox will use ToString to
// get the text to display.
listBox1.Items.Add(NewPlugIn);
}
// When a new item in the listBox is selected,
// hide the current plugin and show the new.
private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)
{
if(CurrentPlugIn!=null)
{
CurrentPlugIn.Visible = false;
}
CurrentPlugIn = (PlugIn)listBox1.SelectedItem;
CurrentPlugIn.Visible = true;
}
在标签风格的shell程序中,AddPlugIn方法的实现略有不同。标签风格的Shell不需要像列表框那样的导航处理代码,因为标签控件已经能够自己处理了。
private void AddPlugIn(string Location, string ControlName)
{
Assembly ControlLib;
PlugIn NewPlugIn;
// Load the assembly.
ControlLib = Assembly.LoadFrom(Location);
// Now create the plugin.
NewPlugIn = (PlugIn)ControlLib.CreateInstance(ControlName);
NewPlugIn.Location = new System.Drawing.Point(0, 0);
NewPlugIn.Dock = DockStyle.Fill;
NewPlugIn.Visible = true;
// Create a new TabPage.
TabPage newPage = new TabPage();
// Set the text on the tabPage with the PlugIn Caption.
newPage.Text = NewPlugIn.Caption;
// Add the PlugIn to the TabPage.
newPage.Controls.Add(NewPlugIn);
// Add the page to the tabControl.
tabControl1.TabPages.Add(newPage);
// Set up the ClickHandler
NewPlugIn.Clicked += new PlugInLib.ClickHandler(Control_Clicked);
}
五、插件核心类PlugIn:
插件核心类PlugIn继承自System.Windows.Forms.UserControl类,通过扩展UserControl类对外提供预定义事件、方法、以及属性,使得每一个插件可以与shell应用程序进行通信。在这个示例中,PlugIn预定义了Clicked事件、Caption属性、TestFunction方法。此外,ToString方法也被重写,以便返回Caption属性而不是对象的名称。
using System.Windows.Forms;
namespace PlugInLib
{
/// <summary>
/// A delegate type for hooking up notifications.
/// </summary>
public delegate void ClickHandler(object sender, EventArgs e);
/// <summary>
/// Summary description for PlugIn.
/// </summary>
public class PlugIn : System.Windows.Forms.UserControl
{
// The following provides "Clicked" event back to the container.
public event ClickHandler Clicked;
protected void DoClick(EventArgs e)
{
if (Clicked != null)
Clicked(this, e);
}
// Provide a "Caption" that the container can display.
protected string m_Caption = "PlugIn";
public string Caption
{
get
{
return m_Caption;
}
set
{
m_Caption = value;
}
}
public override string ToString()
{
return m_Caption;
}
// Provide a method "TestFunction" that the container can call.
public virtual void TestFunction()
{
}
}
}
六、如何创建一个插件
1. 使用Visual Studio创建一个“Windows窗体控件库”项目
2. 添加对PlugInLib项目的引用,引入PlugIn核心类
3. 将缺省的窗体控件的名字修改为更有意义的名称
4. 用using 添加对PlugInLib的引用
5. 修改窗体控件的基类由 System.Windows.Forms.UserControl 修改为 PlugIn
6. 关联任何希望发送给shell应用的事件
7. 适当重写将被shell调用的方法
8. 实现你所希望的UI构造函数
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using PlugInLib; // <---Add using for the plug-In base class
namespace OurControls
{
/// <summary>
/// Summary description for PlugIn3.
/// </summary>
public class PlugIn3 : PlugIn // <---Change base class to PlugIn
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
public PlugIn3()
{
// This call is required by the Windows.Forms Form Designer.
InitializeComponent();
// TODO: Add any initialization after the InitForm call
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
//
// PlugIn3
//
this.Caption = "PlugIn 3";
this.Name = "PlugIn3";
this.Click += new System.EventHandler(this.PlugIn3_Click);
}
#endregion
// Override Base class to receive call from the shell.
public override void TestFunction()
{
Console.WriteLine("TestFunction called by the shell.");
}
// Send clicks to the shell, just because we can.
private void PlugIn3_Click(object sender, System.EventArgs e)
{
DoClick(e);
}
}
}
七、结论:
在上述介绍的插件体系中,插件与shell应用之间的交互应该设计良好。在上面的PlugIn核心类例子中,shell与插件之间唯一真正的交互是通过Caption属性来实现的。另外一种实现方式是shell加载一个公共的数据结构,然后在每个插件被加载时传递给它。
你可以在任何时候添加一个插件,通过创建一个新的插件并在config.xml文件中适当配置它,就是这么简单!