Click here to Skip to main content
16,017,248 members
Articles / Programming Languages / C#
Article

Application Suite Template

Rate me:
Please Sign up or sign in to vote.
3.39/5 (12 votes)
10 Mar 2004CDDL5 min read 66.3K   892   36   12
An example of building an application suite using reflection and custom attributes to dynamically discover and add child applications.

Sample Image - SuiteApp.jpg

Introduction

Suites are a way to package and distribute applications in a common framework to give a common look and feel, or to group functionality under a common container. Extending the suite to add new applications, or to update existing ones, can be problematic. The approach I will demonstrate here is to use dynamic runtime discovery to load applications from DLLs.

Credits

The Outlook Bar used in this project is from Marc Clifton’s article An Outlook Bar Implementation. I made a small modification to support an additional field.

Suite Container

The suite container is the shell that holds everything together. It gives the applications a common look and feel, menu items, and basically a place to hang out. For this project, I’ve created a very simple container with an Outlook Bar to group and show loaded apps and an area to display the apps interface.

Loading Apps

When the suite container starts, it needs to look for any applications to load. One way to do this is to have it search a given path for any DLLs and attempt to load them. The obvious problem with this approach is that the path may contain support DLLs that can’t, or should not, be loaded. A way to get around this is to use custom attributes to indicate that the class, or classes, within the DLL are meant to be part of the application suite.

C#
public static Hashtable FindApps()
{
    // Create hashtable to fill in
    Hashtable hashAssemblies = new Hashtable();

    // Get the current application path
    string strPath = Path.GetDirectoryName(ExecutablePath);
    
    // Iterate through all dll's in this path
    DirectoryInfo di = new DirectoryInfo(strPath);
    foreach(FileInfo file in di.GetFiles("*.dll") )
    {
        // Load the assembly so we can query for info about it.
        Assembly asm = Assembly.LoadFile(file.FullName);
        
        // Iterate through each module in this assembly
        foreach(Module mod in asm.GetModules() )
        {
            // Iterate through the types in this module
            foreach( Type t in mod.GetTypes() )
            {        
              // Check for the custom attribute 
              // and get the group and name
              object[] attributes = 
                t.GetCustomAttributes(typeof(SuiteAppAttrib.SuiteAppAttribute), 
                                                                         true);
              if( attributes.Length == 1 )
              {
                string strName = 
                  ((SuiteAppAttrib.SuiteAppAttribute)attributes[0]).Name;
                string strGroup = 
                  ((SuiteAppAttrib.SuiteAppAttribute)attributes[0]).Group;
                
                // Create a new app instance and add it to the list
                SuiteApp app = new SuiteApp(t.Name, 
                          file.FullName, strName, strGroup);
                    
                // Make sure the names sin't already being used
                if( hashAssemblies.ContainsKey(t.Name) )
                  throw new Exception("Name already in use.");

                hashAssemblies.Add(t.Name, app);
              }
            }
        }
    }

    return hashAssemblies;
}

As you can see from this code, we are searching the application path for any DLLs, then loading them and checking for the presence of our custom attribute.

C#
t.GetCustomAttributes(typeof(SuiteAppAttribute), true);

If found, we then create an instance of the SuiteApp helper class and place it in a Hashtable with the apps name as the key. This will come into play later when we need to look up the app for activation. It does place the limit of not allowing duplicate application names, but to avoid confusion on the user end, it is a good thing.

Using Attributes

Attributes are a means to convey information about an assembly, class, method, etc. Much more information about them can be found here, C# Programmer's Reference Attributes Tutorial. In the case of this project, a custom attribute is created and used to give two pieces of information necessary for the suite to load any app it finds. First, just by querying for the mere presence of the attribute, we can tell that the DLL should be loaded. The second part of information we get from the attribute is the name of the Outlook Bar group it should be added to and the name to be shown for the application.

C#
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class SuiteAppAttribute : Attribute
{
    private string m_strName;
    private string m_strGroup;

    /// <SUMMARY>
    /// Ctor
    /// </SUMMARY>
    /// <PARAM name="strName">App name</PARAM>
    /// <PARAM name="strGroup">Group name</PARAM>
    public SuiteAppAttribute(string strName, string strGroup)
    {
        m_strName = strName;
        m_strGroup = strGroup;
    }

    /// <SUMMARY>
    /// Name of application
    /// </SUMMARY>
    public string Name
    {
        get{ return m_strName; }
    }

    /// <SUMMARY>
    /// Name of group to which the app
    /// should be assigned
    /// </SUMMARY>
    public string Group
    {
        get{ return m_strGroup; }
    }
}

The first thing to note here is the attribute applied to this custom attribute.

C#
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]

This tells the runtime that this particular custom attribute can only be applied to a class and further can only be applied once.

Creating supported apps

To create applications to become part of our suite, we need to start with the Build event. To facilitate debugging, the application(s) must be moved to a location where the suite container can detect and load them. This step can be automated by adding a post-build step.

Build Event Dialog

copy $(TargetDir)$(TargetFileName) $(SolutionDir)SuiteAppContainer\bin\debug

Provided that everything is in the same solution, this will copy the output of the application's compile step to the debug folder of the suite container.

Activating Apps

To activate the application once the icon has been selected, we first make a check that it does indeed exist in the HashTable, if not there are some real problems. We also need to make sure the form has not already been created. Once these checks are verified, the path to the assembly is located and loaded. The InvokeMember function is used to create an instance of the form in question. We set a handler for the form closing event, so it can be handled as we’ll see later.

C#
public void OnSelectApp(object sender, EventArgs e)
{
OutlookBar.PanelIcon panel = ((Control)sender).Tag as 
OutlookBar.PanelIcon;
    
    // Get the item clicked
    string strItem = panel.AppName;

    // Make sure the app is in the list
    if( m_hashApps.ContainsKey(strItem) )
    {
        // If the windows hasn't already been created do it now
        if( ((SuiteApp)m_hashApps[strItem]).Form == null )
        {
            // Load the assembly
            SuiteApp app = (SuiteApp)m_hashApps[strItem];
            Assembly asm = Assembly.LoadFile(app.Path);
            Type[] types = asm.GetTypes();

            // Create the application instance
            Form frm = (Form)Activator.CreateInstance(types[0]);
            
            // Set the parameters and show
            frm.MdiParent = this;
            frm.Show();
            // Set the form closing event so we can handle it
            frm.Closing += new 
            CancelEventHandler(ChildFormClosing);

            // Save the form for later use
            ((SuiteApp)m_hashApps[strItem]).Form = frm;

            // We're done for now
            return;
        }
        else
        {
            // Form exists so we just need to activate it
            ((SuiteApp)m_hashApps[strItem]).Form.Activate();
        }
    }
    else
        throw new Exception("Application not found");
}

If the form already exists then we just want to activate it, bring it to the front.

C#
((SuiteApp)m_hashApps[strItem]).Form.Activate();

Form Closing

We need to be able to capture when the child forms close so that the resources can be freed and when it is required again the form will be recreated rather than attempting to activate a form that has already been closed.

C#
private void ChildFormClosing(object sender, CancelEventArgs e)
{
    string strName = ((Form)sender).Text;

    // If the app is in the list then null it
    if( m_hashApps.ContainsKey(strName) )
        ((SuiteApp)m_hashApps[strName]).Form = null;
}

Menus

The next area to address are the menus. The main container app has a basic menu structure and each application it houses will have its own menus, some with the same items.

Sample screenshot

Combining menus isn’t terribly difficult; it just takes some attention to detail and planning. The menu properties MergeType and MergeOrder are used to sort out how the menus are merged and where the items appear. The default settings are MergeType = Add and MergeOrder = 0. In the case of this example, we want to merge the File menu of the container app with the File menu from the child app. To start with, we need to set the MergeType of the main window file menu to MergeItems. The File menu in the child app must also be set to MergeItems.

Sample screenshot

This gets a little closer but as can be seen, we have duplicates of some items. I’ve renamed the child’s exit menu to better illustrate the problem. To correct this, we need to change the main window’s exit menu item to MergeType = Replace.

Sample screenshot

Now, we have the expected results. The next step is to set the MenuOrder. This doesn’t affect the merging of the menus but does affect where the items appear. Reviewing the sample project, we can see that the exit menu items have a MergeOrder of 99. Merging starts at 0 with higher numbered items being merged lower in the menu.

Sample screenshot

By setting the MergeOrder to 0, we see that the exit menu item is placed higher in the file menu.

Conclusion

This is certainly not an earth shattering killer application. What I hope is, it is a learning tool to explorer capabilities and present the seeds for thought, possibly some of the techniques can be incorporated into other applications.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)



Comments and Discussions

 
GeneralVisual Studio 2005 Pin
simplicitylabs25-Apr-07 19:51
simplicitylabs25-Apr-07 19:51 
GeneralRe: Visual Studio 2005 Pin
Not Active26-Apr-07 10:51
mentorNot Active26-Apr-07 10:51 
GeneralRe: Visual Studio 2005 Pin
simplicitylabs26-Apr-07 12:16
simplicitylabs26-Apr-07 12:16 
GeneralRe: Visual Studio 2005 Pin
Not Active26-Apr-07 15:31
mentorNot Active26-Apr-07 15:31 
GeneralRe: Visual Studio 2005 Pin
simplicitylabs26-Apr-07 15:50
simplicitylabs26-Apr-07 15:50 
GeneralReference Paths Pin
painlessprod16-Jun-05 7:11
painlessprod16-Jun-05 7:11 
GeneralRe: Reference Paths Pin
Not Active16-Jun-05 8:52
mentorNot Active16-Jun-05 8:52 
GeneralRe: Reference Paths Pin
painlessprod16-Jun-05 8:58
painlessprod16-Jun-05 8:58 
GeneralRe: Reference Paths Pin
painlessprod16-Jun-05 9:04
painlessprod16-Jun-05 9:04 
GeneralAssembly.LoadFile Pin
painlessprod16-Jun-05 7:08
painlessprod16-Jun-05 7:08 
GeneralInteresting Pin
Väinölä Harri5-Feb-05 5:26
Väinölä Harri5-Feb-05 5:26 
Generalquite good Pin
surgeproof19-Mar-04 5:22
surgeproof19-Mar-04 5:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.