Click here to Skip to main content
15,867,780 members
Articles / Desktop Programming / MFC

An Introduction to a Model-View-Controller Implementation for MFC

Rate me:
Please Sign up or sign in to vote.
4.67/5 (44 votes)
20 Mar 2009CPOL13 min read 106.1K   4.8K   146   24
Introduction to an MVC Framework that integrates with the MFC Doc/View architecture
Shapes_small.png

For Those Of You Who Are Interested...

I have posted the following articles in the series describing the MVC Framework.

Table of Contents

Introduction

In the Model-View-Controller architectural pattern, the Model represents the application data, the View, the visual components that present the data to the user, and the Controller manages the user's interactions with the various input devices and interprets how they should affect the Model. Once affected, the Model informs the View of the need to update its presentation to the user. That's a rather simplistic explanation; however, if you want a more complete explanation, there are many good articles and books explaining the MVC pattern and the motivations behind its design. One very good source can be found in the Wikipedia [^].

As anyone with MFC experience can tell you, its Document/View architecture is a variation of the MVC pattern. In MFC, the CDocument class represents the Model, and the CView class represents both the View and the Controller. Obviously, the Model-View-Controller Framework presented in this article will bring back the separation of View and Controller to the MFC application.

I have used aspects of the MVC Framework in existing MFC applications with years of legacy code as well as with new MFC Feature Pack AppWizard created applications. The point I make here is that the Framework does not disrupt the existing Doc/View architecture, and can be implemented unobtrusively over time. Old code can be incrementally refactored to use the new MVC architecture while still using the existing Doc/View architecture.

I don't consider the Framework to be complete by any means, as you will see in the TODO section at the bottom of the article; however, I have completed enough to illustrate the underlying concepts. This article introduces the Framework by presenting the classes that form the Framework base that integrates with the MFC Doc/View architecture.

The Source Code

The source code provided with this article is contained in a VS2008 solution, SbjDev, with three projects:

  • SbjCore - The foundation DLL
  • XmlMvc - A DLL that contains MVC Framework extensions that support an XML Model
  • Shapes - The sample EXE

The approach I've taken is that any code that could be used in any application should go into SbjCore. Any code that is specific to an XML implementation of the MVC Framework should go into XmlMvc, and only code which is specific to the application should go into the application. Granted, I'm not always successful, and sometimes, I don't see the generalities in the code that can be factored down to the lower levels, but that has been my intent.

SbjCore.dll

The MVC Framework is part of the foundation DLL, SbjCore.dll. The code is all namespaced, and to some degree, the structure of the namespaces is mimicked in the structure of the Visual Studio Project where it resides. Not surprisingly, the root is the namespace SbjCore, and all of the MVC code is in SbjCore::Mvc. Along with the MVC Framework, SbjCore also contains a large number of utility components, many of which were influenced by CodeProject articles, for helping deal with a wide variety of common programming tasks such as Memory, Strings, Images, the Clipboard, etc. There are also components that implement an Event architecture, DragNDrop, UndoRedo, and more. As I said, anything that can be used in any application goes here.

XmlMvc.dll

The XmlMvc.dll uses the MSXML6 implementation of the Document Object Model (DOM), which provides an ideal base of generic routines for handling an XML Model. All XML extensions to the MVC Framework are contained in this separate DLL. In general, the XmlMvc Model makes the assumption that XML elements are like records in a database and XML attributes are like the fields of the record. In this way, it's able to generalize a lot of code that might have ended up application specific.

Other technologies that one would use to implement a Model such as ADO, SQL, or proprietary DBs would be treated in the same way.

Shapes.exe

Shapes.exe is a rudimentary drawing program created using the MFC Feature Pack AppWizard that integrates the MVC Framework and XmlMvc Model extensions with its base MFC Doc/View architecture.

An existing Shapes.xml file is provided as an illustration of what XML file format the Shapes application expects. As you can see from the listing below, under the Shapes document element is an element called Drawing, which contains elements of type Rectangle and Ellipse. Each of these has a number of attributes which describe its appearance when displayed in the main View of the application.

Shapes.xml
XML
<?xml version="1.0" encoding="utf-8"?>
<Shapes>
  <Drawing name="Test Drawing">
    <Rectangle label="The First Rectangle" left="250" top="250" right="750" bottom="750"
      borderRGB="255" borderWidth="9" fillRGB="128"/>
    <Rectangle label="Blue Rectangle" left="100" top="90" right="200" bottom="200"
      borderRGB="16711680" borderWidth="1" fillRGB="16711680"/>
    <Ellipse label="The First Ellipse" left="300" top="200" right="800" bottom="400"
      borderRGB="65280" borderWidth="1" fillRGB="65280"/>
  </Drawing>
</Shapes>

As you would assume, the Shapes application provides the user with the ability to add, delete, modify, and move individual and multiply selected Rectangles and Ellipses. As illustrated in the screenshot at the beginning of the article, the application contains the Main Design View and two Docking Panes: an Explorer Tree View of the Drawing and a Property Grid showing the attributes of the selected shape.

The MVC Framework

If you've used MFC at all, you should be familiar with the way the CDocument class informs its list of CView classes of the need to update through calls to each CView::OnUpdate method. You should also be familiar with how the CCmdTarget and CWnd classes handle messages through their message maps and handler methods. So, rather than going into detail describing the MFC implementation, I'll provide an overview of the Framework and its base components, with the focus on how they integrate with the Doc/View architecture.

Controlling CCmdTarget and CWnd

The key to the integration is the ability to selectively hijack the Windows messages sent to the CCmdTarget and CWnd classes before they are processed by the default MFC message map and routing implementation. This is accomplished by two classes.

C++
template<class _baseClass>
class ControlledCmdTargetT : public _baseClass
Project location: SbjCore/Mvc/ControlledCmdTargetT.h
C++
template<class _baseClass>
class ControlledWndT : public ControlledCmdTargetT<_baseClass>
Project location: SbjCore/Mvc/ControlledWndT.h

The ControlledCmdTargetT class overrides the OnCmdMsg method of its CCmdTarget derived _baseClass, and the ControlledWndT class additionally overrides the OnWndMsg method of its CWnd derived _baseClass. These overrides direct the incoming messages first to an assigned Controller class for handling before letting the standard MFC message map processing to occur.

By using a template base class, these classes can derive respectively from any CCmdTarget or CWnd derived class. For instance...

C++
class ControlledDocument : public ControlledCmdTargetT<CDocument>

class ControlledView : public ControlledWndT<CView>

MessageHandler Classes

How Windows messages are handled is a major difference between the MFC Doc/View architecture and the MVC Framework. In the Framework, message handlers are not methods of a CCmdTarget or CWnd class as they are in the Doc/View, but are classes in their own right. There are three basic variations, all deriving from a base MessageHandler class, each handling a different type of Windows message.

  • MessageHandler
    • CmdMsgHandler - WM_COMMAND
    • NotifyMsgHandler - WM_NOTIFY
    • WndMsgHandler - other WM_XXX

CmdMsgHandler, NotifyMsgHandler, and WndMsgHandler are all pure virtual, and it is always one of these that provide the base for the actual concrete handlers. The MessageHandler base class is never directly derived from. Also, the Message ID is not directly assigned to the handler, rather it is mapped to the handler when it is added to a Controller.

Class MessageHandler

MessageHandler provides the base for the CmdMsgHandler, NotifyMsgHandler, and WndMsgHandler classes. In addition to providing common base for the derived classes, MessageHandler also provides a chaining mechanism so derived Controllers can provide MessageHandler classes for the same message ID, allowing them to access previously assigned MessageHandler classes. This is similar to the way MFC method based message handlers access base class method handlers. When assigned to a Controller, the Controller calls MessageHandler::Initialize with itself as the Controller, and if there is one, the last previously assigned MessageHandler. CmdMsgHandler, NotifyMsgHandler, and WndMsgHandler provide methods for calling previous handlers they have been assigned.

C++
class AFX_EXT_CLASS MessageHandler
{
public:
  MessageHandler();

  virtual ~MessageHandler();

public:
  void Initialize(Controller* pCtrlr, MessageHandler* pPrevHandler);

Controller* GetController() const;

protected:
  MessageHandler* GetPrevHandler() const;

private:
  struct MessageHandlerImpl* const m_pImpl;
};
Project location: SbjCore/Mvc/MessageHandler.h
Class CmdMsgHandler

CmdMsgHandler is an abstract class for handling WM_COMMAND messages. The CmdTargetController calls the public Handle methods of the CmdMsgHandler for handling not only the CmdMsg variation, but also the CmdUI variation of messages. The private OnHandleCmd method is a pure virtual, and must be implemented in the derived class. The OnHandleCmdUI method has a default implementation that returns true for the CCmdUI::Enable method of the passed CCmdUI. To support the functionality derived from its base MessageHandler class for the chaining of handlers, derived OnHandleCmd and OnHandleCmdUI methods should call the public methods HandleCmdPrev and HandleCmdUIPrev, as appropriate.

C++
class AFX_EXT_CLASS CmdMsgHandler : public MessageHandler
{
public:
  virtual ~CmdMsgHandler();

  CmdTargetController* GetController() const;
public:
  bool HandleCmd(EventID nID);
  bool HandleCmdUI(CCmdUI* pCmdUI);
  bool HandleCmdPrev(EventID nID);
  bool HandleCmdUIPrev(CCmdUI* pCmdUI);
private:
  virtual bool OnHandleCmd(EventID nID) = 0;
  virtual bool OnHandleCmdUI(CCmdUI* pCmdUI);
};
Project location: SbjCore/Mvc/CmdMsgHandler.h
Class NotifyMsgHandler

NotifyMsgHandler is an abstract class for handling WM_NOTIFY messages. The private OnHandleNotify method is a pure virtual, and must be implemented in the derived class. To support the functionality derived from its base MessageHandler class for the chaining of handlers, derived OnHandleNotify methods should call the public method HandleNotifyPrev, as appropriate.

C++
class AFX_EXT_CLASS NotifyMsgHandler : public MessageHandler
{
public:
  virtual ~NotifyMsgHandler();

  CmdTargetController* GetController() const;
public:
  bool HandleNotify(NMHDR* pNMHDR, LRESULT* pResult);
  bool HandleNotifyPrev(NMHDR* pNMHDR, LRESULT* pResult);
private:
  virtual bool OnHandleNotify(NMHDR* pNMHDR, LRESULT* pResult) = 0;
};
Project location: SbjCore/Mvc/NotifyHandler.h
Class WndMsgHandler

WndMsgHandler is an abstract class for handling all the rest of the Windows messages. The private OnHandleWndMsg method is a pure virtual, and must be implemented in the derived class. To support the functionality derived from its base MessageHandler class for the chaining of handlers, derived OnHandleWndMsg methods should call the public method HandleWndMsgPrev, as appropriate. Provision is made for calling the MFC default message handling first. The OnCallDefaultFirst method, by default, returns false; however, derived classes may override this to return true where appropriate.

C++
class AFX_EXT_CLASS WndMsgHandler : public MessageHandler
{
public:
  virtual ~WndMsgHandler();

  WndController* GetController() const;
public:
  LRESULT HandleWndMsg(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
  bool HandleWndMsgPrev(WPARAM wParam, LPARAM lParam, LRESULT* pResult);

  bool CallDefaultFirst();
private:
  virtual LRESULT OnHandleWndMsg(WPARAM wParam,
                  LPARAM lParam, LRESULT* pResult) = 0;
  virtual bool OnCallDefaultFirst();
};
Project location: SbjCore/Mvc/WndMsgHandler.h

Controller Classes

In the MVC Framework, one of the roles the Controller plays is that of a message manager for the ControlledCmdTargetT or ControlledWndT to which it is assigned by maintaining a map of MessageHandler objects. When a ControlledCmdTargetT or ControlledWndT class passes a message to its Controller class, the Controller uses the ID of the message to look for a handler in its map, and if found, it gives the handler a chance to respond. If no handler is found, or the handler wishes for the message to continue to be handled, the base class of the Controlled class is given a chance. In most cases, this will be a CmdTarget or CWnd derived class with a standard MFC message map implementation. It is in this way that the Framework integrates seamlessly with the existing MFC implementation.

As with the MessageHandler classes, there is a base Controller class, and in this case, just two base derivatives: CmdTargetController and WndController.

  • Controller
    • CmdTargetController
    • WndController
Class Controller

Controller provides a base for the CmdTargetController and WndController classes. It is responsible for maintaining the map of MessageHandler classes for its Controlled class. It has methods for adding and removing handlers from its map, and an accessor to get the current handler for a given ID. This is used internally to chain handlers for the same ID.

You'll notice a method PrepareCtxMenu. Derived Controller classes are queried through this method in response to the WM_CONTEXTMENU message as to what menu items should be added to the current invocation of the Context Menu.

C++
class AFX_EXT_CLASS Controller
{
public:
  Controller();

  virtual ~Controller();

public:

  void Initialize();

public:
  void AddHandler(EventID nID, MessageHandler* p);
  void AddHandler(EventID nFirstID, EventID nLastID, MessageHandler* p);
  void RemoveHandler(EventID nID);
  void RemoveHandler(EventID nFirstID, EventID nLastID);

  MessageHandler* GetHandler(EventID nID) const;

  SbjCore::Utils::Menu::ItemRange PrepareCtxMenu(CMenu& ctxMenu) const;

private:
  virtual void OnInitialize();
  virtual SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu(CMenu& ctxMenu) const;

private:
  struct ControllerImpl* const m_pImpl;
};
Project location: SbjCore/Mvc/Controller.h
Class CmdTargetController

The CmdTargetController class controls ControlledCmdTargetT classes. It handles the mapping of the CmdMsgHandler and NoftifyMsgHandler classes. When the ControlledCmdTargetT receives a message, its OnCmdMsg method determines, in the same way that MFC does, which flavor of message it is. It then calls the appropriate CmdTargetController::HandleCmdMsg, CmdTargetController::HandleCmdUIMsg, or CmdTargetController::HandleNotifyMsg method. If the message is not handled, it then calls CmdController::RoutCmdMsg, giving the controller a chance to override the default MFC routing. If the message still isn't handled, or warrants further processing, the base class of the ControlledCmdTargetT is given a chance.

In this class, you'll see the GetUndoRedoMgr method. Explanation of this is really beyond the scope of this article; however, the SbjCore::Mvc::DocController derived from CmdTargetController assigns the SbjCore::UndoRedo::Manager to the task, and it is implemented in full in the Shapes.exe application.

C++
class AFX_EXT_CLASS CmdTargetController : public Controller
{
public:
  CmdTargetController();

  virtual ~CmdTargetController();

public:

  void SetCmdTarget(CCmdTarget* p);

  CCmdTarget* GetCmdTarget() const;

public:

  bool RoutCmdMsg(EventID nID, int nCode, void* pExtra,
                  AFX_CMDHANDLERINFO* pHandlerInfo);

  UndoRedo::Manager* GetUndoRedoMgr() const;

public:

  bool HandleCmdMsg(EventID nID);
  bool HandleCmdUIMsg(EventID nID, CCmdUI* pCmdUI);
  bool HandleNotifyMsg(NMHDR* pNMHdr, LRESULT* pResult);

private:
  virtual bool OnRoutCmdMsg(EventID nID,
  int nCode,
  void* pExtra,
  AFX_CMDHANDLERINFO* pHandlerInfo);

 virtual UndoRedo::Manager* OnGetUndoRedoMgr() const;

private:
 struct CmdTargetControllerImpl* const m_pImpl;
};
Project location: SbjCore/Mvc/CmdTargetController.h
Class WndController

The WndController class controls the ControlledWndT classes. It handles the mapping of the WmdMsgHandler classes. Of course, since it is derived from CmdTargetController, it can also handle CmdMsgHandler and NotifyHandler classes.

As you know from handling Windows messages in MFC, it is sometimes appropriate to call the base class handler before processing the Windows message in your handler. To provide for this, when the ControlledWndT receives a message, its OnWndMsg method calls the WndMsgController::CallDefaultFirst method, which in turn queries the handler map for an entry, and if found, asks the handler if default processing should precede the call to it. Of course, if the message is not handled, or warrants further processing, the base class of the ControlledWndT is given a chance.

C++
class AFX_EXT_CLASS WndController :  public CmdTargetController
{
public:
  WndController();

  virtual ~WndController();

public:
  void SetWnd(CWnd* p);

  CWnd* GetWnd() const;

public:

  BOOL HandleWndMsg(EventID message, WPARAM wParam,
                    LPARAM lParam, LRESULT* pResult);

  bool CallDefaultFirst(EventID message);
private:
  struct WndControllerImpl* const m_pImpl;
};

The Model and Events

Rather than using the CView::OnUpdate method, the MVC Framework instead uses an Event architecture. Each time the Model changes, an Event is fired, indicating the type of change that has occurred. Event handlers are registered for a given type of change, and respond when the Event is fired. Unlike the MFC Doc/View architecture, the Event architecture is not limited to CView and derivative classes, and can be utilized by any object interested in Model change.

Uniquely Identifying an Event

Events are identified by unique ID values which are created using Joseph M. Newcomer's RegisterWindowMessage technique [^] that I've been using forever.

DECLARE_EVENT_ID macro
C++
#define DECLARE_EVENT_ID(name, guid) DECLARE_USER_MESSAGE(name, guid)
Project location: SbjCore/EventMgr/EventMgr.h
Namespace EventMgr

The Event architecture is not actually part of the MVC Framework as it can be used independently. Rather, the namespace SbjCore::EventMgr contains the functions and classes that make up the Event architecture.

EventMgr maintains a private map of registered EventHandler classes which are keyed by unique ID.

C++
typedef UINT EventID;

EventHandler classes register themselves with the private EventMgr handler map during construction, and unregister when destructed. When an Event class with a matching ID key is fired, its NotifyHandlers method will call each registered EventHandler::Handle method, passing a pointer to itself. Listed below are the various components of the Event architecture.

Class Event

The Event class is pretty straightforward. When fired, it notifies its handlers.

C++
class AFX_EXT_CLASS Event
{
public:
  Event();
  Event(EventID nEventID, bool bAutoFire = true);

  virtual ~Event();

public:
  void Init(EventID nEventID, bool bAutoFire = true);

  void NotifyHandlers();

private:
  struct EventImpl* const m_pImpl;
};
Project location: SbjCore/EventMgr/EventMgr.h
Class EventT

The EventT class provides a generic derivation of Event through which handlers can receive any specific data necessary for processing the event.

C++
template <class T>
class EventT : public Event
{
  T theData;
public:
  EventT(EventID nEventID, T data, bool bAutoFire = true) :
    Event(nEventID, false), // must auto fire after fully constructed
    theData(data)
  {
    if (bAutoFire) // now we can auto fire
    {
      NotifyHandlers();
    }
  }

  virtual ~EventT()
  {
  }

  T GetData() const
  {
    return theData;
  }

};
Project location: SbjCore/EventMgr/EventT.h
Fire Function

The Fire function provides a convenient way to fire an event when the only information the handler needs is the fact that the event happened.

C++
AFX_EXT_API void Fire(EventID nEventID);
Project location: SbjCore/EventMgr/EventMgr.h
Class EventHandler

EventHandler is an abstract base class. Derived classes must implement the private virtual method OnHandle which is called from the public method Handle. EventHandler classes register themselves with the EventMgr handler map on construction, and unregister on destruction.

C++
class AFX_EXT_CLASS EventHandler
{
public:
  EventHandler(EventID nEventID);

  virtual ~EventHandler();

public:
  void Handle(Event* pEvent);

private:
  virtual void OnHandle(Event* pEvent) = 0;

private:
  struct EventHandlerImpl* const m_pImpl;
};
Project location: SbjCore/EventMgr/EventMgr.h
AbortException

AbortException provides a way for handlers to short circuit the fire mechanism. When thrown by a handler, the Event::NotifyHandlers method will stop firing the event to subsequent handlers.

C++
typedef CUserException AbortException;
Project location: SbjCore/EventMgr/EventMgr.h

Conclusion

In the preceding sections, I've introduced the classes that provide the base functionality of the MVC Framework, and shown how they integrate with the MFC Doc/View architecture. From these classes, the Framework is extended by providing ControlledCmdTargetT and ControlledCWndT derivatives and associated Controller classes for the main components of an MFC application. From these, the XmlMvc.dll provides extensions that support an XML Model implementation. If there is interest, future articles will explore the specific application of the Framework to CDockablePane types and the controls they contain, the sample DesignView and XML Model used in the Shapes.exe, and the reusable aspects of these components. All of this is contained in the source code provided with this article, so, run Shapes.exe and explore the code in the application and DLLs. I think you'll be surprised at how little code is actually at the application level.

TODO

  • Support for all common controls
  • Support for MFC CView derivatives
  • Implement DocTemplate derivatives for CDockablePane
  • Complete generalization based on ModelItemHandle

History

  • 2008 Oct. 20 - Original article submitted
  • 2008 Nov. 24 - Added link to follow up article
  • 2009 Mar. 19 - Added link to second follow up article

License

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


Written By
SBJ
United States United States
Real name is Steve Johnson. Programming since 1979. Started on a Heathkit Micro with a DEC LSI-11 and UCSD Pascal. Moved to PCs & DOS as soon as Turbo Pascal became available. Did some Assembly, ISR, TSR etc. All this while working for a Manufacturing Co. for 8 years. Had my own solo Co. doing barcode labeling software for 4 years (terrible business man, all I wanted to do was code). Since then working for various software companies. Moved to Windows around the time of 3.1 with Borland C then C++. Then on to VC++ and MFC, and just about anything I could get my hands on or had to learn for my job, and been at it ever since. Of course recently I've been playing with .NET, ASP, C#, WPF etc.

Comments and Discussions

 
QuestionHow does the View update the data from Model? Pin
Ivan.03-Sep-15 1:55
Ivan.03-Sep-15 1:55 
AnswerRe: How does the View update the data from Model? Pin
SBJ8-Oct-15 19:42
SBJ8-Oct-15 19:42 
QuestionTreeCtrl question Pin
huby8914-Sep-12 10:55
huby8914-Sep-12 10:55 
AnswerRe: TreeCtrl question Pin
SBJ5-Nov-12 19:54
SBJ5-Nov-12 19:54 
QuestionUpdate Pin
SBJ11-Feb-12 22:14
SBJ11-Feb-12 22:14 
BugIsn't running on Windows7 VS2010 Pin
Yoav Netzer11-Nov-11 6:47
Yoav Netzer11-Nov-11 6:47 
GeneralRe: Isn't running on Windows7 VS2010 Pin
SBJ11-Nov-11 21:03
SBJ11-Nov-11 21:03 
GeneralRe: Isn't running on Windows7 VS2010 Pin
Jürgen Stephan12-Jun-16 11:50
Jürgen Stephan12-Jun-16 11:50 
GeneralThis is great stuff Pin
steveb12-Jan-11 6:03
mvesteveb12-Jan-11 6:03 
GeneralRe: This is great stuff Pin
SBJ27-Jan-11 10:05
SBJ27-Jan-11 10:05 
Thank you, much appreciated!
QuestionHow to use it for developers Pin
xiang_yan14-Dec-10 18:41
xiang_yan14-Dec-10 18:41 
GeneralCan't run program Pin
softcaster5-May-10 4:14
softcaster5-May-10 4:14 
GeneralRe: Can't run program Pin
SBJ5-May-10 5:27
SBJ5-May-10 5:27 
GeneralRe: Can't run program Pin
Yaroslav Lobachevski1-Oct-11 8:32
Yaroslav Lobachevski1-Oct-11 8:32 
GeneralExcellent! Pin
wang1st25-Aug-09 21:53
wang1st25-Aug-09 21:53 
GeneralRe: Excellent! Pin
SBJ26-Aug-09 7:09
SBJ26-Aug-09 7:09 
GeneralThank you Pin
liuypman30-Jun-09 17:51
liuypman30-Jun-09 17:51 
GeneralRe: Thank you Pin
SBJ30-Jun-09 18:20
SBJ30-Jun-09 18:20 
GeneralNice article.. Pin
tojohere15-Apr-09 21:21
tojohere15-Apr-09 21:21 
GeneralRe: Nice article.. Pin
SBJ16-Apr-09 6:43
SBJ16-Apr-09 6:43 
GeneralVS 2005 Pin
transoft2-Dec-08 12:44
transoft2-Dec-08 12:44 
GeneralRe: VS 2005 Pin
SBJ2-Dec-08 12:52
SBJ2-Dec-08 12:52 
GeneralNicely Done Pin
Rome Singh20-Nov-08 4:52
Rome Singh20-Nov-08 4:52 
GeneralRe: Nicely Done Pin
SBJ20-Nov-08 6:48
SBJ20-Nov-08 6:48 

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.