Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an Extensible User Interface with .NET, Part 1

0.00/5 (No votes)
15 Dec 2002 2  
An architecture for extending the User Interface of a program via plug-in components.

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:

  1. A �shell� application that handles the loading and navigation between the plug-ins.
  2. A base class that supplies all communications between the �shell� and the plug-ins.
  3. 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.

<?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>

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.

// Load and add a plug-in to the panel1 control

// Also set the list box to navigate between plugins.

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);

}

private PlugIn CurrentPlugIn;

// 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;
}

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.

// 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);
}

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
{
    /// <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()
        {
        }
    }
}

Creating a UI plug-in.

  1. Use Visual Studio to create a new �Windows Control Library�.
  2. Add a reference to the PlugInLib that contains the plug-in base class.
  3. Change the name of the user control from UserControl1 to something more descriptive.
  4. Add the using directive for the PlugInLib.
  5. Change the base class for the user control from System.Windows.Forms.UserControl to PlugIn.
  6. Connect any events that you want to send to the shell applications.
  7. Add the necessary overrides for calls from the shell to the plug-in.
  8. 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;            // <---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);
        }
    }
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here