Click here to Skip to main content
12,304,761 members (63,668 online)
Click here to Skip to main content
Add your own
alternative version


181 bookmarked

Building an Extensible Application with MEF, WPF, and MVVM

, 15 Nov 2009 LGPL3
Rate this:
Please Sign up or sign in to vote.
An article for anyone interested in how to build an extensible application using WPF and the Model-View-ViewModel pattern.


This article explains how to write a WPF application using the Model-View-ViewModel pattern and make it extensible so you (or third parties) can extend it with additional features.


Earlier this year, I set out to write an IDE-like application for the home automation space. I had three requirements:

  1. It had to be extensible by third parties
  2. It had to be WPF using the Model-View-ViewModel pattern (mostly because I wanted to learn it)
  3. I was going to release it under the GPL, so all the code had to be GPL compatible

When I found the SharpDevelop Core, I was pretty excited. This covered points 1 and 3 above, but only the latest version (4) was written in WPF, and it didn't use the MVVM pattern. I was disappointed, but I spent many long nights digging through the SharpDevelop code to understand how the extensibility part worked. I can't stress enough how much I was influenced by this excellent project, and I highly recommend the book Dissecting a C# Application: Inside SharpDevelop which explains how the SharpDevelop team wrote that application.

Tearing SharpDevelop down to the basics was still an option, but I wanted to be thorough. I went looking for other extensibility frameworks. I found System.Addin in the .NET Framework. This is a very solid framework for extensibility, but it's also quite complicated. It has a "7 stage pipeline" for every extensibility point, just to give you an idea. If I were on a team writing some huge enterprise application, like SAP, then I'd want us to use something like System.Addin.

Then, I stumbled on Mono.Addins and the Managed Extensibility Framework (MEF), at roughly the same time. I really liked both, but I ultimately decided to go with MEF for two reasons:

  1. MEF is going to be part of .NET 4.0, and when possible, I prefer to build on existing system libraries as much as possible.
  2. In MEF, you can do everything with attributes in your code, but in Mono.Addins, some features are only available using manifest files.

SoapBox Core

I took everything you would need to build your own extensible IDE-like application, and I put it in a framework that I named SoapBox Core (SoapBox is just a nod to "free as in speech, not as in beer"). It has been open sourced under the LGPL license, so you can use it in proprietary applications. The framework consists of these components:

  • Host: Bootstraps your application, loads all extensions, and displays the main window.
  • Logging: Includes a wrapper for the popular NLog logging framework (or you can swap it out with your preferred logging library).
  • Workbench: Provides a main application window with an extensible main menu, extensible tool bar tray, and extensible status bar.
  • Layout: Provides an "IDE-like" layout manager (a wrapper around AvalonDock) that extends the Workbench to provide tabbed document windows, and dockable (or floating) tool windows.
  • Options: Extends the Workbench with an extensible Options dialog so all of your application extensions can provide a single place for settings and configuration.
  • Arena: A built-in 2D physics simulator (a wrapper around Physics2D.Net) that lets you build a 2D environment of dynamic objects that follow rules like gravity, mass, velocity, and collisions.

When you run SoapBox Core by itself, with no extensions, you end up with something that looks like this:

SoapBox Core Screenshot

Boring? Yes. But it's a fully functioning application, and what you don't see is a Tool Bar Tray, a Status Bar, and a Menu, just waiting for you to hook in your extensions.

Building an Application on SoapBox Core

Everyone uses a text editor as a demo application. I figured since I had the 2D Physics engine in there anyway, why not make something a little more dynamic? That's why the demo application is a simple Pin Ball game. First, I'll explain how the Pin Ball demo is written as an extension to SoapBox Core, and then I'll show you how I extended the Pin Ball demo with a "High Scores" add-in.

Creating SoapBox.Demo.PinBall as an Add-In

I recommend using the following folder structure. If you download the source code, this is how it's structured:

  • AvalonDock
    • (AvalonDock project goes in here)
  • NLog
    • (NLog project goes in here)
  • Physics2D
    • (Physics2D projects go in here)
  • References
    • (DLLs like the MEF library go in here)
  • SoapBox
    • SoapBox.Core
      • (all SoapBox.Core projects go in here, each in their own folder)
  • YourNamespace
    • YourSubNamespace1
      • (your projects go in here, each in their own folder)
    • YourSubNamespace2
      • (or in here)
  • bin
    • (Both SoapBox.Core and your projects will all compile into here, so they can find each other)

Here are the steps for creating a new Visual Studio Solution and a project for your new SoapBox Core Add-In:

  1. Create a Visual Studio Solution for whatever it is you're building in the root directory. (The demo is SoapBox.Demo.sln.)
  2. You will need to include all of the SoapBox.Core projects in this solution. I suggest putting them in a SoapBox\SoapBox.Core solution folder. Make sure SoapBox.Core.Host is the startup project.
  3. SoapBox Core will have project references for AvalonDock, NLog, and Physics2D, so you will need to include these in your solution as well.
  4. I suggest creating solution folders for your own projects. First, create a top level one for YourNamespace, then sub folders for each of YourSubNamespaces.
  5. Create a new WPF User Control project, give it a name like YourNameSpace.YourSubNamespace.AddInName. In the location box, specify the \YourNameSpace\YourSubNamespace directory. This will create a new folder under that directory and place your new project in there. (The demo has a project called SoapBox.Demo.PinBall.)
  6. Your new project will have an automatically created user control called UserControl1. You can delete this.
  7. Add a reference to \References\System.ComponentModel.Composition.dll.
  8. Edit your project properties for this new project and go to the Build tab. Change Output path to ..\..\..\bin\ so that the DLL will be deposited in the bin directory with the Host executable.
  9. Add a project reference for this new project to SoapBox.Core.Contracts. This gives you access to interfaces and helper classes you will need to add menu items, tool bars and tool bar items, status bars, options pads, documents, and tool pads to the workbench, along with grabbing references to the logging component.
  10. If you want to build something based on the Arena module (the 2D simulator), you will also need a project reference to SoapBox.Core.Arena, but this is optional. (The demo uses this module.)
  11. When you build the project, you should see your new Add-In DLL in the \bin directory. When SoapBox.Core.Host.exe runs, it will scan that DLL for exports that extend the SoapBox.Core library modules. Of course, we haven't written any yet, so it won't do anything...

Documents and Pads

SoapBox Core works like an IDE, so you have "documents" (typically editable things that sit in the middle of your window) and "pads" (which are basically tool windows that you can leave free-floating, or dock them to a side of the Workbench). A document is anything that implements the SoapBox.Core.IDocument interface, and a pad is anything that implements the SoapBox.Core.IPad interface. In MVVM terminology, document and pad objects are both "ViewModels". You also define a View in XAML that tells WPF how you want to render your document or pad when it comes across them in the visual tree. SoapBox Core offers some helper classes you can derive from, that already implement these interfaces: SoapBox.Core.AbstractDocument and SoapBox.Core.AbstractPad.

It just so happens that the Arena (2D Physics Engine) module defines an AbstractArena class that already implements IDocument for us. In fact, the AbstractArena class already has a DataTemplate defined for it that gives us a basic View. The DataTemplate renders the Arena as a Canvas, and renders objects in the 2D Physics engine as PathGeometry UI elements at the appropriate position on the Canvas (based on their position in "space" calculated in the Physics engine). So, defining our first document is as simple as inheriting from AbstractArena and using the MEF Export attribute to tell the Workbench that we exist:

using SoapBox.Core;
using System.ComponentModel.Composition;

namespace SoapBox.Demo.PinBall
    [Export(SoapBox.Core.ExtensionPoints.Workbench.Documents, typeof(IDocument))]
    [Export(CompositionPoints.PinBall.PinBallTable, typeof(PinBallTable))]
    [Document(Name = PinBallTable.DOCUMENT_NAME)]
    public class PinBallTable : AbstractArena, IPartImportsSatisfiedNotification
        public const string DOCUMENT_NAME = "PinBallTable";

        public PinBallTable()
            // IDocument properties
            Name = DOCUMENT_NAME;
            Title = Resources.Strings.Arena_PinBallTable_Title;

            Gravity = new ArenaVector(0.0f, -800.0f);
            Scale = 0.5f; // screen elements per physics unit

            // Create 3 pinballs
            PinBalls.Add(new PinBall(this, new Point(-100f, 0)));
            PinBalls.Add(new PinBall(this, new Point(0, 0)));
            PinBalls.Add(new PinBall(this, new Point(100f, 0)));
            foreach (PinBall ball in PinBalls)

            // Lots of other stuff removed here...

        [Import(SoapBox.Core.Services.Logging.LoggingService, typeof(ILoggingService))]
        private ILoggingService logger { get; set; }

        [Import(SoapBox.Core.Services.Host.ExtensionService, typeof(IExtensionService))]
        private IExtensionService extensionService { get; set; }

			typeof(IExecutableCommand), AllowRecomposition=true)]
        private IEnumerable<IExecutableCommand> gameOverCommands { get; set; }

        private IList<IExecutableCommand> m_gameOverCommands = null;

        public void OnImportsSatisfied()
            m_gameOverCommands = extensionService.Sort(gameOverCommands);

        // Lots of other stuff removed here...

        public Collection<PinBall> PinBalls
                return m_PinBalls;
        private readonly Collection<PinBall> m_PinBalls =
                new Collection<PinBall>();

So, what's happening here? First, we're using MEF to export ourselves as an IDocument type, specifically for the contract name SoapBox.Core.ExtensionPoints.Workbench.Documents. When the Host starts, it begins by looking for a Window that exports itself as the contract SoapBox.Core.CompositionPoints.App.MainWindow. In our case, that's the Workbench. The Workbench constructor imports a list of documents. That means, this class will be instantiated and passed to a property of Workbench when the application runs. This process in MEF is called Composition.

We're actually exporting this class as a PinBallTable under a different contract as well. That's so that other parts of the application can find this specific extension object, rather than just the collection of documents as a whole.

During the composition, this class actually imports objects exported by other parts. Therefore, we have some properties defined with the Import attribute. In our case, we want a reference to the logging service, so one of the imports is [Import(SoapBox.Core.Services.Logging.LoggingService, typeof(ILoggingService))]. Now, we can log debug, error, and trace information.

As I mentioned earlier, we also want our pin ball game to be extensible. We can offer many different extension points in our project. In this case, we're importing a list of IExecutableCommand objects that we are going to execute when the game is over (that's how the High Scores add-in will hook in). However, when these commands are imported, they will be in the order that MEF found them during the compose. SoapBox Core offers an Extension Service that can sort the extensions into an order you prefer. To grab a reference to the Extension Service, we use this attribute: [Import(SoapBox.Core.Services.Host.ExtensionService, typeof(IExtensionService))]. Then, our class implements IPartImportsSatisfiedNotification, specifically OnImportsSatisfied() to sort the extensions: m_gameOverCommands = extensionService.Sort(gameOverCommands);.

In case you are wondering how it sorts them, I borrowed an idea from SharpDevelop. All extensions have to implement SoapBox.Core.IExtension:

namespace SoapBox.Core
    public enum RelativeDirection
        Before = 1,

    public interface IExtension : IViewModel
        string ID { get; }
        string InsertRelativeToID { get; }
        RelativeDirection BeforeOrAfter { get; }

There is a handy SoapBox.Core.AbstractExtension which implements this interface for you, and if you inherit from that, you can just set these properties in the constructor of your extension. You don't have to set these properties if you don't care about the sort order. To use these, imagine there are three extensions being imported (possibly from separate DLLs):

  • ID = "a"
  • ID = "b", InsertRelativeToID = "a", BeforeOrAfter = After
  • ID = "c", InsertRelativeToID = "b", BeforeOrAfter = Before

In this case, ExtensionService.Sort() will sort them in the order a, c, b. This applies to all extensions including menu item, tool bar, status bar, and options dialog extensions, so if you wanted to insert a new Main Menu item between the View and the Tools menus, you could.

The last thing the PinBallTable class does is add some PinBall objects to the Arena (2D Physics Engine). As you can see, this is done by calling the AddArenaBody method. You can use this to add any object that implements SoapBox.Core.Arena.IArenaBody. Here's what PinBall looks like:

namespace SoapBox.Demo.PinBall
    public class PinBall : AbstractArenaDynamicBody
        public const float PIN_BALL_RADIUS = 20.0f;

        public PinBall(PinBallTable table, Point startingPoint)
            Mass = 1.8f;
            Friction = 0.0001f; // Between 0 and 1
            Restitution = 0.5f; // Energy retained after a collision (0 to 1)

            m_table = table;
            InitialX = (float)startingPoint.X;
            InitialY = (float)startingPoint.Y;

            Sprite = new PinBallSprite();

        private PinBallTable m_table = null;

        // There's a bit more here, but I deleted it for simplicity

The PinBall class only defines the physical properties of the object, but not the geometry. That is actually defined in a separate class called PinBallSprite:

namespace SoapBox.Demo.PinBall
    public class PinBallSprite : AbstractSprite
        public PinBallSprite()
            Geometry = new EllipseGeometry(new Point(0, 0),
                       PinBall.PIN_BALL_RADIUS, PinBall.PIN_BALL_RADIUS);

Note: It looks like you can use any Geometry class here, but you can't. You have to stick to simple shapes like ellipses, rectangles, etc., or you can use a PathFigure, but in that case, you have to define the points of the figure in a counter-clockwise direction.

So we haven't defined what the PinBall looks like. Actually, you don't have to! If you don't define a View for this ViewModel, SoapBox Core provides a default View that renders all AbstractSprite objects as solid filled black objects based on the given Geometry. This is very handy for figuring out the physics of the Pin Ball game before worrying about what it looks like. Of course, eventually, you want the ball to look like a ball, so we actually use a DataTemplate for this. Here's the View for the PinBall:

<ResourceDictionary xmlns=""

    <!-- Make it a shiny ball -->
    <DataTemplate DataType="{x:Type local:PinBallSprite}">
        <Path Stroke="Black"
                    Data="{Binding Path=Geometry}">
                <RadialGradientBrush GradientOrigin="0.33,0.33">
                    <GradientStop Offset="0" Color="White"/>
                    <GradientStop Offset="1" Color="Black"/>

    <!-- Orient the ball within the arena (doesn't rotate because it's a sphere and
            we want the shiny spot in the same spot all the time) -->
    <DataTemplate DataType="{x:Type local:PinBall}">
        <ContentControl Content="{Binding Path=(arena:IArenaBody.Sprite)}">
                    <!-- scale it, including inverting the Y axis -->
                    <ScaleTransform ScaleX="{Binding State.Scale}"
                           ScaleY="{Binding State.Scale}"/>
                    <!-- offset it by it's physical position -->
                    <TranslateTransform X="{Binding State.ScreenX}"
                           Y="{Binding State.ScreenY}" />

Normally, you only need the first DataTemplate (for the sprite), and the default DataTemplate for AbstractArenaBody takes care of scaling, translating, and rotating the body based on its position in the Arena. However, in this case, we're defining a ball with a shiny spot on it, and we don't want the shiny spot to rotate when the ball rotates, so we are overriding the default AbstractArenaBody View with one specifically for PinBall. This is identical to the default, but it doesn't have the rotation transform (we're taking advantage of the fact that balls are... well, round).

Applying Views to ViewModels

So, we just defined a couple of DataTemplates in a ResourceDictionary. Normally, we would need to include a reference to this ResourceDictionary in a merged application dictionary. But we can't do that if the Host doesn't know anything about the extensions beforehand. That's where MEF comes to the rescue. The Host imports a collection of ResourceDictionary extensions on startup, and manually inserts them into the application resources:

	typeof(ResourceDictionary), AllowRecomposition=true)]
private IEnumerable<ResourceDictionary> Styles { get; set; }

	typeof(ResourceDictionary), AllowRecomposition=true)]
private IEnumerable<ResourceDictionary> Views { get; set; }

// Later in the OnImportsSatisfied method...

// Add the imported resource dictionaries
// to the application resources
foreach (ResourceDictionary r in Styles)
foreach (ResourceDictionary r in Views)

... but how do we "export" the PinBallView ResourceDictionary so that the Host will find it? It turns out that you can manually add a code-behind to a ResourceDictionary. Just add a .cs file with the same name as your .xaml file, but with a .cs extension. For instance, the ResourceDictionary for the PinBallView is PinBallView.xaml. Here's the contents of PinBallView.xaml.cs:

namespace SoapBox.Demo.PinBall
    [Export(SoapBox.Core.ExtensionPoints.Host.Views, typeof(ResourceDictionary))]
    public partial class PinBallView : ResourceDictionary
        public PinBallView()


This pattern is repeated over and over again in both SoapBox Core, and the Pin Ball Demo. This is how you apply a View to documents, pads, option dialog pads, or any other ViewModel.

Also notice that Styles and Views have the AllowRecomposition parameter to the attribute set to true. This means these properties support recomposition. New extensions can be discovered after the application has started, the extensions can be added to the part catalog and a recompose will occur. That means MEF will set these properties again, and will call OnImportsSatisfied. Currently SoapBox Core doesn't have a mechanism to add new extensions during execution, but it will eventually.

Everything's a ViewModel

In SoapBox Core, (nearly) every class is a ViewModel. You may notice that I haven't used the standard ViewModel suffix for all the ViewModel classes. I found this was getting far too verbose. Instead, anything that implements SoapBox.Core.IViewModel is considered a ViewModel. IViewModel is just a proxy for INotifyPropertyChanged.

Showing the Document

Now that we've done all this work creating a document to show, something needs to tell the LayoutManager to show it. The simplest method is just to show it on startup. The Host imports a list of IExecutableCommand extensions that can execute when the application starts, and we can hook into that to show our document:

namespace SoapBox.Demo.PinBall
    class StartupCommand : AbstractExtension, IExecutableCommand

        [Import(SoapBox.Core.CompositionPoints.Host.MainWindow, typeof(Window))]
        private Lazy<Window> mainWindow { get; set; }

        [Import(SoapBox.Core.Services.Layout.LayoutManager, typeof(ILayoutManager))]
        private Lazy<ILayoutManager> layoutManager { get; set; }

        [Import(CompositionPoints.PinBall.PinBallTable, typeof(PinBallTable))]
        private Lazy<PinBallTable> pinBallTable { get; set; }

        public void Run(params object[] args)
            // Customize the Workbench title while we're here
            mainWindow.Value.Title = Resources.Strings.Workbench_Title;
            // Show the Pin Ball Table

Notice the use of "lazy" imports here. In this case, the imported object isn't actually instantiated until the .Value property is accessed. Since documents and pads can be expensive to instantiate, all imports of these objects are done with lazy imports to avoid instantiating them until they're actually needed.

Now if you run the application, the PinBallTable will be displayed.

Extending the Menu

Of course, the user could just close the document, and they would have no way to display it again without restarting the application. To be consistent with other Microsoft Windows applications, we should probably add an item to the View menu to let the user display the Pin Ball Table themselves:

namespace SoapBox.Demo.PinBall
    /// <summary>
    /// Add a menu item to the view menu to launch the PinBallTable "document"
    /// </summary>
    [Export(SoapBox.Core.ExtensionPoints.Workbench.MainMenu.ViewMenu, typeof(IMenuItem))]
    class ViewMenuPinBallTable : AbstractMenuItem
        public ViewMenuPinBallTable()
            ID = "PinBallTable";
            InsertRelativeToID = "ToolBars";
            BeforeOrAfter = RelativeDirection.Before;
            Header = Resources.Strings.Workbench_MainMenu_View_PinBallTable;

        [Import(SoapBox.Core.Services.Layout.LayoutManager, typeof(ILayoutManager))]
        private Lazy<ILayoutManager> layoutManager { get; set; }

        private Lazy<PinBallTable> table { get; set; }

        protected override void Run()

As you can see, we just need to inherit from AbstractMenuItem and export the class as an IMenuItem for the contract SoapBox.Core.ExtensionPoints.Workbench.MainMenu.ViewMenu. You may have noticed that we're inserting this menu item before another item called "ToolBars". SoapBox Core actually defines a View menu item called ToolBars that displays all toolbar extensions in the system (and actually lets the user enable or disable individual tool bars). Since there are no tool bars defined right now, you can't see it.

Extending the Status Bar

Extending the Status Bar works the same way, except that there are many different types of controls that can go in the Status Bar, like labels, buttons, radiobuttons, separators, etc. There is a different abstract class defined for each one. Here's how you define a label in the status bar:

[Export(SoapBox.Core.ExtensionPoints.Workbench.StatusBar, typeof(IStatusBarItem))]
public class MyLabel : AbstractStatusBarLabel
    public MyLabel()
        ID = "MyLabel";
        Text = Resources.Strings.Workbench_StatusBar_MyLabel;

Adding a Toolbar

A toolbar itself has to import toolbar items to display. Here's how to create a toolbar:

[Export(SoapBox.Core.ExtensionPoints.Workbench.ToolBars, typeof(IToolBar))]
public class MyToolBar : AbstractToolBar, IPartImportsSatisfiedNotification
    public MyToolBar()
        Name = Resources.Strings.MyToolBar_Name;
        Visible = true; // default to visible

    [Import(SoapBox.Core.Services.Host.ExtensionService, typeof(IExtensionService))]
    private IExtensionService extensionService { get; set; }

		typeof(IToolBarItem), AllowRecomposition=true)]
    private IEnumerable<IToolBarItem> items { get; set; }

    public void OnImportsSatisfied()
        Items = extensionService.Sort(items);


That will add a toolbar to the View->ToolBars menu, and let the user control the visibility of it with a checkable menu item. However, you then need to add items to the toolbar, like this button:

[Export(ExtensionPoints.Workbench.ToolBars.MyToolBar, typeof(IToolBarItem))]
public class MyToolBarButton : AbstractToolBarButton
    public MyToolBarButton()
        ID = "MyToolBarButton";
        ToolTip = Resources.Strings.MyToolBarButton_Tooltip;

    protected override void Run()
        // Whatever you want to happen when the user clicks the button

Extending the Extension

So I finished the pin ball game, and it kept score and had levels, but when the game was over, it just started a new game. I figured it would be neat to create an add-in that extended the pin ball game and kept track of high scores.

I followed the same procedure as before to create an entirely separate project called SoapBox.Demo.HighScores. I created a new Pad called HighScores that inherits from SoapBox.Core.AbstractPad. It takes care of loading, displaying, and saving the high scores. Then, I had to write an IExecutableCommand extension that hooked into the GameOverCommands extensibility point on the pin ball game:

namespace SoapBox.Demo.HighScores
    /// <summary>
    /// This extends the basic pinball table by saving the score and
    /// level attained to a log when the game is over.
    /// </summary>
    class GameOverCommand : AbstractExtension, IExecutableCommand
        [Import(CompositionPoints.Workbench.Pads.HighScores, typeof(HighScores))]
        private Lazy<HighScores> highScores { get; set; }

        /// <summary>
        /// Registers the high scores with the HighScores ViewModel
        /// </summary>
        /// <param name="args"></param>
        public void Run(params object[] args)
            // arg 0 = PinBallTable
            if (args.Length >= 1)
                PinBallTable table = args[0] as PinBallTable;
                if (table != null)
                                       table.Score, table.Level);

I also added another menu item to the View menu to display the HighScores pad. That's all there is to it.

Latest Version of SoapBox Core

Points of Interest

  • Check out WorkBenchView.xaml in the SoapBox.Core.Workbench project to see how to implement a WPF menu with the MVVM pattern, including menu separators.
  • SoapBox.Core.Contracts has a helper static class called NotifyPropertyChangedHelper that lets you implement INotifyPropertyChanged without using hard coded strings for property names.
  • Everything that implements SoapBox.Core.IControl (which is pretty much every menu item, status bar item, and tool bar item) has a VisibleCondition property. This can be set to anything that implements SoapBox.Core.ICondition, but I recommend using SoapBox.Core.ConcreteCondition for this. A "condition" is just an abstraction of a boolean condition that one part can export and other parts can import.
  • Anything that inherits from AbstractCommandControl (buttons, usually) has an EnableCondition property that controls if the button is enabled. If it's not enabled, it automatically changes the icon to gray scale.
  • Take a look at PinBallOptionsItem and PinBallOptionsPad to see how to extend the Options dialog and store your editable options in the user settings.


  • November 7, 2009: Article published (based on SoapBox Core v2009.11.04)
  • November 12, 2009: Article modified (based on SoapBox Core v2009.11.11)


This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


About the Author

Scott Whitlock
Canada Canada
By day I'm a Professional Engineer, doing .NET, VB6, SQL Server, and Automation (Ladder Logic, etc.) programming.

On weekends I write and maintain an open source extensible application framework called SoapBox Core.

In the evenings I provide front line technical support for and I help out with administrative tasks (like formatting stuff). I also pitch in as a moderator from time to time.

You can follow me on twitter.

You may also be interested in...

Comments and Discussions

GeneralMy vote of 5 Pin
ilsath24-Feb-11 4:41
memberilsath24-Feb-11 4:41 

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.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.160530.1 | Last Updated 15 Nov 2009
Article Copyright 2009 by Scott Whitlock
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid