Click here to Skip to main content
Click here to Skip to main content
Go to top

MVVM # Episode 2

, 3 Dec 2013
Rate this:
Please Sign up or sign in to vote.
Using an extended MVVM pattern for real world LOB applications: Part 2
Other Articles In This Series:
Part One
Part Two
Part Three
Part Four  

Introduction

In the first article in this series, I looked at some of the issues I was having with the implementation of MVVM in a WPF application, and suggested some improvements to existing frameworks.

In this article, I will begin a walkthrough creating an MVVM application (albeit a small one) using the techniques I touched on last time.

In the third article, I'll add sufficient meat to the bones created in article 2 to give us a running application, albeit one that doesn't do too much.

In the fourth article, I'll finish off the application to show a (small) but functioning application demonstrating some of the functions available. 

We'll just create the generic pre-requisites here, the 'framework' if you like. Next time, we'll continue with the actual project.

Just because one has to give these things a name, I thought I'd call this MVVM#. mainly because MVMVDCV looked too much like a roman date Wink | <img src= " src="http://www.codeproject.com/script/Forums/Images/smiley_wink.gif" />

Pre-requisites and Caveats

I'm building this using VS2010 and C#, .NET 4.0. It should be translatable to VB.NET.

I'm NOT using TDD here. Although one of the advantages of MVVM is the enhanced testability of your application, I'm steering clear of both to keep the article short, and to avoid revealing my lack of knowledge on the subject of TDD!

I'm assuming familiarity with VS2010 and C# - so no 'Click this, drop down that" however, I do try not to make assumptions, so even novice users should be able to follow along - and the source of the completed application is available for download.

Specification

I wanted this to be fairly realistic within the constraints of an article - so here's the spec:

  • We want the user to be able to see a list of Customers.
  • They need to be able to filter the list by State.
  • They need to select a Customer and edit their details.
  • They need to be able to save their changes.

Well, that's the sort of spec I'm sure many of you are used to! Obviously, we'll have to make lots of assumptions, and design decisions ourselves - but that's all good; we're going to be agile and show the users where we are, frequently, and re-engineer as necessary.

Let's Get Started!

Create a new WPF C# application. I'm calling mine "CustomerMaintenance". We don't want the default MainWindow - so delete it.

Now, we'll create all of the projects we're going to need. Because each of the things we're dealing with should be independent of one another, I like to have each in its own project - that way cross-contamination is harder, easily documented by the 'references' in each project, and I can split development amongst multiple developers more easily if I need to. So, create new Class Library projects as below:

  • Controllers
  • Messengers
  • Models
  • Services
  • ViewModels

We also want to create a project for our Views. This should not be a Class Library, but a WPF User Control Library.

You can either delete the default classes created, or just rename them when we get around to creating them.

Your VS Solution explorer should look like this.

Solution Explorer

Solution Explorere Projects created

Foundation Classes

We're going to be using messaging to communicate around our application. We could use events (so long as we're very careful to remove handlers when classes are disposed). In this implementation, I'm using a Messenger class derived from the MVVM Foundation classes. The Messenger class source code is part of the download for this article. It's a utility class that would be used in all my MVVM projects as-is.

The Messenger class is in its own project (Messengers) so I can keep it independent.

There's a real problem with the implementation of some Messenger systems that can cause unexpected behaviour, and be really difficult to track down. to describe it, imagine this scenario:

ViewModels VMA and VMB are both instantiated. VMA sends messages to which VMB subscribe, but the two know nothing about one another. When VMB handles the message, it performs some function (writes to the database, for example).

VMA sends the message, VMB receives it, writes to the DB and all is good. Now the user closed VMB (if it's in a window, maybe they close the window) so the system removes all 'Strong' references to VMB. There is still a weak reference to it - in the Messenger - but that won't stop it being Garbage Collected, so all is good. Of course, because VMA knows nothing about what's happening with VMB it continues to send the message - after all, other ViewModels may subscribe to the message too...

Now, if you recall Garbage Collection 101, the Garbage Collector doesn't necessarily remove items from memory immediately they are free of all hard references - the Garbage Collector runs when it goddamn feels like it. so if you have plenty of memory available, especially if VMB is small, it's still sitting there. Then VMA sends the message. You can see where this is going, can't you? Yep - VMB STILL RECEIVES THE MESSAGE even though there's no references to it! And it still writes merrily away to the database! Even though the user closed it!

The answer to this is for VMB to unsubscribe from the message before it is closed. But wait, wasn't part of the reason for using weak references that we wouldn't have to worry about the memory leaks associated with forgetting to remove event handlers? (yes it was!) But now we still have to remember to remove message handlers or we risk unexpected behaviour which may actually be harder to track down than memory leaks!

What I have implemented to try to overcome this issue, is a Deregister method, that takes a ViewModel as a parameter, and removes all Message subscriptions to it. I then call this method from my Base ViewModel class in a CloseViewModel method.

I've also added two enumerations to the source, MessageHandledStatus and NotificationResult.

The first, MessageHandledStatus, was added to allow the message handlers (our View Models) to communicate back to the Messenger system.

The default value (NotHandled) tells the system that the ViewModel hasn't handled the message (so the Messenger should keep sending it to any other ViewModel registered as a recipient).

If a ViewModel sets the value to HandledContinue, this tells the Messenger to continue sending out the message, but on completion it will know that something has handled the message.

The HandledCompleted value tells the Messenger not to send the message out to any further recipients as it has been handled

Finally, the NotHandledAbort message tells the Messenger that although the message has not been handled, it should not send it to further recipients.

The NotificationResult enumeration is used to return a some information to the ViewModel that sends the message. This can be used, for example, to instantiate a new ViewModel to handle some event if there are currently no handlers registered to handle it.

My version of the messenger class also uses a class, Message, which is carried around with the message. The class looks like this...

Message.cs

namespace Messengers
{
    public class Message
    {
        #region Public Properties
        /// <summary>
        /// Has the message been handled
        /// </summary>
        public MessageHandledStatus HandledStatus
        {
            get;
            set;
        }
        /// <summary>
        /// What type of message is this
        /// </summary>
        private MessageTypes messageType;
        public MessageTypes MessageType
        {
            get
            {
                return messageType;
            }
        }
        /// <summary>
        /// The payload for the message
        /// </summary>
        public object Payload
        {
            get;
            set;
        }
        #endregion
        #region Constructor
        public Message(MessageTypes messageType)
        {
            this.messageType = messageType;
        }
        #endregion
    }
}

This Message object is passed around with every message - so every message handler has the opportunity to look at, or modify, the HandledStatus and look at the MessageType. This, for example, allows a single message handler to cater for many different message types, and to set the HandledStatus appropriately.

The Message object also contains a 'Payload'. This is some object you want passed around with that message, so when we have saved a Customer, for example, we send a message using:

 Messenger.NotifyColleagues(MessageTypes.MSG_CUSTOMER_SAVED, data);

where the data passed is the CustomerEditViewData, which contains all the information just updated - so if some ViewModel somewhere wants to act, it already has the information at its fingertips, so to speak.

We're also going to be using two other classes from the MVVMFoundation project - namely ObservableObject and RelayCommand. Both of these I create in the ViewModels project in a folder called BaseClasses, as all ViewModel and ViewData classes derive from ObservableObject, and ViewModels are the RelayCommand handlers.

You will also need to add a reference in the ViewModels project, to PresentationCore.

To complete this section, we should add our enumeration for the messages. So add a new file to the Messengers project, called Messages...

MessageTypes.cs

namespace Messengers
{
    /// <summary>
    /// Use an enumeration for the messages to ensure consistency.
    ///
    /// </summary>
    public enum MessageTypes
    {
        MSG_CUSTOMER_SELECTED_FOR_EDIT,// Sent when a Customer is selected for editing
        MSG_CUSTOMER_SAVED		// Sent when a Customer is updated to the repository
    };
}

These are the only two messages our application is going to handle - so adding them now is no problem. In a larger application, we'd be adding new messages as the functionality is specified - it's a good place to go to ensure the functionality provided matches the requirements.

This, if you like, is the basic 'framework' for my MVVM# application. You can, of course, use any implementation you like of the Mediator pattern (our Messenger class). The ObservableObject and RelayCommand classes can also be replaced with some other version providing similar functionality.

ViewModel

The other bit-players in our scenario can now get created too.

We're using a Controller to manage the application - so let's create its interface. Again, this can go in the BaseClasses folder in the ViewModels project. You can see that the interface for the base controller just specifies that it has a Messenger property.

IController.cs

using Messengers;
namespace ViewModel
{
	public interface IController
	{
		Messenger Messenger
		{
			get;
		}
	}
}

Now, we also need base classes for our ViewData and ViewModel classes. Here, some controversy creeps into my implementation. We're going to declare an IView interface for use in our ViewModel.

What!!! I can hear the gasps! Our ViewModels shouldn't know anything about our Views! Well, I live in the real world. I need to be able to tell the Views to activate themselves and to close themselves. More accurately, I need to be able to tell a View when its ViewModel is closing, or when its ViewModel is activating - so giving the View the option to handle these events.

You'll see that is all the IView interface is - definitions of a couple of methods to allow the views to hook into events raised by the ViewModel. It additionally specifies the DataContext property - which every view, as a descendent of UserControl, will have anyway. You'll see this property used in the ViewModel constructor.

IView.cs

namespace ViewModel
{
    public interface IView
    {
        void ViewModelClosingHandler(bool? dialogResult);
        void ViewModelActivatingHandler();
        object DataContext{get;set;}
    }
}

The BaseViewdata is about as simple a class as you could want...

BaseViewData.cs

namespace ViewModels
{
	/// <summary>
	/// The base class from which all View Data objects inherit.
	/// Just an Observable Object right now - 
         /// but a separate abstract class in case we want to add
	/// to it while not modifying ObservableObject itself.
	/// </summary>
	public abstract class BaseViewData : ObservableObject
	{
	}
}

The BaseViewModel source also declares the two delegates that use the methods defined in the IView interface. We also keep, in the BaseViewModel, a collection of Child BaseViewModels. Keeping this list allows each ViewModel to ensure that each of its children unsubscribe from all of their messages (and release any other resources) when the 'parent' is being 'closed'. Incidentally, rather than use the name "Parent" for variables I have used "daddy" - to avoid any possible confusion with other uses of the Parent name. Those of a feminist bent, feel free to rename this 'mummy'.

The BaseViewModel contains a BaseViewData property. The BaseViewData is business data bound to the View, while any other properties of the ViewModel that may be bound by the View provide functionality rather than just data.

In the constructor, a ViewModel is passed an IController and IView reference. This is logical - every ViewModel is going to require a Controller to service it, and a View to provide a GUI. You can see that the constructor is where the methods defined in the IView are wired up to the Event Handlers defined in the BaseViewModel - and you can take note that the ViewModel does not retain any other reference to the View.

Finally two methods, CloseViewModel and ActivateViewModel, are provided.

BaseViewModel.cs

using System.Collections.Generic;

namespace ViewModel
{
    /// <summary>
    /// When the VM is closed, the associated V needs to close too
    /// </summary>
    /// <param name="sender"></param>
    public delegate void ViewModelClosingEventHandler(bool? dialogResult);
    /// <summary>
    /// When a pre-existing VM is activated the View needs to activate itself
    /// </summary>
    public delegate void ViewModelActivatingEventHandler();
    /// <summary>
    /// A base class for all view models
    /// </summary>
    public abstract class BaseViewModel : ObservableObject
    {
        public event ViewModelClosingEventHandler ViewModelClosing;
        public event ViewModelActivatingEventHandler ViewModelActivating;

        /// <summary>
        /// Keep a list of any children ViewModels so we can safely 
        /// remove them when this ViewModel gets closed
        /// </summary>
        private List<BaseViewModel> childViewModels = new List<BaseViewModel>();
        public List<BaseViewModel> ChildViewModels
        {
            get { return childViewModels; }
        }

        #region Bindable Properties

        #region ViewData
        private BaseViewData viewData;
        public BaseViewData ViewData
        {
            get
            {
                return viewData;
            }
            set
            {
                if (value != viewData)
                {
                    viewData = value;
                    base.RaisePropertyChanged("ViewData");
                }
            }
        }
        #endregion
        #endregion
        #region Controller
        /// <summary>
        /// If the ViewModel wants to do anything, it needs a controller
        /// </summary>
        protected IController Controller
        {
            get;
            set;
        }
        #endregion
        #region Constructor
        /// <summary>
        /// Parameterless Constructor required for support of DesignTime 
        /// versions of View Models
        /// </summary>
        public BaseViewModel()
        {
        }

        /// <summary>
        /// A view model needs a controller reference
        /// </summary>
        /// <param name="controller"></param>
        public BaseViewModel(IController controller)
        {
            Controller = controller;
        }

        /// <summary>
        /// Create the View Model with a Controller and a FrameworkElement (View) injected.
        /// Note that we do not keep a reference to the View - 
        /// just set its data context and
        /// subscribe it to our Activating and Closing events...
        /// Of course, this means there are references - 
        /// that must be removed when the view closes,
        /// which is handled in the BaseView
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="view"></param>
        //public BaseViewModel(IController controller, FrameworkElement view)
        public BaseViewModel(IController controller, IView view)
            : this(controller)
        {
            if (view != null)
            {
                view.DataContext = this;
                ViewModelClosing += view.ViewModelClosingHandler;
                ViewModelActivating += view.ViewModelActivatingHandler;
            }
        }
        #endregion
        #region public methods
        /// <summary>
        /// De-Register the VM from the Messenger to avoid non-garbage 
        /// collected VMs receiving messages
        /// Tell the View (via the ViewModelClosing event) that we are closing.
        /// </summary>
        public void CloseViewModel(bool? dialogResult)
        {
            Controller.Messenger.DeRegister(this);
            if (ViewModelClosing != null)
            {
                ViewModelClosing(dialogResult);
            }
            foreach (var childViewModel in childViewModels)
            {
                childViewModel.CloseViewModel(dialogResult);
            }
        }

        public void ActivateViewModel()
        {
            if (ViewModelActivating != null)
            {
                ViewModelActivating();
            }
        }
        #endregion
    }
}

Controller

Because our Controllers will have some common functionality, we'll use a BaseController class from which our controller(s) will inherit. In this case, the only common functionality, in fact, is a reference to the singleton instance of our Messenger class, as defined in the IController interface.

So we just need to create a new class in the Controllers project, called BaseController. As usual, I create it in a folder called Base Classes.

BaseController.cs

using Messengers;
using ViewModel;

namespace Controllers
{
    /// <summary>
    /// The base controller class.
    /// </summary>
    public abstract class BaseController : IController
    {
        /// <summary>
        /// Retain a reference to the single instance of the 
        /// Messenger class for convenience
        /// as it means we can use Controller.Messenger.blah rather than 
        /// Controller.Messenger.Instance.blah
        /// In a large system this also allows us to use multiple Messengers 
        /// (e.g. for different parts of a system
        /// that have no need to communicate between them) 
        /// by making a single change here to return a different Messenger
        /// </summary>
        public Messenger Messenger
        {
            get
            {
                return Messenger.Instance;
            }
        }
    }
}

View

In our Views project, we need to create two items. Firstly, we're going to create a Window. This window will be used by our Views when we want to show them in a window - as you'll see later.

Because we're creating base classes (well, the Window isn't actually a base class, but it sort of fits the idea) I create a folder called Base Classes, then create within it a new Window called ViewWindow.

Because when we display our views, we need to put them on some surface, we'll add a DockPanel to the window. This is the surface on which all of our views will be placed when shown in a window. It must be named WindowDockPanel. Note that you can 'pretty up' your window as much as you like - just so long as it has a WindowDockPanel. (and you can change that functionality if you want, by changing the code that puts Views onto the window - it's all defined in Base classes, so the implementation can be changed to suit your preferences.)

Next we create the BaseView class.

BaseView.cs

using System;
using System.Windows;
using System.Windows.Controls;
using ViewModels;

namespace Views
{
	/// <summary>
	/// A delegate to allow the window closed event to be handled (if required)
	/// </summary>
	/// <param name="o"></param>
	/// <param name="e"></param>
	public delegate
        void OnWindowClose(
        Object sender, EventArgs e);

	/// <summary>
	/// This is the basis of all views.
	/// It cannot be Abstract because of design time issues when 
	/// it tries to instantiate this class.
	/// Note that this 'view' doesn't have any XAML 
         /// (because you can't inherit XAML)
	/// </summary>
	public partial class BaseView : UserControl, IDisposable, IView
	{
		private ViewWindow viewWindow;
        		// If shown on a window, the window in question
		private OnWindowClose onWindowClosed =  null;

		#region Closing

		/// <summary>
		/// The view is closing, so clean up references
		/// </summary>
		public void ViewClosed()
		{
			// In order to handle the case where the 
			// user closes the window 
			// (rather than us controlling the close via a ViewModel)
			// we need to check that the DataContext is not null 
			// (which would mean this ViewClosed has already been done)
			if (DataContext != null)
			{
				((BaseViewModel)DataContext).ViewModelClosing -= 
						ViewModelClosingHandler;
				((BaseViewModel)DataContext).ViewModelActivating -= 
						ViewModelActivatingHandler;
				this.DataContext = null; // Make sure we don't 
					// have a reference to the VM any more.
			}
		}

		/// <summary>
		/// Handle the Window Closed event
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void ViewsWindow_Closed(object sender, EventArgs e)
		{
			if (onWindowClosed != null)
			{
				onWindowClosed(sender, e);
			}
			((BaseViewModel)DataContext).CloseViewModel(false);
		}
		#endregion

		#region IView implementations
		/// <summary>
		/// Tell the View to close itself. Handle the case 
		/// where we're in a window and the window needs closing.
		/// </summary>
		/// <param name="dialogResult"></param>
		public void ViewModelClosingHandler(bool? dialogResult)
		{
			if (viewWindow == null)
			{
				System.Windows.Controls.Panel panel =
        				this.Parent as System.Windows.Controls.Panel;
				if (panel != null)
				{
					panel.Children.Remove(this);
				}
			}
			else
			{
				viewWindow.Closed -= ViewsWindow_Closed;

				if (viewWindow.IsDialogWindow)
				{
					// If the window is a Dialog and is not 
					// active it must be in the process of 
					// being closed
					if (viewWindow.IsActive)
					{
						viewWindow.DialogResult = 
							dialogResult;
					}
				}
				else
				{
					viewWindow.Close();
				}
				viewWindow = null;
			}
			// Process the ViewClosed method to cater for if this has 
			// been instigated by the user closing a window, 
			// rather than by
			// the close being instigated by a ViewModel
			ViewClosed();
		}

		public void ViewModelActivatingHandler()
		{
			if (viewWindow != null)
			{
				viewWindow.Activate();
			}
		}
		#endregion

		#region Constructor
		public BaseView()
		{
		}
		#endregion
		#region Window
		/// <summary>
		/// The Window on which the View is displayed 
		/// (if it is displayed on a Window)
		/// The Window will be created by the View on demand 
		/// (if required) or may be
		/// supplied by the application.
		/// </summary>
		private ViewWindow ViewWindow
		{
			get
			{
				if (viewWindow == null)
				{
					viewWindow = new ViewWindow();
					viewWindow.Closed += ViewsWindow_Closed;
				}
				return viewWindow;
			}
		}
		#endregion

		#region Showing methods
		/// <summary>
		/// Show this control in a window, sized to fit, with this title
		/// </summary>
		/// <param name="windowTitle"></param>
		public void ShowInWindow(bool modal, string windowTitle)
		{
			ShowInWindow(modal, windowTitle, 0, 0, Dock.Top, null);
		}

		/// <summary>
		/// Show this control in an existing window, by default docked top.
		/// </summary>
		/// <param name="window"></param>
		public void ShowInWindow(bool modal, ViewWindow window)
		{
			ShowInWindow(modal, window, window.Title, 
				window.Width, window.Height,
        				Dock.Top, null);
		}

		/// <summary>
		/// Maximum Flexibility of Window Definition version of Show In Window
		/// </summary>
		/// <param name="window">The Window in which to show this View</param>
		/// <param name="windowTitle"> A Title for the Window</param>
		/// <param name="windowWidth">The Width of the Window</param>
		/// <param name="windowHeight">The Height of the Window </param>
		/// <param name="dock">How should the View be Docked </param>
		/// <param name="onWindowClosed">Event handler for when the window 
                  /// is closed </param>
		public void ShowInWindow(
       			 bool modal, ViewWindow window,
       			 string windowTitle, double windowWidth,
       			 double windowHeight,
      			 Dock dock, OnWindowClose onWindowClose)
		{
			this.onWindowClosed = onWindowClose;

			viewWindow = window;
			viewWindow.Title = windowTitle;

			DockPanel.SetDock(this, dock);
			// The viewWindow must have a dockPanel 
			// called WindowDockPanel. 
			// If you want to change this to use some 
			// other container on the window, then
			// the below code should be the only place 
			// it needs to be changed.
			viewWindow.WindowDockPanel.Children.Add(this);

			if (windowWidth == 0 && windowHeight == 0)
			{
				viewWindow.SizeToContent = 
					SizeToContent.WidthAndHeight;
			}
			else
			{
				viewWindow.SizeToContent = SizeToContent.Manual;
				viewWindow.Width = windowWidth;
				viewWindow.Height = windowHeight;
			}

			if (modal)
			{
				viewWindow.ShowDialog();
			}
			else
			{
				viewWindow.Show();
			}
		}

		/// <summary>
		/// Show the View in a New Window
		/// </summary>
		/// <param name="windowTitle">Give the Window a Title</param>
		/// <param name="windowWidth">Set the Window's Width</param>
		/// <param name="windowHeight">Set the Window's Height</param>
		/// <param name="dock">How to Dock the View in the Window</param>
		/// <param name="onWindowClosed">Event handler for 
		/// when the Window closes</param>
		public void ShowInWindow(
        			bool modal, string windowTitle,
        			double windowWidth, double windowHeight,
        			Dock dock, OnWindowClose onWindowClose)
		{
			ShowInWindow(modal, ViewWindow, windowTitle, 
			windowWidth, windowHeight, dock, onWindowClose);
		}
		#endregion
		#region IDisposable Members

		void IDisposable.Dispose()
		{
			// Remove any events from our window to prevent any 
                     	// memory leakage.
			if (viewWindow != null)
			{
				viewWindow.Closed -= this.ViewsWindow_Closed;
			}
		}

		#endregion
	}
}

You'll need to add a reference to the ViewModel project in order for this to compile.

This is a reasonably basic BaseView - there's a few different methods that can be used for showing our View, but the list is not complete by any means. I've tried to give the basic requirements in this version. It is certainly open to expansion. You will also see the implementation of our event handlers for when the ViewModel closes, and when it is Activated.

End of the Second Part

We've now completed the project up to the point where we need to start creating application specific code. In other words, we've created our framework - but I really don't want to use that word - I don't see this as a framework, but just as a bunch of classes, put together to allow me to develop a WPF MVVM# application.

The application should build - if you're typing it in rather than downloading it, check your namespaces, as VS does tend to add folder names to namespaces just to annoy me.

Next time, we'll start developing the application proper - and finally have something to run.

License

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

Share

About the Author

_Maxxx_
Software Developer (Senior) Devo
Australia Australia
Software developer par excellence,sometime artist, teacher, musician, husband, father and half-life 2 player (in no particular order either of preference or ability)
Started programming aged about 16 on a Commodore Pet.
Self-taught 6500 assembler - wrote Missile Command on the Pet (impressive, if I say so myself, on a text-only screen!)
Progressed to BBC Micro - wrote a number of prize-winning programs - including the best graphics application in one line of basic (it drew 6 multicoloured spheres viewed in perspective)
Trained with the MET Police as a COBOL programmer
Wrote platform game PooperPig which was top of the Ceefax Charts for a while in the UK
Did a number of software dev roles in COBOL
Progressed to Atari ST - learned 68000 assembler & write masked sprite engine.
Worked at Atari ST User magazine as Technical Editor - and was editor of Atari ST World for a while.
Moved on to IBM Mid range for work - working as team leader then project manager
Emigrated to Aus.
Learned RPG programming on the job (by having frequent coffee breaks with the wife!!)
Moved around a few RPG sites
Wrote for PC User magazine - was Shareware Magazine editor for a while.
Organised the first large-scale usage of the Internet in Australia through PC User magazine.
Moved from RPG to Delphi 1
Developed large applications in Delphi before moving on to VB .Net and C#
Became I.T. Manager - realised how boring paper pushing can be
And now I pretty much do .Net development in the daytime, while redeveloping PooperPig for the mobile market at night.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberschugol7-Apr-11 4:30 
GeneralRe: My vote of 5 Pinmember_Maxxx_7-Apr-11 17:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    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 | Mobile
Web03 | 2.8.140926.1 | Last Updated 3 Dec 2013
Article Copyright 2011 by _Maxxx_
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid