Click here to Skip to main content
15,881,281 members
Articles / Programming Languages / C#

Building Snap-In Applications

Rate me:
Please Sign up or sign in to vote.
4.89/5 (46 votes)
23 Aug 2003CPOL17 min read 105.2K   3.1K   141   6
This article details how to build a Snap-In-Capable application, similar to the way that MMC works.

Introduction

I was called upon several years ago to create an application framework that would be flexible and open-ended: I knew at delivery it would have to support Functions A, B and C, but next year, I knew it would also have to do Function X, where X could be just about anything relating to the project. I could see that Microsoft had already come up with the solution: the Microsoft Management Console. I used MMC with its Snap-Ins to manage SQL Server, IIS, P&M Server, etc., and obviously Microsoft hadn't thought of every possible Snap-In at the time they released MMC 1.0. That's just what I needed: a thin shell program that exposed common GUI elements - ToolBars and Menus - and common components - TCP/IP sockets, data encryptors and user authenticators.

Believe it or not, with a little research, I found that a Snap-In application could be written in Visual Basic 6.0 without having to resort to hacking ODL into VB-friendly TLB's (I was a VB masochist) as I would, if I wanted to write an Explorer Shell Extension. Not long afterward, I had a working prototype, and a product release followed.

As you may have guessed, C# and VB.NET provide object oriented language features to create a Snap-In-type program out of the box and with comparatively little coding (and with no more binary compatibility issues!). It is my goal in the following discussion to explain from the ground up, the architecture and implementation of a Snap-In application using C#.

OO Review: Polymorphism

Before I explain how the solution works, you need to have a fair grasp on polymorphism (a word both as clunky and important as "orthogonality"). If you know all there is to know about polymorphism, feel free to skip ahead - this will be review and lead-in.

You're likely very familiar with (and can even explain to your grandmother) what programmatic inheritance is. But if you're like many programmers I've met, you probably mumble wishy-washy utterances about polymorphism as though it's the useless ugly duckling of OO languages. This misunderstanding between coders and polymorphism is unfortunate - I think it is the most powerful feature of OO programming and can result in the highest amount of code reuse.

I think the disconnect is because inheritance feels natural and has fairly obvious applications: say I've programmed an Animal, and a Cat derives from Animal, so it can do everything Animal does - Walk(), Eat() - simply by being (inheriting from) an Animal. Conceptually, polymorphism is probably not as plain to get a handle on.

Polymorphism is, in a way, the opposite of inheritance. Where inheritance walks down a tree from the general to the specific, polymorphism traverses back up from the specific to the general. Say I have a function like Move(Animal a) whose implementation will simply call a.Walk() on whatever animal is passed in. It's polymorphism that allows me to pass the Animal-derived Cat whiskers like this: Move(whiskers). Instead of having to write specific functions Move(Dog d) and Move(Cat c), I can write the generic Move(Animal a). Powerful stuff! Think of polymorphism as turning your specific, derived class into its parent, abstract class on demand.

Great, so what good is that to me? Well, a Snap-In program will simply contain an arbitrary set of snap-in "modules" that will all have to respond to similar requests: Initialize() and Activate() for example. In turn, each module will expect certain things from the container "shell", such as CurrentUserId or SendData(). Compare this Shell with Modules to a Zookeeper with Animals:

 ZookeeperShell
Keeps a set of..Animal[]Module[]
..and can expect to be able to..feed the animalenable the module
..by calling an instance's interfaceanimal.Eat()module.Activate()
In turn, an instance knows it can get its needs met by calling the containingkeeper.DemandAffection()shell.UpdateStatusBar()

So then, as long as I've got objects that fulfill the contract of being an Animal or Module, my Zookeeper or Shell knows that they can interact with them in the same way, using the same method calls. This is a two-way communication though, so in turn, Animal objects know they can request to get petted by the Zookeeper, and Module objects can request a StatusBar change from the Shell. This is just how MMC Snap-Ins and Windows Explorer Shell Extensions are implemented, and hopefully this somewhat elided view of the mechanics will get you going if you want to start playing with PIDL's.

Solution architecture

Even if you skipped ahead to this section, as you can see in the table immediately above, the solution to this problem is obviously in defining and implementing interface contracts between a Shell program and a Module, using polymorphic method calls. OK, maybe that wasn't so obvious, but now you have something impressive to say to your boss.

Contract programming (I don't mean job contracts!) is a great way of making sure, programs do what they say they're going to do, because the rules enforcement is required by the language. My co-workers are in fact sick to death of hearing me say, "We should be programming to an interface". I suppose I'm like that guy with a hammer who sees nails everywhere. But then again, I've realized the rewards and am keen on ways of coordinating teams of miscreant programmers. Programming to an interface ensures at least some level of consistency between what two coders write.

C# provides the keyword, interface, that does just this one thing: define a class interface (contract) against which to program an implementation. By design, interfaces have no, and cannot have, implementation. So, if you create an object that inherits an interface, you must be sure you're ready to implement it - the compiler will require it of you.

Through interface implementation and the wonders of polymorphism, the shell I'm going to develop can keep an array of IModule objects, which it knows it can expect to be able to call, using _modules[i].Initialize(this) for any valid i. In turn, each module can keep as a member field the reference passed into it by the Initialize(IShell shell) call and make use of things like _shell.ToolBar.Buttons.Add(_myModuleButton). Note, the "I" prefix is a pretty standard convention for naming interfaces - it's not required, but it's strongly recommended.

The following is simpler than you will ultimately implement in your solution, but there's enough here to get you well underway - at least giving you the gist of the back-and-forth plumbing. In its entirety, Interfaces.cs from the source ZIP:

C#
using System;
using System.Drawing;
using System.Windows.Forms;

namespace SnapIn
{
    public interface IShell
    {
        ToolBar TheToolBar {get; set;}
        StatusBar TheStatusBar {get; set;}
    }

    public interface IModule
    {
        void Initialize(IShell shell);
        void Activate();
        void Deactivate();
        void Move(Point topLeft, Point bottomLeft);
    }
}

Notice that there is no implementation, nor can there be implementation, in these interface definitions. I've simply lain the groundwork for a shell and one or more modules to come.

Warning: There is a bad practice demonstrated here. Though I hesitate publishing questionable code, knowing I'm violating such things as good code encapsulation, I ask you to be forgiving and trust that I know better, but for now am more interested in getting you up and running without any extra confusing baggage (apart from my manner of writing). So, when you take over, please be sure to lock down the IShell interface to something more "shy" like SetStatusText(string text). You probably don't want to give some unknown modules a pointer to the StatusBar - they might add 942 Panels with hot-pink flashing text, and where would you be then? The Marketing Department, I think they call it.

Who's Talking to Whom

The one aspect of this solution that is not clear from the interface specification is, how a shell and modules startup and work together. Basically, the entry point to the application is the ubiquitous Main() which is also an implementer of IShell. In my program, this will be a C# project called Shell, which builds Shell.EXE. Modules are compiled to DLLs since they are going to be loaded into Shell's process. What's inside the DLL can reasonably be one of two things. For my program, I decided that each module would be a UserControl; the alternative is making each module a MDI child Form. Do note that your shell program should probably only try to support one or the other - trying to handle both is likely just to confuse your users, not to mention your programmers.

I'm not sure that there is one best choice of UserControl over MDI child Form - it mainly depends on the interaction you (and your users) expect between modules. I think MDI, with its tiled window display would indicate to the user that, it's possible to work in one module and "tab" over to (or even drag and drop into) another module. If this is the case for your application, converting the shell and the modules I wrote to use MDI is pretty simple. However, I've taken the approach that modules do not know about or assume the presence of another module, so there will be only one active UserControl loaded into the main application area of Shell at a given time.

Code walk-through

If you haven't already done it, be sure you grab all the code from the link above. I'm going to assume you have it and are able to look at it easily.

Interfaces

The first step in the solution is creating the interface specification. It's really quite easy since all you have to do is define how you want a shell and a module to communicate. You do not have to (and in fact you cannot) provide any implementation details. Interfaces.CSPROJ has the single Interfaces.CS, which you have seen above.

The most interesting thing in Interfaces.CS is IModule.Initialize(IShell shell). This is the secret to enabling the two-way communication between a shell and a module. Without it, Shell would be able to communicate with any module it loaded, but the module would not be aware of anything about the context in which it was loaded. Passing the this pointer into the module during initialization, gives modules a sense of their place in the world, providing them opportunities to give the user additional I/O beyond their client rectangle.

Otherwise, as you can see, IShell exposes two properties that allow access to an implementer's ToolBar and StatusBar. It's here that you may eventually want to extend the IShell's interface to allow modules to request additional data from memory, the network, or a database. If you're going the MDI route, you may also find it useful to expose methods to allow modules to request access to other modules. Perhaps you would add something like IModule GetModule(Type moduleType, int index), so modules can locate each other by Type and index, if more than one instance exists.

Important: Modules should be autonomous. If you find yourself wiring up much more than a dozen calls each between IShell and IModule, you might be making a mistake. The only thing modules need to run is a container: a Form or MDI parent. The shell is only there to provide an operating system process, perceptual unity and a common set of functionality to all modules. Further, all a module needs to expose in its interface are, high-level methods to notify it when it's been instantiated, brought to the foreground, dismissed, moved, etc.

Shell

The GUI on the Shell is bare-bones: a MainMenu, a ToolBar and a StatusBar. You're not limited to just these, but always be sure to ask yourself when you add GUI components to the Shell, if they will need to be there for all modules: TCP/IP socket, yes; ProgressBar, no. Very likely, you will have a minimum of implementation for handling events and so forth for module-available components. If you're going to transfer a file for a module, you'll want to encapsulate all the compression and threading logic into a SendFile(string path) interface. You may also want some default ToolBarButtons or MenuItems that are there for all modules to use. It's up to you and your program's requirements. Just remember the rule of thumb is, if only one module is going to use it, the code should probably go into the module; if many modules are going to use it, keep it clean and wrapped up for all modules via the IShell interface.

To tell C# that you're intending to implement the IShell interface, you'll first need to add a reference to the Interfaces.DLL (or Interfaces.CSPROJ if you haven't built it yet). Then, you declare your desire to inherit Form (assuming you don't want to reinvent the Window) and implement IShell as follows:

C#
public class FrmMain : System.Windows.Forms.Form, IShell
{
    ...
}

If I were to try to compile now with no implementation, I would get the build error CS0535: "'class' does not implement interface 'member'" for both of the missing properties. The compiler is doing its job, checking that I'm actually fulfilling the contract I promised. In this way, IModule implementers are guaranteed to be able to get dependable interactions with IShell implementers.

So, the one thing you're required to code then, is the interface contract. In C#, this is simply done by writing methods with the same name and signature as the interface specified. (For VB.NET programmers, you can actually call the method anything you like, because you have to use the Implements keyword to specify which implemented method meets the requirements of each interface method. Further, VS 2003 can automatically write out method stubs for you, after you write Implements IShell and press [Enter], which is pretty handy.) So my most basic Shell looks like this:

C#
public class Shell : System.Windows.Forms.Form, IShell
{
    private System.Windows.Forms.StatusBar SbrMain;
    private System.Windows.Forms.ToolBar TbrMain;

    [STAThread] static void Main()
    {
        Application.Run(new Shell());
    }

    public ToolBar TheToolBar
    {
        get
        {
            return this.TbrMain;
        }
        set
        {
            this.TbrMain = value;
        }
    }

    public StatusBar TheStatusBar
    {
        get
        {
            return this.SbrMain;
        }
        set
        {
            this.SbrMain = value;
        }
    }
}

Any module that wants to talk with a shell that implements IShell can always rely on the shell having the properties TheToolBar and TheStatusBar. I chose my words here carefully, though. IShell implementers must have those properties, but they don't necessarily have to have a ToolBar or StatusBar! They could return null or throw a NotImplementedException if they don't have these objects. All contract programming gets you is a guarantee that your call to MyShellReference.TheStatusBar won't fail with CS0117: "'type' does not contain a definition for 'function'". What the implementer does from there is wide open.

The details

The rest of Shell's implementation is academic, and I'm sure you can read it and figure out what's going on. So you know generally how I think it's best to approach this open-ended type of architecture, I'll just make a couple key points.

You want to load as little as possible when your shell starts up: perception is everything, and a slow app perceives badly. Since there will be an unknown number of modules, creating an instance of each when your shell loads could take a long time and annoy your users. Since you have to externalize the module configuration (you realized that by now of course), it's probably worthwhile considering how your App.config will be designed and what a module must provide to get loaded properly. Essentially each IModule implementer that wants to get loaded into your IShell implementer will need a node or set of nodes in the App.config (or home-brewed XML configurator), indicating such important things as where the assembly lives and what type(s) it contains.

For production, you will probably also want to add practical things like MenuItem text, accelerator keys and icon resource locations. But since I'm not putting this into production, I short-cut and simply loaded the paths and types from App.config and hard-coded their MenuItems (I have to leave you with some work, don't I)?

So the interesting bits that are left have to do with loading modules from disk and displaying them at appropriate times and places. There's no rocket science in ActivateModule, just some state management and a call to a method that uses the System.Activator. When the user clicks one of the MenuItems I want the appropriate module to load into the main display. Since I am using a simple array of IModules, I can track which module is active by index, and I check if the active module is the one the user just clicked to avoid re-Initializing or re-Activating an already-loaded module. Further, I can keep modules resident or destroy them with the releaseActive argument. If set to false, a module, once loaded, stays loaded, so if it's doing some background processing, it keeps processing even if another module has the foreground; otherwise, modules are null-ed and left on the curb for the GC. For sake of demonstration, I exposed a UI on this with the "Null Inactive Mods" ToolBarButton.

But let me reiterate - if you take nothing else from this segment - load your application as quickly as possible using a config file or other module metadata and load modules later on-demand. If you load a module from disk at the last possible moment - when the user clicks the associated MenuItem - users' waiting happens at a seemingly appropriate and staggered times.

Modules

Now the fun part! Modules are of course where your application actually does something, and there's no right way to program a module, since by our definition, modules largely unknown. Hopefully, though, you or your co-workers or your clients who will develop for your shell will at least keep with the theme of your application and not add a Joke-of-the-Day module to a serious accounting suite. This is a real risk though when you expose your interfaces to the world - so just bear it in mind.

Much like the beginnings of Shell, MyModule declares its intentions to implement the interface IModule. As mentioned earlier, modules for my shell are going to be UserControls so they inherit that class, but they could also just as reasonably have been Forms if I chose to implement Shell as an MDI parent. Following is my implementation for MyModule's rendition of an IModule.

C#
public class MyModule : System.Windows.Forms.UserControl, IModule
{
    private IShell _shell = null;

    public void Initialize(IShell shell)
    {
        _shell = shell;
        this.Visible = false;
        this.Parent = (Form)shell;
    }

    public void Activate()
    {
        AddShellButtons();
        _shell.TheToolBar.ButtonClick += new 
                        ToolBarButtonClickEventHandler(OnToolBarButtonClick);
        _shell.TheToolBar.MouseEnter += new EventHandler(OnToolBarMouseEnter);
        _shell.TheToolBar.MouseLeave += new EventHandler(OnToolBarMouseLeave);
        this.Visible = true;
    }

    public void Deactivate()
    {
        RemoveShellButtons();
        _shell.TheToolBar.ButtonClick -= new 
                         ToolBarButtonClickEventHandler(OnToolBarButtonClick);
        _shell.TheToolBar.MouseEnter -= new EventHandler(OnToolBarMouseEnter);
        _shell.TheToolBar.MouseLeave -= new EventHandler(OnToolBarMouseLeave);
        this.Visible = false;
    }

    public new void Move(Point topLeft, Point bottomLeft)
    {
        this.Top = topLeft.Y;
        this.Left = topLeft.X;
        this.Height = bottomLeft.Y - topLeft.Y;
        this.Width = bottomLeft.X - topLeft.X;
    }
}

The important things about which this module needs to be notified are its instantiation, its activation or enablement, the complementary deactivation or disablement, and a moving or resizing event. Your modules may need additional messages via IShell but those are for you to determine. As noted above though, I encourage you to try to keep the communication from IShell to IModule constrained to things that all IModule implementers will need or that all IModules have. I know from experience this isn't possible 100% of the time, but it's an admirable goal.

The rest of my modules is just fun and games, but it's where you are going to make your money. MyModule just writes some Shakespeare into a RichTextBox in a different color, depending on which button you click. YourModule starts an exponentially slowing ProgressBar - do not wait for it to finish! - that shows that background processing can go on in different modules (when the "Null Inactive Mods" is disabled). Remember that when you encapsulate the IShell appropriately, you will have to wire up events a little differently: consider exposing events in your IShell interface instead; this has been my preferred method.

Documentation

If it were possible to put some default implementation in an interface, a thing I would add is the code in Initialize(IShell shell). I'd like to be able to rely on all modules remembering that this shell is their parent and going invisible. By design, this isn't possible with interfaces, so, I strongly suggest leaving some documentation-type artifacts lying around near your code.

Moreover, some things may not be obvious to other developers coding for your shell... For example, it might not be readily obvious that the programmer is responsible for cleaning up after a module when it's being deactivated (though of course programmers will remember to throw buttons on the ToolBar when activated!). Sure, someone coding FunkyModule will eventually realize that the ToolBar is getting sort of cluttered with module-added buttons, but why not save developers the debug time and document it? Even less obvious is that modules might want to unsubscribe from the event message queue! And of course, just as a matter of courtesy, modules should hide themselves when deactivating. If you write this up as a short list of bulleted to-do items, you'll make a lot of people happier.

Summary

It's really not so hard once you know the trick - contract (interface) programming. As long as you can know ahead of time what things one object will want to say and do to another, you can put it into an interface definition and implement it as often as you like. Ultimately, there is no limit to what you can do with this model, and I've applied it at much lower levels than shells and modules as well. Hopefully, this will become a very powerful tool in your kit for future use.

Any questions, comments and brickbats may be E-mailed to todd.sprang@cox.net. Good luck and happy coding!

Change history

  • 08.12.03, 08.18.03 - First draft

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United States United States
Architect, designer and coder since the late 90's and still going strong. Cut my teeth on Perl and C++, but soon moved to VB6/ASP and then onto .NET bliss and here I remain (until something better comes along). Love talking and thinking about coding practices and techniques, so feel free to shoot me a line. Lately been getting stronger at SOA and WCF and messing around with jqGrid.

Comments and Discussions

 
GeneralToolBar Images Pin
Doug Wilson27-Jan-05 20:07
Doug Wilson27-Jan-05 20:07 
GeneralGreat article Pin
Anonymous3-Sep-03 17:17
Anonymous3-Sep-03 17:17 
GeneralGood one.. Pin
NetPointerIN26-Aug-03 15:12
NetPointerIN26-Aug-03 15:12 
GeneralWell Done Pin
Furty24-Aug-03 16:55
Furty24-Aug-03 16:55 
GeneralRe: Well Done Pin
tsprang25-Aug-03 8:53
tsprang25-Aug-03 8:53 
GeneralRe: Well Done Pin
tsprang16-Sep-03 6:49
tsprang16-Sep-03 6:49 

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.