Click here to Skip to main content
15,860,844 members
Articles / Desktop Programming / WPF

Catel - Part 4 of n: Unit testing with Catel

Rate me:
Please Sign up or sign in to vote.
4.55/5 (10 votes)
28 Jan 2011CPOL11 min read 48.5K   572   11   10
This article explains how to write unit tests for MVVM using Catel.

Catel is a brand new framework (or enterprise library, just use whatever you like) with data handling, diagnostics, logging, WPF controls, and an MVVM Framework. So, Catel is more than "just" another MVVM Framework or some nice Extension Methods that can be used. It's more like a library that you want to include in all the (WPF) applications you are going to develop in the near future.

This article explains how to write unit tests for MVVM using Catel.

Article Browser

Table of Contents

1. Introduction

Welcome to part 4 of the articles series about Catel. This article explains how to write unit tests for MVVM using Catel.

If you haven’t read the previous article(s) of Catel yet, it is recommended that you do. They are numbered so finding them shouldn’t be too hard.

I must admit that some parts of this article are greatly inspired by the article about unit testing in Cinch by Sacha Barber, which actually led me to writing an article about unit testing in Catel at all.

If you are wondering why you should even write unit tests, you should also wonder why you aren’t still living in a cage and writing stuff on the walls. Writing unit tests makes sure that you don’t break existing functionality in an application when making changes. This lowers the cost of QA (since you don’t need a technical tester executing regression tests all the time). I don’t say that a tester isn’t needed; my opinion is that at least someone else besides the developer should take a human look at the software before it is even released. If you are still not convinced why you should write unit tests, please go back to your cave and stop reading this article for the sake of your own pleasure.

During this article, you will notice how easy it is to write unit tests for your View Models. It might even be fun, although I doubt that there are a lot of developers that find writing unit tests fun. At the end of this article, we will write unit tests for a “real-world” application so this article isn’t too abstract to read.

This article does not cover the basics of unit testing. It assumes that you already know what unit tests are, and how to write them. This article is specifically written to explain how View Models of the MVVM pattern can be unit tested, especially with the use of Catel.

2. Testing Commands

Thanks to commands (which implement the ICommand interface), testing View Models and UI logic has never been so easy. Now commands can be invoked programmatically without having to automate the UI; it is very simple to reproduce a button click during a unit test.

When testing commands, it is very important to test the state as well. The command implementation of Catel has a CanExecute and an Execute method which can be invoked manually. Therefore, it is very easy to test a command. The code below shows a test that checks whether the Remove command can be executed. At first, the command cannot be executed because there is no selected person. After the selected person is set, the command should be able to execute:

C#
Assert.IsFalse(mainWindowViewModel.Remove.CanExecute(null));
mainWindowViewModel.SelectedPerson = mainWindowViewModel.PersonCollection[0];
Assert.IsTrue(mainWindowViewModel.Remove.CanExecute(null));

To execute a command in code, one should use the following code:

C#
mainWindowViewModel.Remove.Execute(null);

3. Testing Services

Catel uses several services that are exposed to the ViewModelBase class. These services are registered by interface, so can be different for each View Model. This chapter explains how the services can be used during unit tests.

By default, the ViewModelBase registers the right service implementations (either the WPF or Silverlight specific implementations). However, during unit tests, you won’t be there to click the confirmation buttons in a message box. Therefore, Catel also offers unit test implementations of all the services.

3.1. IoC containers

Catel uses Unity to implement IoC containers. IoC containers make it possible to change dependencies at runtime by configuration without actually having to change the code or even recompiling. To enable the unit test implementations for a unit test project, we have to create an App.config file (if it doesn’t already exist) and use the following content. Note that if you already have a configuration file, do not completely overwrite the configuration file, but merge them.

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="unity" 
      type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, 
            Microsoft.Practices.Unity.Configuration" />
  </configSections>
  
  <unity>
    <containers>
      <container>
        <types>
          <type type="Catel.MVVM.Services.IMessageService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.MessageService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IOpenFileService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.OpenFileService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IPleaseWaitService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.PleaseWaitService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.ISaveFileService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.SaveFileService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IUIVisualizerService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.UIVisualizerService, Catel.Windows"/>
        </types>
      </container>
    </containers>
  </unity>
</configuration>

The code above maps all the known interfaces to the test implementations of the services. Unity will make sure that the right services are injected into the View Models. As soon as a View Model instance is created in the test project (don’t forget to initialize it), you can retrieve the test implementation of the service like this:

C#
var mainWindowViewModel = new MainWindowViewModel();
((IViewModel)mainWindowViewModel).Initialize();

var messageService = 
  (MVVM.Services.Test.MessageService)
  mainWindowViewModel.GetService<IMessageService>();

3.2. IMessageService

This service is almost completely empty in the test implementation since most message boxes just contain the OK button, which can be ignored during unit tests. The only methods that actually return a value are the methods that have a return value.

The implementation adds a new property ExpectedResults. This way, a unit test can set an expected result so the service will know what to return on the next call. If there is no queue available, an exception will be thrown since the unit test is not correctly written. Below is the most important part of the service implementation:

C#
public MessageResult Show(string message, string caption, 
                     MessageButton button, MessageImage icon)
{
    if (ExpectedResults.Count == 0)
    {
        throw new Exception(Exceptions.NoExpectedResultsInQueueForUnitTest);
    }

    return ExpectedResults.Dequeue();
}

Now we have discussed the implementation of the service, let’s take a look at how we can use it:

C#
Test.MessageService service = 
   (Test.MessageService)GetService<IMessageService>();

// Queue the next expected result
service.ExpectedResults.Add(MessageResult.Yes);

3.3. IOpenFileService and ISaveFileService

The IOpenFileService and ISaveFileService implementations are the same; therefore they are handled in the same paragraph.

The implementation adds a new property ExpectedResults. This way, a unit test can set an expected result so the service will know what to return on the next call. If there is no queue available, an exception will be thrown since the unit test is not correctly written. Below is the most important part of the service implementation:

C#
public bool DetermineFile()
{
    if (ExpectedResults.Count == 0)
    {
        throw new Exception(Exceptions.NoExpectedResultsInQueueForUnitTest);
    }

    return ExpectedResults.Dequeue().Invoke();
}

Now that we have discussed the implementation of the service, let’s take a look at how we can use it:

C#
Test.OpenFileService service = 
   (Test.OpenFileService)GetService<IOpenFileService>();

// Queue the next expected result
service.ExpectedResults.Add(() =>
                {
                    service.FileName = @"c:\test.txt";
                    return true;
                });

3.4. IPleaseWaitService

This implementation is really simple, and does not have to be automated in the unit tests at all. There is only one method implemented to support unit testing. The code is shown below:

C#
public void Show(PleaseWaitWorkDelegate workDelegate, string status)
{
    // Invoke work delegate
    workDelegate();
}

Instead of showing PleaseWaitWindow and let the window handle the work delegate, the service itself executes the work delegate and then returns.

3.5. IProcessService

The implementation adds a new property ExpectedResults. This way, a unit test can set an expected result so the service will know what to return on the next call. If there is no queue available, an exception will be thrown since the unit test is not correctly written.

However, unlike the other services, this service will only return a result code and invoke the callback in case the process is started successfully. Therefore, an intermediate class ProcessServiceTestResult, which is defined below, is required:

C#
/// <summary>
/// Class representing the process result.
/// </summary>
public class ProcessServiceTestResult
{
    #region Constructor & destructor
    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="ProcessServiceTestResult"/> class,
    /// with <c>0</c> as default process result code.
    /// </summary>
    /// <param name="result">if set to <c>true</c>,
    /// the process will succeed during the test.</param>
    public ProcessServiceTestResult(bool result)
        : this(result, 0) { }

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="ProcessServiceTestResult"/> class.
    /// </summary>
    /// <param name="result">if set to <c>true</c>,
    /// the process will succeed during the test.</param>
    /// <param name="processResultCode">The process result
    /// code to return in case of a callback.</param>
    public ProcessServiceTestResult(bool result, int processResultCode)
    {
        Result = result;
        ProcessResultCode = processResultCode;
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets or sets a value indicating whether the process
    /// should be returned as successfull when running the process.
    /// </summary>
    /// <value><c>true</c> if the process should be returned
    /// as successfull; otherwise, <c>false</c>.</value>
    public bool Result { get; private set; }

    /// <summary>
    /// Gets or sets the process result code.
    /// </summary>
    /// <value>The process result code.</value>
    public int ProcessResultCode { get; private set; }
    #endregion
}

Shown below is the most important part of the service implementation:

C#
public void StartProcess(string fileName, string arguments, 
            ProcessCompletedDelegate processCompletedCallback)
{
    if (string.IsNullOrEmpty(fileName))
    {
        throw new ArgumentException(
          Exceptions.ArgumentCannotBeNullOrEmpty, "fileName");
    }

    if (ExpectedResults.Count == 0)
    {
        throw new Exception(Exceptions.NoExpectedResultsInQueueForUnitTest);
    }

    var result = ExpectedResults.Dequeue();
    if (result.Result)
    {
        if (processCompletedCallback != null)
        {
            processCompletedCallback(result.ProcessResultCode);
        }
    }
}

Now that we have discussed the implementation of the service, let’s take a look at how we can use it:

C#
Test.ProcessService service = (Test.ProcessService)GetService<IProcessService>();

// Queue the next expected result (next StartProcess will
// succeed to run app, 5 will be returned as exit code)
service.ExpectedResults.Add(new ProcessServiceTestResult(true, 5));

3.6. IUIVisualizerService

IUIVisualizerService is the most complex of all, if you can even call it complex. The reason for this is that a separate expected results queue is available for the windows that are shown as a dialog or as a regular window. Below is the code that shows how Show is implemented, but ShowDialog is implemented in the same way:

C#
public bool Show(string name, object data, 
            EventHandler<UICompletedEventArgs> completedProc)
{
    if (ExpectedShowResults.Count == 0)
    {
        throw new Exception(Exceptions.NoExpectedResultsInQueueForUnitTest);
    }

    return ExpectedShowResults.Dequeue().Invoke();
}

Now that we have discussed the implementation of the service, let’s take a look at how we can use it:

C#
Test.UIVisualizerService service = 
   (Test.UIVisualizerService)GetService<IUIVisualizerService>();

// Queue the next expected result
service.ExpectedShowResults.Add(() =>
                {
                    // If required, handle custom data manipulation here
                    return true;
                });

4. Example Project

Now that you have learned all the things you need to know about unit testing in Catel, it’s time to bring it into practice. As an example, a person application is created. It’s a very simple application which does nothing more than adding, modifying, and removing persons in a list. But, as simple as the application might seem, it contains most aspects that need to be tested when using MVVM.

In the solution of Catel, we have created two new projects, starting with the name Catel.Articles.04 - Unit testing. The first one is the actual application that is written for this example. The second one is the test project containing unit tests.

4.1. Functionality

The application is fairly simple, and should speak for itself. However, here are some screenshots to give you an idea of what the application is capable of doing.

The main window of the application consists of an items control with all the persons listed underneath each other. At the right are manipulation buttons which should be fairly easy to understand thanks to the images.

Image 1

Note that it is possible to double-click a person item to edit it. This is implemented thanks to the EventToCommand trigger of Catel.

When the Add or Edit button is clicked, the detail window will popup containing some fields to edit the values.

Image 2

As said before, the application is very simple and does nothing useful, but that’s actually the point to keep it as simple as possible. You will probably recognize these kinds of scenarios, and can map them back to your own problems in your own applications.

4.2. Setting up IoC

In the unit test project, we don’t want to use the same services as in real life. For example, why would one want to see a message box during a unit test? Therefore, Catel offers unit test implementations of all View Model services provided by Catel. The actual behavior of the services is described earlier in this article, so we are going to use them, not explain them.

To make sure that the unit test uses the right services, we are going to configure this via the application configuration of the unit test project.

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="unity" 
       type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, 
             Microsoft.Practices.Unity.Configuration" />
  </configSections>
  
  <unity>
    <containers>
      <container>
        <types>
          <type type="Catel.MVVM.Services.IMessageService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.MessageService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IOpenFileService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.OpenFileService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IPleaseWaitService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.PleaseWaitService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.ISaveFileService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.SaveFileService, Catel.Windows"/>
          <type type="Catel.MVVM.Services.IUIVisualizerService, Catel.Core" 
            mapTo="Catel.MVVM.Services.Test.UIVisualizerService, Catel.Windows"/>
        </types>
      </container>
    </containers>
  </unity>
</configuration>

As you can see, we map the interfaces to the test implementations of the services. The Unity framework will take care that the right instance of the services is used during the unit tests.

4.3. Testing Models

Let’s start with the easy one. We have only one Model, and want to test it on a few things, such as validation. We also have an automatic property called FullName, which is just a concatenation of all the known names (first, middle, and last).

First, let’s test the validation of the Model:

C#
[TestMethod]
public void Validation()
{
    Person person = new Person();
    IDataErrorInfo personAsErrorInfo = (IDataErrorInfo) person;

    Assert.IsTrue(person.HasErrors);
    Assert.IsFalse(string.IsNullOrEmpty(personAsErrorInfo[person.GenderProperty.Name]));
    Assert.IsFalse(string.IsNullOrEmpty(personAsErrorInfo[person.FirstNameProperty.Name]));
    Assert.IsTrue(string.IsNullOrEmpty(personAsErrorInfo[person.MiddleNameProperty.Name]));
    Assert.IsFalse(string.IsNullOrEmpty(personAsErrorInfo[person.LastNameProperty.Name]));

    person.Gender = Gender.Male;
    person.FirstName = "John";
    person.LastName = "Doe";

    Assert.IsFalse(person.HasErrors);
    Assert.IsTrue(string.IsNullOrEmpty(personAsErrorInfo[person.GenderProperty.Name]));
    Assert.IsTrue(string.IsNullOrEmpty(personAsErrorInfo[person.FirstNameProperty.Name]));
    Assert.IsTrue(string.IsNullOrEmpty(personAsErrorInfo[person.MiddleNameProperty.Name]));
    Assert.IsTrue(string.IsNullOrEmpty(personAsErrorInfo[person.LastNameProperty.Name]));
}

The code above first tests whether the required validation for the properties is correct when a new instance is created. Then, it changes the required values and checks if the errors disappear.

We also need to test the FullName property:

C#
[TestMethod]
public void FullName()
{
    Person person = new Person();
    Assert.IsTrue(string.IsNullOrEmpty(person.FullName));

    person.FirstName = "Geert";
    Assert.AreEqual("Geert", person.FullName);

    person.MiddleName = "van";
    Assert.AreEqual("Geert van", person.FullName);

    person.LastName = "Horrik";
    Assert.AreEqual("Geert van Horrik", person.FullName);

    person.MiddleName = string.Empty;
    Assert.AreEqual("Geert Horrik", person.FullName);
}

4.4. Testing View Models

Now that we know that our Model is fine (which is very important, the source data must be correct), we can continue with writing unit tests for the View Models. In this article, I will only cover the MainWindowViewModel class. The PersonViewModel class is so straightforward that it is a nice exercise for you to complete.

We first start with testing the initialization of the View Model. During initialization, we expect the View Model to initialize one person, namely me. Below is the unit test:

C#
[TestMethod]
public void Initialization()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.AreEqual("Geert van Horrik", 
      mainWindowViewModel.PersonCollection[0].FullName);
}

The initialization seems to work just great. Let’s focus on the commands. First, we test the Add command. There are two situations we need to test, because the user can either choose OK or Cancel in our modal dialog. If the user clicks Cancel, we don’t want to add the new person:

C#
[TestMethod]
public void AddPerson_Confirmed()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.IsTrue(mainWindowViewModel.Add.CanExecute(null));

    var uiVisualizerService = 
      (MVVM.Services.Test.UIVisualizerService)
      mainWindowViewModel.GetService<IUIVisualizerService>();
    uiVisualizerService.ExpectedShowDialogResults.Enqueue(() => true);
    mainWindowViewModel.Add.Execute(null);

    Assert.AreEqual(2, mainWindowViewModel.PersonCollection.Count);
}

[TestMethod]
public void AddPerson_Canceled()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.IsTrue(mainWindowViewModel.Add.CanExecute(null));

    var uiVisualizerService = (MVVM.Services.Test.UIVisualizerService)
           mainWindowViewModel.GetService<IUIVisualizerService>();
    uiVisualizerService.ExpectedShowDialogResults.Enqueue(() => false);
    mainWindowViewModel.Add.Execute(null);

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
}

Up to the next command, the Edit command. The Edit command also has two situations we need to test. Below are the unit tests:

C#
[TestMethod]
public void EditPerson_Confirmed()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.IsFalse(mainWindowViewModel.Edit.CanExecute(null));
    mainWindowViewModel.SelectedPerson = mainWindowViewModel.PersonCollection[0];
    Assert.IsTrue(mainWindowViewModel.Edit.CanExecute(null));

    var uiVisualizerService = 
      (MVVM.Services.Test.UIVisualizerService)
      mainWindowViewModel.GetService<IUIVisualizerService>();
    uiVisualizerService.ExpectedShowDialogResults.Enqueue(() =>
    {
        mainWindowViewModel.PersonCollection[0].FirstName = "New name";
        return true;
    });
    mainWindowViewModel.Edit.Execute(null);

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.AreEqual("New name", mainWindowViewModel.PersonCollection[0].FirstName);
}

[TestMethod]
public void EditPerson_Canceled()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
    Assert.IsFalse(mainWindowViewModel.Edit.CanExecute(null));
    mainWindowViewModel.SelectedPerson = mainWindowViewModel.PersonCollection[0];
    Assert.IsTrue(mainWindowViewModel.Edit.CanExecute(null));

    var uiVisualizerService = 
      (MVVM.Services.Test.UIVisualizerService)
      mainWindowViewModel.GetService<IUIVisualizerService>();
    uiVisualizerService.ExpectedShowDialogResults.Enqueue(() => false);
    mainWindowViewModel.Edit.Execute(null);

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
}

Until now, it has been fairly easy to write the unit tests, right? We only had to instantiate and initialize a View Model, then set the expected results of the services and check the results. The Remove command is just as simple, but now shows how to use the IMessageService test implementation.

C#
[TestMethod]
public void RemovePerson_ConfirmWithYes()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);

    Assert.IsFalse(mainWindowViewModel.Remove.CanExecute(null));
    mainWindowViewModel.SelectedPerson = mainWindowViewModel.PersonCollection[0];
    Assert.IsTrue(mainWindowViewModel.Remove.CanExecute(null));

    var messageService = (MVVM.Services.Test.MessageService)
          mainWindowViewModel.GetService<IMessageService>();
    messageService.ExpectedResults.Enqueue(MessageResult.Yes);
    mainWindowViewModel.Remove.Execute(null);

    Assert.AreEqual(0, mainWindowViewModel.PersonCollection.Count);
}

[TestMethod]
public void RemovePerson_ConfirmWithNo()
{
    var mainWindowViewModel = new MainWindowViewModel();
    ((IViewModel)mainWindowViewModel).Initialize();

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);

    Assert.IsFalse(mainWindowViewModel.Remove.CanExecute(null));
    mainWindowViewModel.SelectedPerson = mainWindowViewModel.PersonCollection[0];
    Assert.IsTrue(mainWindowViewModel.Remove.CanExecute(null));

    var messageService = (MVVM.Services.Test.MessageService)
      mainWindowViewModel.GetService<IMessageService>();
    messageService.ExpectedResults.Enqueue(MessageResult.No);
    mainWindowViewModel.Remove.Execute(null);

    Assert.AreEqual(1, mainWindowViewModel.PersonCollection.Count);
}

5. Conclusion

This article first explained the unit testing capabilities of Catel and how to use them. Finally, we have written a complete sample application including unit tests for both Models and View Models. I hope that this article shows you how easy it is to write unit tests, and that you should definitely start writing tests for your software.

It also shows a very strong point of the MVVM pattern. If you don’t follow a pattern that creates loosely coupled systems, you will soon notice that it is very hard, if not impossible, to write unit tests for your software and UI logic.

6. History

  • 22 January, 2011: Added the IProcessService explanation
  • 25 November, 2010: Added article browser and a brief introduction summary
  • 23 November, 2010: Some small textual changes
  • 22 November, 2010: Initial version

License

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


Written By
Software Developer
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionExcpetion message Pin
AlanGRutter6-Aug-13 18:11
AlanGRutter6-Aug-13 18:11 
AnswerRe: Excpetion message Pin
Geert van Horrik6-Aug-13 20:38
Geert van Horrik6-Aug-13 20:38 
GeneralRe: Exception message Pin
AlanGRutter7-Aug-13 10:51
AlanGRutter7-Aug-13 10:51 
GeneralRe: Exception message Pin
Geert van Horrik7-Aug-13 20:36
Geert van Horrik7-Aug-13 20:36 
QuestionThis is good, but.... Pin
Pete O'Hanlon17-Aug-11 0:03
subeditorPete O'Hanlon17-Aug-11 0:03 
AnswerRe: This is good, but.... Pin
Geert van Horrik17-Aug-11 3:01
Geert van Horrik17-Aug-11 3:01 
GeneralRe: This is good, but.... Pin
Pete O'Hanlon17-Aug-11 3:19
subeditorPete O'Hanlon17-Aug-11 3:19 
GeneralRe: This is good, but.... Pin
Geert van Horrik17-Aug-11 3:40
Geert van Horrik17-Aug-11 3:40 
GeneralAnother good one Pin
Sacha Barber11-Jan-11 23:01
Sacha Barber11-Jan-11 23:01 
GeneralRe: Another good one Pin
Geert van Horrik11-Jan-11 23:02
Geert van Horrik11-Jan-11 23:02 

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.