Introduction
Many times it can be desirable to extend or enhance the User Interface (UI) of a program after it has been deployed. Usually this means redeploying the entire application. This document describes a �plug in� architecture that allows for the UI to be extended at any time. An example of a program with an extensible UI is the Microsoft Management Console (MMC) and its associated snap-ins.
Overview
There is one main requirement that your program must meet before this architecture can be considered.
- There should be NO interaction between UI plug-ins. This does not mean they cannot share a common data structure or business objects, but each UI plug-in should not try to make direct calls to other plug-ins.
In this architecture all UI elements are contained in a set of plug-ins based upon the System.Windows.Forms.UserControl
class. All plug-ins are described in a configuration file, and loaded at runtime. Extending the UI is accomplished by creating a new plug-in and adding the appropriate entries to the config file.
Some of the advantages of this architecture are:
- Independent development of different UI elements. For example, if you are developing a Personal Information Manager (PIM), one person could be working on the "Appointments/Calender" UI, while another works on the "Contacts" UI.
- Application control. You could restrict the functionality of the application based upon the user's name or the user's roll or purchased options.
- You can add new UI elements at any time. In the PIM example above, you could add a "Diary" UI after the application has been distributed.
The architecture consists of 3 parts:
- A �shell� application that handles the loading and navigation between the plug-ins.
- A base class that supplies all communications between the �shell� and the plug-ins.
- The individual UI plug-ins themselves.
The shell
At startup the shell application reads a configuration file to get the names and locations for each UI plug-in. It then uses reflection to load each plug-in. In the screen shots below, the Shell application contains a ListBox
used to navigate between plug-ins, and a Panel
in which the plug-ins are loaded.
Here is an example of a �shell� with 2 plug-ins loaded. The ListBox
on the left is used to select between each plug-in, while the panel on the right will display the plug-in when it is made visible.
Clicking on "PlugIn1" made the plug-in visible.
And then clicking on "PlugIn Number 2" makes it visible in the panel.
The Tabbed Shell application shows another way of navigation:
Here is the Tabbed Shell after it has been loaded.
And here it is after "PlugIn Number 2" has been selected.
How the Shell finds the plug-ins.
The plug-ins to be loaded at runtime are listed in an XML file named config.xml.
="1.0" ="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>
In the form load event, the config.xml file is loaded into a DataSet
via ReadXml
. Then it iterates each DataRow
calling AddPlugin
with the �location� and �name� of the plug-in.
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());
}
}
Two examples of the AddPlugIn code.
The AddPlugIn
loads the assembly containing the plug-in and creates an instance of it. It also adds the plug-in to the ListBox
. When a new item in the list box is selected, we need to hide the current plug-in, and show the new selection.
private void AddPlugIn(string Location, string ControlName)
{
Assembly ControlLib;
PlugIn NewPlugIn;
ControlLib = Assembly.LoadFrom(Location);
NewPlugIn = (PlugIn)ControlLib.CreateInstance(ControlName);
NewPlugIn.Location = new System.Drawing.Point(0, 0);
NewPlugIn.Dock = DockStyle.Fill;
NewPlugIn.Visible = false;
panel1.Controls.Add(NewPlugIn);
NewPlugIn.Clicked += new PlugInLib.ClickHandler(Control_Clicked);
listBox1.Items.Add(NewPlugIn);
}
private PlugIn CurrentPlugIn;
private void listBox1_SelectedIndexChanged(object sender,
System.EventArgs e)
{
if(CurrentPlugIn!=null)
{
CurrentPlugIn.Visible = false;
}
CurrentPlugIn = (PlugIn)listBox1.SelectedItem;
CurrentPlugIn.Visible = true;
}
The AddPlugIn
for the Tabbed Shell application is only slightly different. The Tabbed Shell application needs no navigation code because it is handled by the TabControl
.
private void AddPlugIn(string Location, string ControlName)
{
Assembly ControlLib;
PlugIn NewPlugIn;
ControlLib = Assembly.LoadFrom(Location);
NewPlugIn = (PlugIn)ControlLib.CreateInstance(ControlName);
NewPlugIn.Location = new System.Drawing.Point(0, 0);
NewPlugIn.Dock = DockStyle.Fill;
NewPlugIn.Visible = true;
TabPage newPage = new TabPage();
newPage.Text = NewPlugIn.Caption;
newPage.Controls.Add(NewPlugIn);
tabControl1.TabPages.Add(newPage);
NewPlugIn.Clicked += new PlugInLib.ClickHandler(Control_Clicked);
}
The PlugIn base blass
The PlugIn
base class is based upon the System.Windows.Forms.UserControl
class and extends it to provide predefined events, methods, and properties that each plug-in can use to communicate with the shell applications. In this example a Clicked
event, a Caption
property, and a TestFunction
method are predefined. In addition, ToString
is overridden to return the Caption
instead of the object name.
using System;
using System.Windows.Forms;
namespace PlugInLib
{
public delegate void ClickHandler(object sender, EventArgs e);
public class PlugIn : System.Windows.Forms.UserControl
{
public event ClickHandler Clicked;
protected void DoClick(EventArgs e)
{
if (Clicked != null)
Clicked(this, e);
}
protected string m_Caption = "PlugIn";
public string Caption
{
get
{
return m_Caption;
}
set
{
m_Caption = value;
}
}
public override string ToString()
{
return m_Caption;
}
public virtual void TestFunction()
{
}
}
}
Creating a UI plug-in.
- Use Visual Studio to create a new �Windows Control Library�.
- Add a reference to the
PlugInLib
that contains the plug-in base class.
- Change the name of the user control from
UserControl1
to something more descriptive.
- Add the
using
directive for the PlugInLib
.
- Change the base class for the user control from
System.Windows.Forms.UserControl
to PlugIn
.
- Connect any events that you want to send to the shell applications.
- Add the necessary overrides for calls from the shell to the plug-in.
- Construct your UI as you would for any other
UserControl
.
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using PlugInLib;
namespace OurControls
{
public class PlugIn3 : PlugIn
{
private System.ComponentModel.Container components = null;
public PlugIn3()
{
InitializeComponent();
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Component Designer generated code
private void InitializeComponent()
{
this.Caption = "PlugIn 3";
this.Name = "PlugIn3";
this.Click += new System.EventHandler(this.PlugIn3_Click);
}
#endregion
public override void TestFunction()
{
Console.WriteLine("TestFunction called by the shell.");
}
private void PlugIn3_Click(object sender, System.EventArgs e)
{
DoClick(e);
}
}
}
Conclusions
With this architecture, the interaction between the plug-ins and the shell should be well defined and limited. In the PlugIn
base class example shown above, the only real interaction between the shell and plug-in is the Caption
property. Another possible interaction would be for the shell to load a common data structure that is passed to each plug-in when it is loaded.
You can add new plug-ins at any time, simply by creating a new plug-in and adding the appropriate entries to the config.xml file.
Notes
In the demo zip available in the downloads section, config.xml file is located in both the Release and Debug directories of the two "Shell" applications. These files contain the absolute pathname to the OurControls.dll including a drive letter. You will need to modify these paths for your local system.