Custom Controls and UI Automation
We came across an interesting problem a customer hit when trying to write a custom control. The issue was that although the control appeared to work correctly in the application, when it came time to do out-of-process testing using UI Automation (UIA), the newly added functionality of the custom control wasn’t being exposed, and therefore couldn’t be tested. In this post, I’ll explain a little what the problem is, and offer a possible solution.
First, kudos to the customer for taking the time to test the custom control, and realizing there was an issue to begin with. This is a good example of why it's important to do software testing and verify assumptions, as although things seem to “just work”, that’s not always the case.
The custom control in question is a modified tab control, with a Header property that allows you to specify content in addition to the usual tab items. This is a little similar to the concept of a HeaderedContentControl, and the markup would look like this:
<local:CustomTabControl Style="{StaticResource TabControlStyle}">
<local:CustomTabControl.Header>
<!-- Any content can go here -->
<Button Content="Header Button" />
</local:CustomTabControl.Header>
<local:CustomTabControl.Items>
<TabItem Header="Tab 1" />
<TabItem Header="Tab 2" />
</local:CustomTabControl.Items>
</local:CustomTabControl>
As you may have guessed, the problem is that although the custom tab control exposed the tab items properly to UIA, it didn’t also expose the Header content. Is this expected? Yes, because controls in WPF are responsible for exposing their UIA items themselves.
This is done through the AutomationPeer class. All the major control classes in WPF, such as TabControl, have a corresponding AutomationPeer class that exposes the control’s functionality to UIA, creatively named TabControlAutomationPeer in this case. This is explained in some detail in this MSDN page.
When a UIA client looks at the structure of the application, WPF will raise the OnCreateAutomationPeer event on elements in the WPF application tree. In the case of a regular TabControl, when the event is raised, a TabControlAutomationPeer instance is created. If you write a custom control but don’t handle this event, the behavior of the base class your control derives from is the behavior that will be exposed to UIA. Since TabControlAutomationPeer doesn’t know about the newly added Header property in CustomTabControl, it doesn’t expose it to UIA.
Now that the problem is clearer, lets focus on a solution. In order for CustomTabControl to behave as expected, you would need to create its CustomTabControlAutomationPeer counterpart. Since we’re only adding one property to TabControl, we can derive from TabControlAutomationPeer and reuse most of the base class functionality:
public class CustomTabControlAutomationPeer : TabControlAutomationPeer
{
public CustomTabControlAutomationPeer(TabControl owner)
: base(owner)
{
}
protected override string GetClassNameCore()
{
return "CustomTabControl";
}
protected override List<AutomationPeer> GetChildrenCore()
{
List<AutomationPeer> automationPeers = base.GetChildrenCore();
CustomTabControl owner = base.Owner as CustomTabControl;
if (owner != null)
{
UIElement headerUIElement = owner.Header as UIElement;
if (headerUIElement != null)
{
AutomationPeer peer = UIElementAutomationPeer.CreatePeerForElement(headerUIElement);
if (peer != null)
{
automationPeers.Add(peer);
}
}
}
return automationPeers;
}
}
The CustomTabControl now needs to create an instance of the new CustomTabControlAutomationPeer:
public class CustomTabControl : TabControl
{
...
protected override AutomationPeer OnCreateAutomationPeer()
{
return new CustomTabControlAutomationPeer(this);
}
}
If you launch a UIA tool like UI Spy (great tool which is available in the .Net Framework SDK), this is what you would see before the change, with a view scoped to the custom tab control:
This is what you can see after the change, with the Header button properly exposed:
So is that all we need? Actually, no. There is an issue with the solution I have described so far. What happens if the content of the Header property is changed to something else? At the moment, nothing from a UIA perspective. Because UIA doesn’t keep track of changes, and because you didn’t notify it of any changes, the image you see above would remain the same, and it would incorrectly believe the Header content is the same.
To solve this, you need to find a way to notify UIA of the change. Luckily, WPF makes this fairly straightforward when using a DependencyProperty, which is how you would expose a Header property in the custom control. When you declare the property, you can specify a PropertyChanged handler for the Header which would look like this:
public class CustomTabControl : TabControl
{
...
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register("Header", typeof(object), typeof(CustomTabControl),
new FrameworkPropertyMetadata(OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CustomTabControl tabControl = d as CustomTabControl;
if (tabControl != null && (e.OldValue != e.NewValue))
{
if (AutomationPeer.ListenerExists(AutomationEvents.StructureChanged))
{
// Notify the UI automation client that the content changed
TabControlAutomationPeer peer = UIElementAutomationPeer.FromElement(tabControl) as TabControlAutomationPeer;
if (peer != null)
{
peer.RaiseAutomationEvent(AutomationEvents.StructureChanged);
}
}
}
}
}
When the Header content changes, this raises an event which lets UIA know it needs to perform an update. A VS 2008 sample with all the code in this post can be found below, and hope you found this useful!
Comments
Anonymous
January 13, 2010
Hello, This is a very nice article for adapting custom controls to UI Automation. But I have a further question. I have implemented a custom control which directly derives from Control class and does not behave like basic controls (Button, ComboBox, TabControl...) So in my automationpeer class I do not have a proper provider interface to inherit. Is it possible to write my own provider class? Or do you have any other suggestions. RegardsAnonymous
February 08, 2011
Thanks a lot Patrick for this inormative article. It really helps when you realize what exactly went wrong. But I have the same problem, PLUS i don have the source code of the application. So, is there any way i can cusom properties?Anonymous
March 31, 2013
Hi Patrick Danino, I am using CodedUI to automate on a WPF application, I have 2 buttons in a form, I want to verify the text of each button. But when running only have passed script. Because The actual result of the second script is always the actual of the first button. So how I get the second text to verify? Please help me. (I saw they are the same AutomationID when I record)Anonymous
April 01, 2013
The comment has been removedAnonymous
April 03, 2013
Thanks Patrick Danio, I have resolved this issue. BC It was written by WPF so We have to get objects base on instances, compare positions of button and text on that to verify it exists or not But we have to add more properties for objects, such as: Instances, Name... before gettingAnonymous
December 06, 2014
That's an informative article Patrick. I'm currently working on developing a test framework for wpf control using Coded UI. Our control uses third party chart control. Can i expose customer properties through the code and access through coded UI test client.?? I've gone through articles in some blogs, but they seemed more trivial to me and the customization is limited by the control set supported by wpf. My use case is, I want to expose the chartArea to the test client. Do i need to implement my own IValueProvider for that, is it possible at all? Thanks SrinikethAnonymous
April 15, 2015
Thanks for this post. This information helped me to move little ahead in my task. But I ran into a related issue. Inside the CustomTabControl I have a UserControl. The UserControl has buttons and other controls. When I call Invoke() function on any of the buttons (InvokePattern.Invoke()), the application becomes non responsive. I have implemented CustomTabControl and CustomTabControlAutomationPeer classes as suggested in this post. After implementing this I am getting the AutomationElement from FindFirst call successfully, until I implemented this I was getting null. The below call fails to run Automation testing to Invoke the Button: <Controls:CustomTabControl x:Name="MainRegion" SelectedIndex="0" VerticalAlignment="Stretch" prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}" /> The below call succeeds Automation testing to Invoke the Button. <ContentControl x:Name="MainRegion" prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}" /> MainRegion adds UserControl inside it, the UserControl has the buttons that is not working in automation. What is that I am missing in the implementation? Your help is appreciated. Thanks, Santhosh.