Introduction
In Part 1, I described an architecture that allows you to create user interface plug-ins that can be loaded at runtime. In this document, I will describe methods for accessing application level user interface elements from the plug-ins. Please see Creating an Extensible User Interface with .NET, Part 1 before reading this article.
Overview
Some user interface elements naturally belong to the shell and should not reside in the plug-in. By convention, a status bar should run the entire width of the application window. Putting a status bar inside a plug-in would look odd and out of place. The main menu is another UI element that belongs to the shell application, but should be accessible from a plug-in. The following two examples show methods you can use to provide access to these two user interface elements.
Adding a StatusBar to the shell.
In this first example we will add a status bar to the shell and give the plug-ins a reference to one of the status bar's panels. We also include a helper function for placing text into the status bar panel.
Base class changes.
First we will add the needed code to the plug-in base class located in PlugIn.cs.
- Add a local variable to hold the
StatusBarPanel
.
private System.Windows.Forms.StatusBarPanel _StatusPanel;
- Next add a property to set and get the
StatusBarPanel
.
public System.Windows.Forms.StatusBarPanel StatusPanel
{
get
{
return _StatusPanel;
}
set
{
_StatusPanel = value;
}
}
- And finally add a helper function that a plug-in can call to set the text in the
StatusBarPanel
. Note that we check that the _StatsPanel
has been set before assigning the text.
protected void ShowStatus(string Text)
{
if(_StatusPanel != null)
{
_StatusPanel.Text = Text;
}
}
Shell application changes
Now we can modify the shell application.
- Add a
StatusBar
control to the form.
- Add a
StatusBarPanel
to the StatusBar
.
- Within
AddPlugIn
set the plug-in�s StatusPanel
property to the StatusBarPanel
you just added.
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.ClickDelegate(PlugIn_Clicked);
NewPlugIn.SetMenu += new PlugInLib.SetMenuDelegate(PlugIn_SetMenu);
NewPlugIn.StatusPanel = statusBarPanel1;
listBox1.Items.Add(NewPlugIn);
}
Plug-in modifications
Adding menus to the shell
It is very desirable for each plug-in to provide its own set of menus. The menus should only appear when the plug-in is visible. Doing this is a little more complicated than the previous example. The overriding goal of this design is that the plug-in should not need to manage its own menus. When the plug-in is not visible, then its menus should be automatically removed from the shell's main menu.
In this example, the plug-in will define its menus and place a reference to them in the base class. The base class will then call a function in the shell to install or delete the menus. The base class will use its own visibility property to determine if the menus should be installed or deleted.
Base class changes.
Again we will start by modifying the plug-in base class.
- Add a declaration for a
SetMenuDelegate
. A delegate declaration defines a reference type that can be used to encapsulate a method with a specific signature. Here it defines a function that will be supplied by the shell application to add and delete items to its main menu.
public delegate void SetMenuDelegate(bool Install,
System.Windows.Forms.MenuItem[] MenuItems);
- Declare an instance of the
SetMenuDelegate
delegate. This is where the shell application will place a reference to the function that will add and delete menu items.
public SetMenuDelegate SetMenu;
- Since we are going to let the base class manage the menus, we need a place to store them.
private System.Windows.Forms.MenuItem[] _MenuItems;
- And we need a property to set them.
public System.Windows.Forms.MenuItem[] MenuItems
{
get
{
return _MenuItems;
}
set
{
_MenuItems = value;
}
}
- Override the
OnVisibleChanged
function.
This is where all the magic happens. When the plug-in's visibility changes, the base class will call the shell and let it add or delete the menus. After we have called SetMenu
, we call base.OnVisibleChanged(e)
so that normal processing continues. If we didn't do this, the plug-in would never receive "VisibileChanged" events.
protected override void OnVisibleChanged(System.EventArgs e)
{
if((SetMenu != null) && (_MenuItems != null))
{
SetMenu(this.Visible, _MenuItems);
}
base.OnVisibleChanged(e);
}
If your shell application shows multiple plug-ins simultaneously, you could override OnEnter
and OnLeave
to show the menus only when the plug-in has the focus.
Shell application changes
Next we modify the shell application to handle calls through the SetMenuDelegate
.
- Add a
MainMenu
control to the form and add any menus required for the shell application.
- Add a function to add and delete menus. This function must match the signature of the
SetMenuDelegate
in the plug-in base class.
private void PlugIn_SetMenu(bool Install,
System.Windows.Forms.MenuItem[] MenuItems)
{
if(Install == true)
{
this.mainMenu1.MenuItems.AddRange(MenuItems);
}
else
{
foreach(MenuItem m in MenuItems)
{
this.mainMenu1.MenuItems.Remove(m);
}
}
}
- In
AddPlugIn
, set the plug-in�s SetMenu
property to the PlugIn_SetMenu
function.
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.ClickDelegate(PlugIn_Clicked);
NewPlugIn.SetMenu += new
PlugInLib.SetMenuDelegate(PlugIn_SetMenu);
NewPlugIn.StatusPanel = statusBarPanel1;
listBox1.Items.Add(NewPlugIn);
}
Adding menus to the plug-in.
You cannot add a MainMenu
to a UserControl
. You will need to code your menus by hand. Due to all the overloaded MenuItem
constructors, this is quite easy to do.
- First declare your top-level menus.
private System.Windows.Forms.MenuItem FileMenu;
private System.Windows.Forms.MenuItem EditMenu;
- In the constructor for the plug-in, you can now create your complete menus.
Here we create a "File" menu with 2 sub-items, "New" and "Save" and an "Edit" menu with "Cut", "Copy" and "Paste". Then we bundle the top-level menus into an array and assign it to the MenuItems
property.
FileMenu = new MenuItem("File",new MenuItem[]
{
new MenuItem("New",new EventHandler(menuNew_Click)),
new MenuItem("Save",new EventHandler(menuSave_Click))
});
EditMenu = new MenuItem("Edit",new MenuItem[]
{
new MenuItem("Cut",new EventHandler(menuEdit_Click)),
new MenuItem("Copy",new EventHandler(menuEdit_Click)),
new MenuItem("Paste",new EventHandler(menuEdit_Click))
});
this.MenuItems = new System.Windows.Forms.MenuItem[]
{
FileMenu,
EditMenu,
};
Now when this plug-in becomes visible, its menus will be added to the application's menus. The events generated when a menu selection is made will fire directly to event handlers in the plug-in.
Conclusion
With the addition of these two methods of communicating with the shell application, you should have a complete framework for creating an application with an extensible user interface.
Notes
In the ExtensibleUI2_demo.zip file, config.xml file is located in both the Release and Debug directories of the "Shell" application. 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.