Monday, July 30, 2007

Control Templates and UIAutomation don't get along all that well

Off and on I try and use the UIAutomation libraries from .NET 3.0 for a variety of things. Most often it is testing but not always. Since I do more unit testing than anything else I find LogicalTreeHelper, VisualTreeHelper and the AutomationPeer functionality more useful than AutomationElement.
On a recent project we were doing some work on a screen that wasn't particularly easy to get to. It took about 9 clicks. Not that bad but I'm lazy. We were doing a lot of fancy WPF stuff and we really wanted to see things live in the full application context instead of in Blend or just raw xaml. So we would run the app a lot. I got tired of doing the same clicks and occasionally making mistakes just to get the screen to show. So I chose to write a small app that would use UIAutomation to get me to the screen. Seemed simple enough. I've done plenty of work with UIAutomation and I thought it would be easy. Was I in for a suprise.
We had done something clever to a ListBox to get an button to show up at the top of the list box before the items of the list. We had used a control template to get the button in there. Of course there were at least a dozen other ways to do it but that's a different post.
So to start out I did something sane for once and pointed UISpy at the application before I jumped in and started writing code. To my suprise UISpy could not see the button! I tried the Raw view, the content view, custom views, everything. No button.
Then I put some extra code into the application so I could use VisualTreeHelper to see if it could see the button. Sure enough there it was. So what was wrong with UISpy and therefore UIAutomation?
I thought it would be good to try and reproduce the problem in a smaller more isolated way. Then I could be sure it wasn't something else we did. We had a pretty complicated application going so I wanted to reduce the number of variables. I just created a standard WPF application and put this in the cleverly named Window1.xaml

<Window x:Class="WindowsApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:WindowsApplication1="clr-namespace:WindowsApplication1;assembly="
    Title="WindowsApplication1" Height="300" Width="300"
    >
    <Grid>
        <Grid.Resources>
            <Style TargetType="{x:Type WindowsApplication1:SpecialListBox}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type WindowsApplication1:SpecialListBox}">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition/>
                                    <RowDefinition/>
                                </Grid.RowDefinitions>
                                <TextBox x:Name="templateTextBox" Grid.Row="0">Can you see me now?</TextBox>
                                <StackPanel Grid.Row="1">
                                    <ItemsPresenter/>
                                </StackPanel>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
        <WindowsApplication1:SpecialListBox x:Name="specialList">
            <ListBoxItem>one</ListBoxItem>
            <ListBoxItem>two</ListBoxItem>
        </WindowsApplication1:SpecialListBox>
 
    </Grid>
</Window>


There is nothing special about SpecialListBox. It is just a class that derives from ListBox and doesn't override anything, yet. There was also no reason that I switched from Button to TextBox in the smaller code base.
I fired that up, pointed UISpy at it and UISpy could not see TextBox from the control template. It showed the two ListBoxItems just fine. I did some poking around and figured it out eventually.
Since this is a WPF app UIAutomation will talk to AutomationPeers exposed by each WPF element's OnCreateAutomationPeer method. AutomationPeer has a GetChildren method that UISpy and other UIAutomation code will use to navigate the control heirarchy. ListBox will return a ListBoxAutomationPeer instance when its OnCreateAutomationPeer method is called. ListBoxAutomationPeer only returns the list items from its GetChildren implementation. So it will never return the textbox I added to the control template because it doesn't know how.
That seems to put us in a situation where we can't ever get to the textbox. We could refactor and not put the TextBox in the ListBox's ControlTemplate and use a different form of composition like a UserControl. But I thought I'd see if I could get UIAutomation to place nice with the ControlTemplate.
The solution I chose to use is to create my own AutomationPeer for the ListBox. I'm already using a class derived from ListBox anyway so overriding OnCreateAutomationPeer to return my own AutomationPeer is pretty easy. For the custom AutomationPeer itself it seems to make sense to start out by deriving from ListBoxAutomationPeer and seeing if GetChildren can be overriden. It can't, but the GetChildrenCore method can, and that is good enough.
All that has to be done is to add a TextBoxAutomationPeer to the list of children and we should be good to go. SpecialListBox ends up looking like this:

    public class SpecialListBox : ListBox
    {
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new SpecialListBoxAutomationPeer(this);
        }
    }


The fun bits are in the SpecialListBoxAutomationPeer. I used a helper class from WPFUtilities to help me get at the TextBox. All it does is make VisualTreeHelper a bit easier to use, but under the covers the work is done by VisualTreeHelper.

    public class SpecialListBoxAutomationPeer : ListBoxAutomationPeer
    {
        public SpecialListBoxAutomationPeer(ListBox owner) : base(owner)
        {
        }
 
        protected override string GetLocalizedControlTypeCore()
        {
            return "SpecialListBox";
        }
 
        protected override List<AutomationPeer> GetChildrenCore()
        {
            TextBox textBox = null;
            DepthFirstVisualTreeIterator iter = new DepthFirstVisualTreeIterator();
            foreach (DependencyObject dependencyObject in iter.GetNodes(Owner))
            {
                FrameworkElement element = dependencyObject as FrameworkElement;
                if (element != null && element is TextBox && element.Name == "templateTextBox")
                {
                    textBox = (TextBox)element;
                    break;
                }
            }
            List<AutomationPeer> children = base.GetChildrenCore();
            TextBoxAutomationPeer button = new TextBoxAutomationPeer(textBox);
            children.Add(button);
            return children;
        }
    }


SpecialListBoxAutomationPeer just finds the TextBox, creates an AutomationPeer for it, gets the children that would normally be returned and adds the TextBoxAutomationPeer to them.

This all ends up being a bit more work than I think should have to be done. It also requires me to change my code. I can't fix this kind of problem after the fact. I still think re-writing this to use a user control is a better solution. That way I don't have to mess with custom AutomationPeers or anything. Still, it was a good learning exercise.

Thursday, March 15, 2007

Problems adding a test project in Orcas CTP

I've been playing with the March 2007 CTP of Orcas and the associated VSTS goodness that it contains. It did something a bit funny to me the other day that wasted an hour or two of poking around when the solution ended up being simple but not obvious.
I started out with a simple class library and then went to add a test project. I'm an NUnit guy but I frequently use the unit testing in VSTS. Of course the first thing you have to do with any unit testing project is add a reference to the assembly you want to test. When I did this I got a warning telling me that the assembly that was being referenced (the class library) was being built for a newer version of the framework than the project that was referencing it (the test project). That seemed bad.
I was trying to write some unit tests around LINQ and just couldn't get the test project to compile when I added any 3.5 code to the test project. I know that in the project properties there is a way to set the target framework, but when I went to the property pages for the project there was an exception. It's a CTP so no big deal, but I really want to write some unit tests around what I'm doing.
On a lark I closed visual studio and opened it again. When I opened it the project conversion wizard popped up telling me the test project was created with an earlier version of Visual Studio. It wasn't, but this is a CTP so no real worries. Once the project conversion wizard ran all was good.
Long story short, if you are adding a test project in the ctp, close and re-open the solution and let the project conversion wizard run before you go too far.

Tuesday, March 06, 2007

Unit Testing WPF controls with Automation Peers

Recently I ran into a situation where I wanted to be able to test some data binding code in a WPF application. I am trying to be careful not to test the data binding framework but I also need to make sure that the way I am talking to the framework is right. That discussion could fill up a few (dozen) blog posts anyway so I’ll stay away from it for now.
I’ve written an overly simplified example in an attempt to demonstrate the problem without all the baggage of the original application I was working on. So I’ve got a text box that shows a number and a button that you can click to increment the number. You could also just type a new number in the text box. I haven’t added any of the validation code that should be here. I’m trying to stay bare bones and focus on unit testing WPF controls.
Here are the UI parts


<Window x:Class="WPFSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPFSample" Height="343" Width="400">

<Grid>
<Button Height="23" Margin="153,0,164,103" Name="incrementButton" VerticalAlignment="Bottom" Click="IncrementClicked">Increment</Button>
<TextBox Height="26" Margin="144,125,148,0" Name="valueTextBox" VerticalAlignment="Top" Text="{Binding Path=Number}"></TextBox>
</Grid>
</Window>


public partial class Window1 : System.Windows.Window
{
NumberHolder holder = new NumberHolder();
public Window1()
{
InitializeComponent();
DataContext = holder;
}

private void IncrementClicked(object sender, RoutedEventArgs args)
{
holder.Number++;
}
}

A NumberHolder is simply just

public class NumberHolder : INotifyPropertyChanged
{
private int number = 0;

public int Number
{
get { return number; }
set
{
number = value;
FirePropertyChanged("Number");
}
}

private void FirePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

What I want to do is check what value is in the text box, click the button and then assert that the value is now one greater than it was. If this were Windows Forms I probably would just make the textbox and button public and call them from the test. Sure it breaks encapsulation but in this case it’s just me so I’m not that worried about it. Well it turns out that I’ve got less control over such things in WPF than I had in Windows Forms. I can’t set the visibility of the button or text box object as easily as I could before and it is defaulting to internal.
I need another way to get to the controls. We’ve faced this sort of problem for years. WPF gives us a new tool to try and use. Each control exposes something called an Automation Peer. This is normally used by the new UI Automation framework so that accessibility applications or testing applications can see and manipulate your controls. Sounds just like what I’m looking for.
Getting a peer from a control is easy. Assuming you have access to the control which, of course, is one of the problems I’m trying to solve. But I can still get to the peers if I can get to a parent. Since I’m unit testing I’m going to be creating an instance of the container (in my case a Window) so I have that. WindowAutomationPeer takes an instance of Window in its constructor so we’re all set there. Once I have the WindowAutomationPeer I can ask it for its children.

[Test]
public void ClickTheButton()
{
Window1 window = new Window1();
WindowAutomationPeer windowPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = windowPeer.GetChildren();
}

In this case GetChildren won’t return anything. The window only has children once it’s shown. Since I’m trying to unit test this class I’d rather just instantiate it and call some methods on it and be done. Looks like that isn’t going to work out yet. If I were smart I’d drop it and find another way. But I’m stubborn and I want to see where this goes. So I’ll throw in a window.Show(); call. If you’ve ever tried anything like this you’re probably thinking, “That will cause a threading issue any minute now.” Probably right but I’m going to plod on anyway.
I’m going to cheat now and rely on the fact that the children are in an expected order. I wouldn’t do that in a production app but for now it is good enough to keep going

TextBoxAutomationPeer textBoxPeer = (TextBoxAutomationPeer)children[0];
ButtonAutomationPeer buttonPeer = (ButtonAutomationPeer)children[1];

The first thing I want to do is to see the value of the text box before I do anything. I’ve got the textBoxPeer so I can do that but it isn’t obvious how to do it. Peer’s can tell you a lot about a control but they don’t let you interact with the control. Providers let you interact with controls. I could argue that checking the value of a control is not interacting with it but I didn’t write the framework so it wouldn’t get me too far. To see the value in the text box I need an IValueProvider. It turns out that TextBoxAutomationPeer is an IValueProvider but it uses explicit interface implementation so we have to cast. Finally I’ve hit an assert.

Assert.AreEqual("0", ((IValueProvider)textBoxPeer).Value);

That == green bar == happiness. But that’s boring so I’m not happy for long. Next I want to click the button and I run into the same provider vs. peer problem but with a similar solution. I need an IInvokeProvider and the button peer explicitly implements that.

((IInvokeProvider)buttonPeer).Invoke();
Assert.AreEqual("1", ((IValueProvider)textBoxPeer).Value);

That fails. The Invoke call doesn’t throw an exception but the value in the textbox is still 0. I double checked the app and clicking the button does work. So that means we are probably running into my other favorite part of testing UIs, threading. Normally that window would live in its own thread, but I’ve got it riding along in the same thread as my test. That probably leads to deep unhappiness. Cross thread communications in unit tests isn’t the easiest thing to deal with. I might end up there but for now I’d like to try something else.
AutomationPeers have an Owner property that will let you get at the actual UIElement, in this case the button. Once I can get my hands on the button I can get it to raise the click event like this

Button button = (Button) buttonPeer.Owner;
RoutedEventArgs args = new RoutedEventArgs(Button.ClickEvent, button);
button.RaiseEvent(args);


If I squeeze that in between the two asserts replacing the call to Invoke I get a passing test.
So far peers have helped but not as much as I might like. A peer can let me get at a control without having to publicly expose the control. The biggest downside is that I seem to have to show the window to get things to work. I also want to make sure I close the window when I’m done so a bit of refactoring leads us to this test fixture.

[TestFixture]
public class Window1Tests
{
private ButtonAutomationPeer buttonPeer;
private TextBoxAutomationPeer textBoxPeer;
private Window1 window;
private WindowAutomationPeer windowPeer;

[SetUp]
public void SetUp()
{
window = new Window1();
window.Show();
windowPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = windowPeer.GetChildren();
buttonPeer = (ButtonAutomationPeer)children[0];
textBoxPeer = (TextBoxAutomationPeer)children[1];
}

[Test]
public void ClickingIncrementIncrementsTheNumberInTheTextBox()
{
Assert.AreEqual("0", ((IValueProvider)textBoxPeer).Value);
Button button = (Button) buttonPeer.Owner;
RoutedEventArgs args = new RoutedEventArgs(Button.ClickEvent, button);
button.RaiseEvent(args);
Assert.AreEqual("1", ((IValueProvider)textBoxPeer).Value);
}

[TearDown]
public void TearDown()
{
window.Close();
}
}

Putting the call to Close in the TearDown makes sure that it will get closed even if the test fails.