Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Component Resources during Automated Test

Tags:

c#

testing

wpf

xaml

I've reached a point where I would like to write an automated test to verify the content of a WPF View that's bound up to a View Model in a particular state.

In concept, its fairly simple. Create a View Model, set its state, create the appropriate View, add the View to a Window, set the Data Context of the View, show the Window, take a screenshot, compare against a previously taken screenshot. This sort of test is useful to detect unintended changes, as well as verifying that all the View can actually be created without error.

However, creating an instance of my View is proving problematic. It requires a set of resources that are not included in the XAML definition itself. These resources are included in the Application level resource dictionary in the actual application, so by the time the View is created in the real application, those resources are already available to it.

When I create an instance of this View inside my test, it throws a XamlParseException about being unable to find various resources (understandably).

I do not want to simply add appropriate resource dictionaries to the XAML definition of the View itself because this would increase the amount of effort (computer effort) required to create one of these View objects, as well as increasing the amount of memory required for each instance. My understanding is that this is a result of ResourceDictionary's not being shared in that way.

I have tried:

  • Creating an instance of App.xaml inside the test (which sets the Application.Current property).
  • Setting the Application.Current.Resources property to the the Resources property of an instance of App.xaml.
  • Setting the Resources property of the Page directly.
  • Setting the Resources property of the Window directly (the Window the Page is being added to for the test).

Essentially, I need to know if there is a way to engineer a situation where I can configure a set of Application resources for standalone instances of WPF components to use inside an automated test.

You can reproduce the problem by creating the following structure, with everything but the View_Test.cs file in one project, and the View_Test.cs file living in a test project. Run the app and it works. Run the test and it fails.

App.xaml

<Application 
    x:Class="Blah.App"        
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Styles.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <SolidColorBrush x:Key="SpecialBrush" Color="Black" />
</ResourceDictionary>

MainWindow.xaml

<Window 
    x:Class="Blah.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Blah="clr-namespace:Blah"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Blah:View/>
    </Grid>
</Window>

View.xaml

<UserControl 
    x:Class="AutomatedTestUserControlApplicationResources.View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    mc:Ignorable="d" 
    d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="{StaticResource SpecialBrush}">

    </Grid>
</UserControl>

View_Test.cs

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Blah;
using System.Windows;

namespace Blah.Test
{
    [TestClass]
    public class View_Test
    {
        [TestMethod]
        public void Test()
        {
            var view = new View();

            var window = new Window();
            window.Content = view;

            window.ShowDialog();
        }
    }
}

Update:

I had some luck with creating an extra constructor for the View in question that takes a ResourceDictionary, as a way to inject the View with some context for its initialization. This constructor overload is only used for tests, in the real application the resource context is already available from the Application resources.

public View(ResourceDictionary resourceContext = null)
{
    if (resourceContext != null) Resources.MergedDictionaries.Add(resourceContext);
    InitializeComponent();
}

This solves the specific example that I posted above in a way that is not dependent on initializing unrelated objects just to get the View to work (which flies in the face of good dependency injection practices).

However, it brought to light some additional problems when I tried to implement it in my actual project. My resource context at the Application level is actually the merge of 4 different resource dictionaries, the latter of which are dependent on the earlier (in that they reference resources specified in an earlier entry).

AppResources.xaml

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Style/GlobalColour.xaml"/>
        <ResourceDictionary Source="Style/GlobalBrush.xaml"/> <!-- Dependent on GlobalColour-->
        <ResourceDictionary Source="Style/GlobalStyle.xaml"/>
        <ResourceDictionary Source="Resources/GlobalContent.xaml"/>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Creating a ResourceDictionary from this file in my test project, and then injecting that ResourceDictionary into my View during construction throws a XamlParseException relating to a StaticResource not being found (the resource that cant be found lives in GlobalBrush, and is dependent on an entry in GlobalColour).

I'll update as I explore further.

Update:

I had absolutely no luck manually creating and using the AppResources ResourceDictionary above. I could not get the interdependencies between dictionaries in the MergedDictionaries to work. I couldn't even manually flatten the ResourceDictionary instance, because when I tried to access a resource in a dictionary that was dependent on a resource in a parallel dictionary it threw a XamlParseException.

As a result, the idea of injecting a ResourceDictionary into the View via a constructor was not feasible for use in my solution (although it works if the app resources are a flat ResourceDictioanry).

At the end of this journey, I have come to the conclusion that the only way to instantiate a View where the xaml does not directly contain references to the resources (without having to instantiate the entire App) is include references to the appropriate ResourceDictionary wherever a resource is used, directly in the xaml. You then have to manage the performance issues at run time (because you're instantiating hundreds of duplicate ResourceDictionaries) by using a SharedResourceDictionary (there are a number of implementations of this concept available on the internet).

like image 917
Todd Bowles Avatar asked Dec 27 '13 08:12

Todd Bowles


1 Answers

This isn't actually all that hard you just need to use Application.LoadComponent to create instances of everything so that the right resources are available at the right time.

The key is to load everything via its XAML rather than creating instances of the classes as the class only contains half the information.

[TestClass]
public class View_Test
{
    [TestMethod]
    public void Test()
    {
        //set initial ResourceAssembly so we can load the App
        Application.ResourceAssembly = Assembly.GetAssembly(typeof (App));

        //load app
        var app = (App) Application.LoadComponent(new Uri("App.xaml", UriKind.Relative));

        //load window and assign to app
        var mainWindow = (Window) Application.LoadComponent(new Uri("MainWindow.xaml", UriKind.Relative));
        app.MainWindow = mainWindow;

        //load view and assign to window content
        var view = (UserControl) Application.LoadComponent(new Uri("View.xaml", UriKind.Relative));
        mainWindow.Content = view;

        //show the window
        mainWindow.Show();
    }
}

Edit: Simpler Version

I just had a look at some disassembled code to see how its done internally and this can be simplified to not require the XAML references. The most important bits to get things going are setting Application.ResourceAssembly and creating the App and calling InitializeComponent on it. The window isn't necessary specifically you could just create a new window to hold the view.

[TestClass]
public class View_Test
{
    [TestMethod]
    public void Test()
    {
        Application.ResourceAssembly = Assembly.GetAssembly(typeof (App));

        var app = new App();
        app.InitializeComponent();

        var mainWindow = new MainWindow();
        app.MainWindow = mainWindow;

        var view = new View();
        mainWindow.Content = view;
        mainWindow.Show();
    }
}
like image 158
David Ewen Avatar answered Oct 15 '22 19:10

David Ewen