Monday, July 5, 2010

Automation Peers for Automation of WPF applications

We need UI to be tested for the required functionality the way it would be presented to the user. MVVM has provided us the way to test some of the functionalities through view models but we always want to test the behavior of the application as it is being used by end users. The answer is right, write some automation scripts. But how??

WPF provides us the facility to write automation tests using Automation Peer framework. The whole idea is to provide a framework for imitating the actions of actual users. It seems same as calling events directly but it is much better than that. Don’t believe me…see this!

http://social.msdn.microsoft.com/forums/en-US/wpf/thread/1ba3696e-6ca2-48de-80df-31477c9504ce/

As I said, it is better to use UI automation. It allows behaving as being used by the real user. If the control has some associated command which can’t be executed, automation peer wouldn’t allow the behavior to run.

How does it work?
All UI automation works through three Ps (Peer, Provider and Pattern).The whole idea is to get a PATTERN object with for each control. All the automation is done using the properties and methods of this PATTERN object. For getting an automation object, we have to obtain a PEER object, depending upon the control in interest (e.g. TextBoxAutomationPeer for TextBox). The peer object provides a PROVIDER which can be used to get the required PATTERN.

WPF has provided us with different peer objects for different types of available controls. Each Peer defines GetPattern() method, which returns a pattern object depending upon the type of peer (e.g. TextBoxAutomationPeer provides a patterns which implements IValueProvider. These providers are available in UIAutomation.dll (System.Windows.Automation.Providers namespace).

Namespaces:
  1. System.Windows.Automation.Peers
  2. System.Windows.Automation.Providers

Assemblies:
- UIAutomation.dll
- PresentationCore.dll

Let's see this in Practice
Let us create a small program with the following main form:



The XAML definition of above form is as follows:

<Window x:Class="AutomationPeer.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<TextBox Height="21" Margin="98,105,60,0" x:Name="result" VerticalAlignment="Top" />
<TextBox Margin="98,29,26,0" x:Name="operand1" Height="23" VerticalAlignment="Top" />
<TextBox Height="23" Margin="98,66,26,0" Name="operand2" VerticalAlignment="Top" />
<Label Height="24" HorizontalAlignment="Left" Margin="16,27,0,0" Name="label1" VerticalAlignment="Top" Width="76">Operand 1</Label>
<Label Height="24" HorizontalAlignment="Left" Margin="16,66,0,0" Name="label2" VerticalAlignment="Top" Width="76">Operand 2</Label>
<Label Height="24" HorizontalAlignment="Left" Margin="16,105,0,0" Name="label3" VerticalAlignment="Top" Width="76">Result</Label>
<Button Height="23" HorizontalAlignment="Left" Margin="17,0,0,104" Name="btnEvaluate" VerticalAlignment="Bottom" Width="75" Click="btnEvaluate_Click">Evaluate</Button>
</Grid>
</Window>

Where the definition of Click event of btnEvaluate (btnEvaluate_Click) is as follows:

private void btnEvaluate_Click(object sender, RoutedEventArgs e)
{
double operand1Value = double.Parse(operand1.Text);
double operand2Value = double.Parse(operand2.Text);

double resultValue = operand1Value + operand2Value;

result.Text = string.Format("{0}", resultValue);
}

In this event, we are getting the values entered in two boxes and adding them. We are displaying result in text block (result). Now let us create the real automation by modifying the constructor of Window1 as follows:

public Window1()
{
InitializeComponent();

var peerOperand1 = new TextBoxAutomationPeer(this.operand1);
var providerOperand1 = (IValueProvider)peerOperand1.GetPattern(PatternInterface.Value);
providerOperand1.SetValue("5.3");

var peerOperand2 = new TextBoxAutomationPeer(this.operand2);
var providerOperand2 = (IValueProvider)peerOperand2.GetPattern(PatternInterface.Value);
providerOperand2.SetValue("2.1");


var peerEvaluator = new ButtonAutomationPeer(this.btnEvaluate);
var providerEvaluator = (IInvokeProvider)peerEvaluator.GetPattern(PatternInterface.Invoke);
providerEvaluator.Invoke();
}

Here we have ValuePattern of two text boxes (operand1 and operand2) using IValueProvider provider obtained through TextBoxAutomationPeer. Finally we have obtained InvokePattern for btnEvaluate using IInvokeProvider and ButtonAutomationPeer.

When we run the application, the form displays as follows:



The form has displayed the exact result in the text block after executing the btnEvaluate_Click event for btnEvaluate method.

Automating Custom Controls:
In order to automate custom controls, we would need to define new peers. We will be discussing the details of defining user defined peers later.

Automating from outside:
In above discussion, we have seen how to automate the form from within it. Now the problem is when we want to do this automation from outside the form in our test scripts. Do we need to make our controls public? No!!!

WPF allows us to navigate through the peer hierarchy using GetChildren() and GetParent() methods. In order to automate the control, we need to traverse through this hierarchy to the concerned control and automate that. All the automation peers may provide the definition of GetChildrenCore() so that peer system could generate this peer navigation hierarchy. The default definition in UIElementAutomationPeer traverses through the visual tree of elements.

Now we deep dive a bit in the world of Automation Peer. Let's look at the class hierarchy.

Inheritance Hierarchy of Automation Peers:



All control automation peer inherit from FrameworkElementAutomationPeer directly (like TextBlockAutomationPeer) or indirectly (like ButtonAutomationPeer through ButtonBaseAutomationPeer).

Let's play again:
Now we provide the same automation to the form from outside the form. Let's remove the automation code from constructor of Window1. Now it should look like as follows:

public Window1()
{
InitializeComponent();
}

In order to keep the example simple to understand, We are not creating a separate project. Modify App.xaml as follows:

<Application x:Class="AutomationPeer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup"
>
<!--StartupUri="Window1.xaml"-->
<Application.Resources>

</Application.Resources>
</Application>


Now let's provide implementation for Application_Startup event.

private void Application_Startup(object sender, StartupEventArgs e)
{
ProgramAutomation pAutomation = new ProgramAutomation();
pAutomation.automate()
}


Where ProgramAutomation is defined as follows:

class ProgramAutomation
{
public ProgramAutomation()
{
}

public void automate()
{
var myWindow1 = new Window1();

//It is required that a window is available on UI before calling GetChildren on it
myWindow1.Show();

var myWindow1AutomationPeer = new WindowAutomationPeer(myWindow1);
var myWindow1AutomationPeerChildres = myWindow1AutomationPeer.GetChildren();

//get automation peers of operands text boxes and button
var operand1AutomationPeer = myWindow1AutomationPeerChildres[1]; //myWindow1AutomationPeerChildres.Where(p => p.GetName() == "operand1").FirstOrDefault();
var operand2AutomationPeer = myWindow1AutomationPeerChildres[2]; //.Where(p => p.GetName() == "operand2").FirstOrDefault();
var evaluatorAutomationPeer = myWindow1AutomationPeerChildres[6];//.Where(p => p.GetName() == "btnEvaluate").FirstOrDefault();
var resultAutomationPeer = myWindow1AutomationPeerChildres[0];

((IValueProvider)((TextBoxAutomationPeer)operand1AutomationPeer).GetPattern(PatternInterface.Value)).SetValue("5.3");
((IValueProvider)((TextBoxAutomationPeer)operand2AutomationPeer).GetPattern(PatternInterface.Value)).SetValue("2.1");
((IInvokeProvider)((ButtonAutomationPeer)evaluatorAutomationPeer).GetPattern(PatternInterface.Invoke)).Invoke();
string result = ((IValueProvider)((TextBoxAutomationPeer)resultAutomationPeer).GetPattern(PatternInterface.Value)).Value;
System.Windows.MessageBox.Show(result);
}
}


Now as you might have an idea from the code that we are setting the values of the two operands as before. After invoking the button event, we are getting value of result text box. The message box should show 7.4 on the screen. But it doesn't. Why??

It must be remembered that IInvokeProvider allows an event to be fired, which is asynchronous. It just sends a request to execute its action. It doesn't guarantee when it would be invoked. That is why the message box is always empty because the invoke is executed after we execute the following:

string result = ((IValueProvider)((TextBoxAutomationPeer)resultAutomationPeer).GetPattern(PatternInterface.Value)).Value;

The only solution seems to be invoking the action on a separate thread using dispatcher and waiting for it to return using Thread.join.

Otherwise, just display an empty message box to the user. This is a blocking call as the runtime has to wait for user action. In the mean time, it executes the Invoke(). The new definition of automate() is as follows:

public void automate()
{
var myWindow1 = new Window1();

//It is required that a window is available on UI before calling GetChildren on it
myWindow1.Show();

var myWindow1AutomationPeer = new WindowAutomationPeer(myWindow1);
var myWindow1AutomationPeerChildres = myWindow1AutomationPeer.GetChildren();

//get automation peers of operands text boxes and button
var operand1AutomationPeer = myWindow1AutomationPeerChildres[1];
var operand2AutomationPeer = myWindow1AutomationPeerChildres[2];
var evaluatorAutomationPeer = myWindow1AutomationPeerChildres[6];
var resultAutomationPeer = myWindow1AutomationPeerChildres[0];


((IValueProvider)((TextBoxAutomationPeer)operand1AutomationPeer).GetPattern(PatternInterface.Value)).SetValue("5.3");
((IValueProvider)((TextBoxAutomationPeer)operand2AutomationPeer).GetPattern(PatternInterface.Value)).SetValue("2.1");
((IInvokeProvider)((ButtonAutomationPeer)evaluatorAutomationPeer).GetPattern(PatternInterface.Invoke)).Invoke();

System.Windows.MessageBox.Show("");
string result = ((IValueProvider)((TextBoxAutomationPeer)resultAutomationPeer).GetPattern(PatternInterface.Value)).Value;
System.Windows.MessageBox.Show(result);
}

5 comments:

Unknown said...

This is a wondeful article Muhammad

Do you know how we can get automation peer working for MVVM

thanks
Ajay

Unknown said...

Wonderful article Muhammad

Do you know how we can implement automation peer for MVVM

Thanks
Ajay

Unknown said...

Wonderful article Muhammad

Do you know how we can get automation peer implemented for MVVM

Thanks
Ajay

Muhammad Shujaat Siddiqi said...

Thanks Ajay. From the application side, you don't have to do much except assigning automation Ids.

Your control authors would need to implement automation peer for their custom controls.

Automation is generally a separate project run by a different team in close collaboration with the development team.

In this example, I wanted to present UIAutomation as a concept for WPF applications. You might want to consider some frameworks which provide an abstraction on top of this e.g. White. There are also other good options.

Unknown said...

Hi Muhammad,

Suppose in place of standard "TextBox" control if I place my custom "TouchTextBox" control, by default for this AutomationPeer will not implemented.

So we have to implement for my custom control.

Can you explain how to do that, and how from external app or UIAutomation it will been seen or it will reflect.

Thanks
Chandra