How To Unit Test Avalon / Windows Presentation Foundation

WindowRunner helps you launch a window on a separate thread and then stuff your content into it:

namespace

UnitTestingAvalon
{
    using System;

    public class WindowRunner
    {
#region Constructor
        internal WindowRunner(RetrieveWindow windowCallback, CreateContent contentCreator, object contentInformation)
{
if (windowCallback == null) { throw new ArgumentNullException("windowCallback"); }
this.WindowCallback = windowCallback;

            if (contentCreator == null) { throw new ArgumentNullException("contentCreator"); }
this.ContentCreator = contentCreator;
this.ContentInformation = contentInformation;
}
#endregion Constructor

        #region WindowCallback
internal delegate void RetrieveWindow(System.Windows.Window Window);
private RetrieveWindow WindowCallback { get { return this.windowCallback; } set { this.windowCallback = value; } }
private RetrieveWindow windowCallback;
#region WindowCallback

        #region ContentCreator
internal delegate System.Windows.FrameworkElement CreateContent(object contentInformation);
private CreateContent ContentCreator { get { return this.contentCreator; } set { this.contentCreator = value; } }
private CreateContent contentCreator;
#endregion ContentCreator

#region ContentInformation
private object ContentInformation { get { return this.contentInformation; } set { this.contentInformation = value; } }
private object contentInformation;
#endregion ContentInformation

#region Run
internal void Run()
{
System.Windows.Window avalonWindow = new System.Windows.Window();
avalonWindow.SizeToContent = System.Windows.SizeToContent.WidthAndHeight;
avalonWindow.Content = this.ContentCreator(this.ContentInformation);
this.WindowCallback(avalonWindow);
avalonWindow.ShowDialog();
}
#endregion Run
}
}

The content creator method might look like this:

protected System.Windows.FrameworkElement CreateSurveyorControlFor(object surveyDefinition)
{
    Surveyor surveyorControl = new Surveyor();
return surveyorControl;
}

ApplicationUnderTest drives all this:

namespace

UnitTestingAvalon
{
using System;
using System.Threading;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Threading;
internal static class ApplicationUnderTest
{
#region ApplicationThread
private static Thread ApplicationThread { get { return ApplicationUnderTest.applicationThread; } set { ApplicationUnderTest.applicationThread = value; } }
private static Thread applicationThread;
#endregion ApplicationThread

#region HostWindow
internal static Window HostWindow { get { return ApplicationUnderTest.hostWindow; } set { ApplicationUnderTest.hostWindow = value; } }
private static Window hostWindow = null;
#endregion HostWindow

        #region Initialize / Deinitialize
internal static void Initialize(WindowRunner.CreateContent contentCreator, object contentInformation)
{
ApplicationUnderTest.CreateHostWindow(contentCreator, contentInformation);
}

        internal static void Deinitialize()
{
if (ApplicationUnderTest.HostWindow != null)
{
ApplicationUnderTest.Invoke(delegate { ApplicationUnderTest.HostWindow.Close(); });
ApplicationUnderTest.HostWindow = null;
}

ApplicationUnderTest.ApplicationThread.Join();
ApplicationUnderTest.ApplicationThread = null;
}
#endregion Initialize

#region Avalon Invoke
// Avalon requires all interaction with UI to happen on the UI's thread. This invoke simplifies doing so.
internal delegate void DispatcherInvokeCallback();
internal static void Invoke(DispatcherInvokeCallback callback)
{
ApplicationUnderTest.HostWindow.Dispatcher.Invoke(DispatcherPriority.Normal, callback);
}
#endregion Avalon Invoke

#region WaitForIdle
private static void WaitForIdle(int timeout)
        {
if (timeout <= 0)
{
throw new ArgumentException("The timeout specified must be greater than zero.");
}

DispatcherOperationCallback operationCallback = new DispatcherOperationCallback(delegate { return null; });
ApplicationUnderTest.HostWindow.Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, new TimeSpan(0, 0, timeout), operationCallback, null);
}
#endregion WaitForIdle

#region CreateHostWindow
private static void CreateHostWindow(WindowRunner.CreateContent contentCreator, object contentInformation)
{
            WindowRunner windowRunner = new WindowRunner(ApplicationUnderTest.RetrieveWindow, contentCreator, contentInformation);
ApplicationUnderTest.ApplicationThread = new System.Threading.Thread(new System.Threading.ThreadStart(windowRunner.Run));
// Avalon requires UI to run on STA threads.
ApplicationUnderTest.ApplicationThread.SetApartmentState(System.Threading.ApartmentState.STA);
ApplicationUnderTest.ApplicationThread.Start();
// Give up the rest of our timeslice so the application's thread gets scheduled. This is required on uniprocessor machines.
Thread.Sleep(0);
}
        #endregion CreateHostWindow

#region RetrieveWindow
private static void RetrieveWindow(System.Windows.Window window)
{
ApplicationUnderTest.HostWindow = window;
}
#endregion RetrieveWindow
}
}

So here's the flow:

  1. Your Test Method Setup calls ApplicationUnderTest.Initialize, providing the method that will create your UI as well as any random information that method requires.
  2. ApplicationUnderTest.Initialize calls ApplicationUnderTest.CreateHostWindow.
  3. ApplicationUnderTest.CreateHostWindow creates a new WindowRunner instance and starts it running on a separate thread.
  4. WindowRunner.Run creates a new Avalon.Window.
  5. WindowRunner.Run stuffs the return value from your UI creation method into the Window.
  6. WindowRunner.Run calls back into ApplicationUnderTest to give it a reference to the Window.
  7. WindowRunner.Run shows the Window modally.
  8. ApplicationUnderTest.CreateHostWindow blocks until the Window appears (not shown here).
  9. ApplicationUnderTest.CreateHostWindow and then .Initialize return.
  10. Your test case executes.
  11. Your Test Method Teardown calls ApplicationUnderTest.Deinitialize.
  12. ApplicationUnderTest.Deinitialize closes the Window.
  13. ApplicationUnderTest.Deinitialize blocks until the Window's thread exits.
  14. ApplicationUnderTest.Deinitialize returns.