Click here to Skip to main content
15,918,967 members
Articles / Desktop Programming / WPF

Using a Plugin-Based Application

Rate me:
Please Sign up or sign in to vote.
4.84/5 (44 votes)
14 Aug 2016Ms-PL21 min read 105.6K   155   54
An introduction into the Managed Service Extensibility Framework (MSEF)

There is a small Tutorial for this topic on YouTube (/watch?v=pvXi1lbLz-s)


Please read the article very carefully because some guys tried to use it and did not understand how it works. For example: in some cases you are forced to setup VS to recompile all DLL's on every build! that is imported because due the loose of Dependency between the start project and the Module containing DLL VS does not recognize that the DLL has changed so it would maybe not build it again. To enable this behavior go to TOOLS - PROJECTS AND SOLUTIONS -> BUILD AND RUN and set the option "On Run, When projects are out of date" To "Always Build". (Visual studio restart required)

Required knowledge

I expect you to know your language. You should be aware of class inheritance, some basic understanding of Inversion of Control and Dependency injection.


The Repository contains 4 Branches.

The Trunk:


  • Contracts.DLL

    • Shell Interfaces and Attributes host


    • The Framework itself

and 3 Examples

They all depends on the Main Nuget Package.


  • JPB.Shell.Example.
    • Console.exe
      • Start application
    • BasicMathFunctions.DLL
      • Implementation for a Simple calculator
    • Contracts.DLL
      • The Application specify Interfaces

The Console application should help you to understand how a Plugin based application should be designed and how in general the framework could be used without any UI specific relevation



  • JPB.Shell.
    • exe
      • Contains the Loading mechanism but not more
    • CommonApplicationConatiner.DLL
      • Contains the a Generic Ribbon UI that loads the other services
    • CommonContracts
      • For interfaces only ... this extends IService interfaces to extend the functionality
    • VisualServiceScheduler
      • A WPF MVVM Module that provides a Surface that can be used in any other app without rebuilding
  • JPB.Foo.
    • Client.Module.DLL
      • Very simple Visual Module
    • ClientCommenContracts
      • The extension of JPB.Shell.Contracts
    • CommenAppParts
      • Single sample implementation of the extended Interfaces

This Solution should show you a sample application that contains how to build a complete Plugin based application for WPF. The basic app is the same as in some Web project or Forms. You provide the Modules via MSEF and then load them by accessing there properties.


Building, Maintaining and the actual usage of a plugin based application is something new to a lot of us Developer. But in our world of fast changing Requirements and Open-Source projects around us all, its something that will cross the way of almost every Dev in this career. Even if this just a moment in a project when we are thinking "hmm this will change in future lets use an interface to ensure that the work will be not that big". For that case i will introduce you a method to get this approach in a new level. Lets talk about some Buzzwords as Dependency Injection and Inverse of Control (IoC). There some nice Articles here on CodeProject so i will not explain them in deep just to ensure we are talking about the same thing. Dependency injection means that we move the responsibility of certain function from the actual actor away. We Inject some logic into a class that does not know what exactly happen. The only constraint to this approach is that at the most times the target Actor must support this kind of behavior. And at least if the class is not intent to allow this coding pattern you should not use it. Most likely you would break the desired code infrastructure if you try to take control over some process when the process is not designed to allow this. Inverse of Control is like the Dependency Injection a coding pattern and is intended to give a process from one actor to an other. As the name say, it allows the Control over a process from the original Actor to some other. This apples to the simple usage of an Delegate for Factory creating as on the Complete WPF Framework. We give away the Control about how something is done.


The reason why this framework as created by me was very simple: I was facing some problems with the creation of a program that was developed fast but then, as a Big surprise for me and the Stakeholder was grown as Hell.

At the end we had a Program that was so big that is was nearly unable to maintain. We decided to redevelop and as we had so many different functions inside this application ( what was not desired to contain all this functions ) we also decided to allow this kind of function "madness".

Then the idea of some kind of Plugin/Module driven application was born inside my head. I'd made some research and found the Managed Extensibility Framework (MEF) inside the .net Framework. But as fast I'd found this, i found the limitations of it. In my opinion it was just to Slow and for this kind of "simple" work. I started extending MEF and wrote some manager classes. The result of my efforts you can explorer here today. As it started for UI related application we fast discovered the full advantages of my code. The code is simple as useful and does 2 things. First it speeds up the enumeration process with the usage of Plinq and some other Optimisations. Second it limit's the Access. Why would this be a good thing you may ask yourself now. MEF provides you a lot of "extra" functionality and some kind of "Nuclear Arsenal" of Configuration. This is for a developer that has no idea of Dependency injection and IoC far to much. So Limiting the Configuration and hide some things you do not necessary need to take care of, is the exact right thing. Some configuration was just changed into some more centralized way.


The Managed Service Extensibility Framework (MSEF) builds up on MEF and extents the usage and Access to MEF. It wraps all MEF Exports into services that can be accessed through its Methods. A class represents one or more Services. When a class defines one or more Services it must not implement its functionality. In the Worst case scenario this will break the law of OOP.


Every shared code we want to manage must be wrapped into an Assembly. This Assembly contains classes that inherits from IService. IService is most likely of an Marker interface with just one definition of an starting Method that will be invoked when the Service started once. Every service that will be accessed by the Framework will be handled as SingleInstance. So Technically there should be just one instance per Service definition. You can create your own instances by using new(), but they will not be observed by the MSEF.

namespace JPB.Shell.Contracts.Interfaces.Services
    public interface IService
        void OnStart(IApplicationContext application);

The IService Interface it the 1st Important thing when we talk about the Implementation. But searching for inheritance would be slow. So we need something additional like an Attribute. An .net Attribute is the desired way of adding meta data to a class. The base Attribute is the ServiceExportAttribute

public class ServiceExportAttribute : ExportAttribute, IServiceMetadata
as you see it also Inherits from the MEF Export attribute and in addition to the MSEF interface. The base class ensures a enumeration from MEF so all classes that are marked for Export are also usable from MEF without change. The IServiceMetadata attribute contains some Additional Infos.
public interface IServiceMetadata
    Type[] Contracts { get; }
    string Descriptor { get; }
    bool IsDefauldService { get; }
    bool ForceSynchronism { get; }
    int Priority { get; }

As i said a class can be used to define one or more Services. To maintain this information to the outside without analyzing the class self the Information must be written to the Attribute. For that the Contracts array is used. For every type in this collection the defined class will be seen as the type.

Every aspect of this code is extendable. If the code does not fit into your need just extend it or wrap it. Just this basic information must be provided. 

This Array of implemented interfaces has pro's and con's. On the Pro site its fast as hell to not load the class from the assembly and then analyze it. But this is also a big disadvantage. You have to define your service "declaration" twice. In the MetaData and also in your actual class. When both are not match there are 2 possible scenarios. You implement a class in your code but not define it in your MetaData. This is not so bad because the worst thing is that the MSEF will not find it. Not good but not the end of the world. The more dangerous thing is that you define a interface in your MetaData but not implement it! Every compiler would not throw an error if you declare an interface but not actually implement it. In this case the code would compile but it will throw an exception on runtime what is, actually pretty bad. I'm planning an Roslyn extension to take care of these problems but this may take a while.

Using the code

The normal usage would be the following:

You define some sort of service by creating Interfaces that inherits from IService.

public interface IFooDatabaseService : IService
    void Insert<T>(T entity);
    T Select<T>(string where);
    void Update<T>(T entity);
    bool Delete<T>(T entity);

That is your service definition. You could just extend this interface as much you want even inheriting from other Interfaces or extend this Service by another service. That's all possible as long as it inherits from IService. To make the service usable you must implement it in some DLL and mark it with the ServiceExport Attribute.

    descriptor: "Some name",
    contract: new[] 
        //Additional Services like services that are inherit 
class FooDatabaseService : IFooDatabaseService
    public void Insert<T>(T entity)
        throw new NotImplementedException();
    public T Select<T>(string where)
        throw new NotImplementedException();
    public void Update<T>(T entity)
        throw new NotImplementedException();
    public bool Delete<T>(T entity)
        throw new NotImplementedException();
    public void OnStart(IApplicationContext application)
        throw new NotImplementedException();

You may recognized that the class had no access modifier so its private by default. This is possible but really bad. As MEF does not more then using Reflection it can and will break the law of OOP. So keep in mind that these laws are not applied here and only enforced due compile time.

We now created all necessary things. We extended the IService interface to allow our own service implementation and marked the class as an Service with the ExportService attribute. Next step would be the enumeration of services and usage of that service.

To start the initial process we must first create the processor. The process is handled be the ServicePool class.

public class ServicePool : IServicePool
    internal ServicePool(string priorityKey, string[] sublookuppaths) 

It only contains a internal constructor and can only created with the ServicePoolFactory.

namespace JPB.Shell.MEF.Factorys
    public class ServicePoolFactory
        public static IServicePool CreatePool()
            return CreatePool(string.Empty, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
        public static IServicePool CreatePool(string priorityKey, params string[] sublookuppaths)
            var pool = new ServicePool(priorityKey, sublookuppaths);
            ServicePool.Instance = pool;
            if (ServicePool.ApplicationContainer == null)
                ServicePool.ApplicationContainer = new ApplicationContext(ImportPool.Instance, MessageBroker.Instance, pool, null, VisualModuleManager.Instance);
            return pool;
        public static async Task<IServicePool> CreatePoolAsync(string priorityKey, params string[] sublookuppaths)
            return await new Task<IServicePool>(() => CreatePool(priorityKey, sublookuppaths));

This factory takes care of all internal properties and the initial call of InitLoading.


Another way would be to access the static ServicePool.Instance property what will do the same as calling CreatePool.

The Service pool will start the Enumeration of all files in the given path by using the priority key to define Assemblies that will be searched with level 1 priority. These files are handled as "necessary" for your program. They should contain all basic logic like the Window in a Visual application or other starting procedures (later more to that). The process will be done as the following:


PriorityKey = "*Module*"

  1. Enumerate all DLLs in your shell directory
  2. Search for a specific part in the name and Flag them as high priority of the name to improve Performance (That just means that DLLs with a name like "FooClientModule.DLL" are loaded at Startup but DLLs like "FooClient.DLL" not! )
  3. Search those high priority assemblies for exports and add them into my StrongNameCatalog, skip all non high priority assemblies
  4. Wait for ending of 3
  5. Start the default service IApplicationContainer
  6. The main window is opening
  7. All assemblies without the high priority flag will be searched for exports

Handling of IApplicationContainer.

This interfaces is provided by the Framework self and indicates a service that must be started as it's available. If the enumeration process is done this service will be instantiated. For this process the ServiceExport Attribute contains the optional parameter ForceSyncronism and Priority. If a IApplicationProvider does not provide this Information ForceSyncronism will be False and the Priority will be very Low. The first parameter is very important because it will cause to block your caller as long as there are Services that are not executed.

  1. Enumerate all IApplicationProvider that are marked to executed Async and start them Unobserved
  2. Execute all services that are marked to be executed synchrony inside the caller thread

This info is important when using multiple Applications that are depend on each other. When ApplicationSerivce A tries to load data from ApplicationService B but service B is not loaded it will fail and cause in Strange behaviors then remember just because it will work on your machine does not mean that it will work on every mashie. This is caused be the huge impact of Multithreadding and Tasking. Every Machine decides on its own how Tasks and Threads are handled.

Usage and Meaning of IApplicationProvider.

The Idea is simple. As far as we have a application that is only connected with loosely dependencies over Interfaces, there is no Starting mechanism too. To support this kind of unobserved starting from the caller, we use this interface. Useful implementations would be Service that pulls data from a Database that must be available at start. So when we think back to our FooDatabaseService, it provides us a method to access a Database and Preload the request Data.

But this brings us too the next problem is a service based application. The communication between services is a very complex point. As long as we could not really expect a service to be exist, the framework brings its own. This kind of communication is implemented inside the IService interface.

public interface IService
    void OnStart(IApplicationContext application);

Every service contains this starting logic and handles the IApplicationContext. This interfaces provides us the basic functions like DataStorage(DataBroker), DataCommunication(MessageBroker), ServiceHandling(ServicePool) and some more. The idea is that services are Isolated from each other and from the Main logic of your application. They can not know you and you don't know them. So the Framework provides a way of communication

public interface IApplicationContext
    IDataBroker DataBroker { get; set; }
    IServicePool ServicePool { get; set; }
    IMessageBroker MessageBroker { get; set; }
    IImportPool ImportPool { get; set; }
    IVisualModuleManager VisualModuleManager { get; set; }

IDataBroker provides your service a centralized interface for Storing data in a persistent way. In the the current State of Development this is the the only Context Property that is null per default. If you want to provide your services a DataBroker then you need to set it from the outside or from one of your Services. One possible solution would be:

    descriptor: "AppDataBroker",
    isDefauld: false,
    forceSynchronism: true,
    priority: 0,
    contract: new[] {
class FooDataBroker : IDataBroker, IApplicationProvider
    #region IDataBroker Implementation

    /// <summary>
    /// </summary>
    /// <param name="application"></param>
    void Contracts.Interfaces.Services.IService.OnStart(IApplicationContext application)
        //Do some Initial things like open a database and pull application settings
        //add yourself as the DataBroker
        application.DataBroker = this;

From the top: We define the Export attribute to mark this class as a Service. Force the Synchronism and priority to 0 to load this before all other interfaces ( depends on your application logic may this service depends also on an other ). At least we define 2 interfaces to be exported. First IDataBroker so if some other components will ask for this Interface ( inherits also from IService ) it will get this one, and 2nd IApplicationProvider so that we will be called as soon as Possible.

Next thing is the IMessageBroker that allows us to Transport data from one Costumers to another. It has a Standard implementation that allows everyone to add him self as a Consumer based on a Type as a key. If then some other consumer will publish data that is of type of a key, all Costumers will be notified.

public interface IMessageBroker
    void Publish<T>(T message);
    void AddReceiver<T>(Action<T> callback);

A useful other implementation would be a service that checks T for be some kind of special message and if so it could publish the message instead of locally to a WCF service and spread the message to other Customers. This could extend you're local application from Plugin based to even Remote Applications.

Image 1

So as no one knows the caller, no one would be able to work with other parts of the application. For that general case every service has to know the global ServicePool. To get even rid of this dependency between every Service and the MSEF framework processor, the ServicePool is contained inside the ApplicationContext. With this infrastructure, no service is known of the Caller and we centralized all logic inside the ServicePool. Like the Hollywood principle "Don't call us, we call you" we got a absolutely clear abstraction of every logic.

But this has also a disadvantage. If no one in your application else then the Original Starter inside the .exe, knows the IServicePool, how could they Query against it? At this point the Developer comes into play. There is no "how it must be done" solution for this problem, just one restriction. To maintain your plugin approach do not reference JPB.Shell or your exe directly and Vice versa. 


Store the Contracts.Interfaces.Services.IService.OnStart(IApplicationContext application)

That's simple. For each DLL that contains a Service like your FooDatabaseService create a 2nd Service and call it Module. This service is an IApplicationProvider and stores the IApplicationContext in a Static variable. Because of the fact that the Module service will always be called before every other service, the variable ( let's call it Context ) will never be null and you always have a valid reference to the MSEF without knowing the caller or the MSEF DLL.

Last but not least we will talk about the most basic part, the direct call of IServicePool. e.g.. How can we obtain a service from the Framework.

//Module is my VisualModule that is invoked before
           var infoservice = Module
               //Context is my static variable of IApplicationContext
               //ServicePool is simply the current instance of ISerivcePool
               //GetSingelService gets a singel service of the requested Interface it has the FirstOrDefauld behavior
           //Check if null
           if (infoservice == null)
           //For example get the last Entry in the ImportPool that logs every actively that is done be ServicePool
           var logEntry = Module.Context.ImportPool.LogEntries.Last();
           //Call a Function on that service that we got
           //in that case we want to insert the Log into the DB

Metadata and Attributes

There are 2 ways for using Metadata. Based on a Service implementation and based on the service self. Every service can define its own Metadata by using ServiceExport and its Properties or the service ( represented by its Interface ) can define some standard properties that will be applied on all inherited services.

Image 2

In this case the service interface does not contain any kind of Metadata, the implementations defines Metadata on there own.

Image 3

In this case we inverted the MetaData away from the Implementation to the declaration of the Service.

This offers as the following usage:

public class FooMessageBox : IFooMessageBox
    Contracts.Interfaces.IApplicationContext Context;
    public void showMessageBox(string text)
        MessageBox.Show("Message from Service: " + text);
    public void OnStart(Contracts.Interfaces.IApplicationContext application)
        Context = application;

For this case we can skip the MetaData declaration on Class level and move the MetaData to the interface.

[InheritedServiceExport("FooExport", typeof(IFooMessageBox))]
public interface IFooMessageBox : IService
    void showMessageBox(string text);

This has good and bad sides:

We do not need to care about the Metadata because we did that once while we created the Interface.


We can not longer control or modify the Metadata. Since the Attribute needs constant values, every implementation of IFooMessageBox provides the same Metadata and for the framework, there all the same. So we got this problem:

Image 4

You can see since we are using the InheritedServiceExportAttribute we don't know who is who because there all declares the same Metadata.

Using the code for UI

There is no deniable advantage for UI application that are using Plugins to extend there surface and logic. Even for this reason the Framework was original designed. I talked a lot how to use the Framework in general so now i will introduce you to the possible usage for a UI Application.

By setting up a Complete new Project you need a starting logic that invokes the Enumeration of the modules and a directory. So even by creating a new ConsoleApplication you need a starting logic. But let us go a bit more Specific. As it was designed for it, the Framework contains a easy to use service for applications that are using the MVVM.

It contains a Interface IVisualService and a Service IVisualModuleManager. Both builds up to support MVVM.

The IVisualModuleManager is implemented by the VisualModuleManager and it is most likely a wrapper for the ServicePool that filters for IVisualService's.

A Possible usage would be that the executable calls the ServicePool and starts the Process. A DLL contains a IApplicationProvider and he will show a Windows with a List. Then the ApplicationProvider will use the given VisualModuleManager to ask for all Instances of IVisualServiceMetadata and populates the Listbox with it. As we are only Query for the MetaData, the no actual Service is created! This has an extreme effect on Performance. When selecting an item on this list box, the AppProvider will call VisualModuleManager to create a IVisualService based on the given Metadata. Then the other Service will be created and we can access a VisualElement by calling the property View of IVisualService. Last but not least, the VisualServiceManager need some inherited metadata called VisualServiceExport. If you would like to you can extend both the Metadata or the IVisualService to add your own properties or functions.

An proper example with a Ribbon is in Github.

Before you start

To ensure that your application and you Models goes into the same folder ( that is very important because how should MEF know where are your DLLs? ;-), set the BuildPath to the same as the Shell ( Default is "..\..\..\..\bin\VS Output\" for Release and "..\..\..\..\bin\VS Debug\" for Debug. Than ensure that your set the "On Run,When Projects out of date" option to Always build. Also Remind yourself that MEF is not designed to load Services from a .exe. So only .DLL 's are supported. 

Features that you should know

There are 2 Preprocessor Directives that controls some of the behavior.


Allows the StrongNameCatalog the using of PLinq to search and include the Assemblies into the ¬ Catalog with a mix of Eager and Lazy loading. For my opinion you should not disable this in your version because it is Tested, Save and very fast. Just for debugging purposes does this make sense.


As in this link described, the Catalog contains a way to guarantee a small amount of security. I never used this so i can not say anything to that.

Possible Scenario's

There are a lot of possible Scenarios. I will explain some to give you some ideas how the system is working and for what kind of work it is designed. We will start with a simple application that doing some work. Like a Calculator it can Multiply numbers. As the System was designed with some Interface logic it contains an interface named ICalculatorTask. This interface defines only one method that is called Calculate(). this Method takes only 2 Parameters of type string. There are now 2 ways to implement this Plugin approach.

1. Reimpliment ICalculatorTask and Inherit from IService

This approach will cause all existing and future tasks the be "known" as a service. This is sometimes not exactly what we want. This would be the easy way and sometimes the best but this depends on your coding style. By Implementing IService and then Defining the export attribute, we could load all interfaces at once and without any dependency to it.

2. Inherit from ICalculatorTask and create an Service Adapter

This approach will take use of the Adapter pattern and will wrap all Service functionally to an own class. That is useful if we don't want to alter the existing code and prevent the target classes the be known that they are really used as Services. To do this we will create a class called CalculatorWrapper. This base calls will take a ICalculatorTask instance and will delegate all tasks to it. Then we inherit from this wrapper class and create a new class called Multiply or what every exact instance we are defining.

this simple Scenario would be a good idea to use a Plugin based system.

an proper example with a Console and a small Calculator is in Github.

Points of Interest

I had a lot of fun while creating this project and I guess it is worth it, to share not only the idea with other developers and everyone interested in it. When I started with MEF in .NET 4.0 I was driven crazy because the pure MEF system is very Complex. I maintain this project now for more then a year and still got ideas how to extend or improve it.

I would highly appreciate any new ideas and impressions from you.

Also i would like to hear / see applications you made with it.

Just Contact me here. Thank you in advice.


As suggested from user John Simmons i cleaned up the repository and removed everything WPF specific from the trunk solution. Now i only contains the 2 Main assemblies:

  1. JPB.Shell.MEF
  2. JPB.Shell.Contracts

Help me!

As i am very interested in Improving my Skills and the Quality of my code, i created a Form the receive input from you directly. If you are using this project please feel free to take it, it will not least longer then a minute.


V1: Initial creation

V1.1: Minor bugfixes as:

  1. Fixed the MEF container bug that causes a IService method OnLoad could be called multiple times
  2. Fixed the unporpper usage of INotifyPropertyChanged implementation of IServicePool
  3. Due the first call, all services are wrapped into a new Lazy instance that will take care of OnStart.

V2 Minor changes:

  1. Removed unessesary things from the Trunk
  2. Made a Console example
  3. Added as Nuget Packet ( See Top )

V3 Changes:

Rewrote the Article in CodeProject

V3.1 hotfix:

  1. Added an Exception to Master that triggers when no Valid path is provided
  2. Added the RibbonWindow installer to the WPF branch. Please run it befor you try to execute the example


New features:

  1. New funktion for loading callbacks inside the IImportPool


This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Written By
Software Developer Freelancer
Germany Germany
A nice guy.
And WPF Developer.

like everything I can get my hand on in .net.
But never java.

Comments and Discussions

QuestionNothing Pin
Boer Coene4-Mar-14 9:00
Boer Coene4-Mar-14 9:00 
AnswerRe: Nothing Pin
GerVenson4-Mar-14 10:01
professionalGerVenson4-Mar-14 10:01 
QuestionCan't run Pin
Andre De Beer21-Nov-13 20:06
Andre De Beer21-Nov-13 20:06 
AnswerRe: Can't run Pin
GerVenson21-Nov-13 22:11
professionalGerVenson21-Nov-13 22:11 
GeneralRe: Can't run Pin
Andre De Beer21-Nov-13 22:37
Andre De Beer21-Nov-13 22:37 
GeneralRe: Can't run Pin
GerVenson21-Nov-13 23:12
professionalGerVenson21-Nov-13 23:12 
QuestionMy vote of 5 Pin
bæltazor21-Nov-13 20:05
bæltazor21-Nov-13 20:05 
QuestionThe tag 'DXWindow' does not exist in XML namespace Pin
Sumeet Kumar G21-Nov-13 5:23
Sumeet Kumar G21-Nov-13 5:23 
I'm getting the following error
The tag 'DXWindow' does not exist in XML namespace
AnswerRe: The tag 'DXWindow' does not exist in XML namespace Pin
GerVenson21-Nov-13 5:33
professionalGerVenson21-Nov-13 5:33 
QuestionDownload does not work Pin
Boer Coene19-Nov-13 7:19
Boer Coene19-Nov-13 7:19 
AnswerRe: Download does not work Pin
GerVenson19-Nov-13 7:20
professionalGerVenson19-Nov-13 7:20 
GeneralRe: Download does not work Pin
Boer Coene19-Nov-13 7:30
Boer Coene19-Nov-13 7:30 
GeneralRe: Download does not work Pin
GerVenson19-Nov-13 7:32
professionalGerVenson19-Nov-13 7:32 
GeneralRe: Download does not work Pin
Boer Coene19-Nov-13 7:48
Boer Coene19-Nov-13 7:48 
GeneralRe: Download does not work Pin
GerVenson19-Nov-13 7:50
professionalGerVenson19-Nov-13 7:50 
GeneralRe: Download does not work Pin
Boer Coene19-Nov-13 8:06
Boer Coene19-Nov-13 8:06 
GeneralRe: Download does not work Pin
GerVenson19-Nov-13 21:50
professionalGerVenson19-Nov-13 21:50 

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.