技术驱动生活,科技畅想未来!

坚持每天进步一点!

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

很多时候,我们希望可以扩展或者改善应用程序的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示例,左边的列表框用于展示所有可用选择的插件,当插件被选中后,右边的面板则用来显示插件。

clip_image001

当点击左边列表中的"PlugIn1"后,可以看到"PlugIn1"插件被加载到右边的面板中:

clip_image002

而点击左边列表框中的"PlugIn Number2"后,可以看到右边的面板加载了"PlugIn Number2":

clip_image003

在标签风格的Shell中,可以看到另外一种加载插件的界面:

clip_image004

下图是选中"PlugIn Number2"后的界面:

clip_image005

三、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"。

private void Form1_Load(object sender, System.EventArgs e)

{

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方法加载包含插件的程序集,并创建插件对象实例,然后将插件的名字添加到左边的列表框中,当列表框中的一个插件被选中后,我们就隐藏当前的插件,显示被选中的插件。

// Load and add a plug-in to the panel1 control
// 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不需要像列表框那样的导航处理代码,因为标签控件已经能够自己处理了。

// Load and add a plug-in to the TabControl1 control
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;
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;
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文件中适当配置它,就是这么简单!

posted on 2008-04-13 22:02  ajiefjcn  阅读(301)  评论(0编辑  收藏  举报