Introduction
The Coded UI Tests, available in Visual Studio Ultimate or Premium, enable the creation of automated tests for the User Interface. This is a really nice feature because you are no longer forced to make "hand made" tests which take hours to be performed. The WPF controls in the framework are ready to be used by the Microsoft UI Automation which is itself used by the coded UI tests. This means that when you use the screen recorder to record the tests on your UI, it is able to find the several controls used in your application.
When you create your own custom controls or extend a standard one, the recorder would not be able to find them at first and so a whole part of the screen may not be available for tests. Actually, it is possible to record a test but every step will be done using the screen position: click at (120,30), drag from (120,30) to (10,40). This is really annoying because any change in the UI may break all your tests.
In this post, we will see how to make a custom control fully useable in a Coded UI Tests scenario. We will answer the question "Why cannot the code UI test recorder find anything inside my WPF or Silverlight custom control ?"
Note: The same technique is used by the accessibility client and by enabling this feature, you also put at ease people using your application through UI automation clients, like the partially-sighted person.
How It Works
Everything is based on a class named AutomationPeer
. When a UI automation client analyzes your user interface, it looks for an automation peer and uses it to walk through the tree of peers. The tree of peers is nearly a visual tree but it exposes only the relevant part of the interface
.
Every standard control has a corresponding automation peer already defined. But if you add new parts on them or if you build a new control from scratch, you have to provide a related automation peer.
If not, the automation peer will not be aware of these parts and won’t be available to the automation clients. Also, if a standard control is used but the “normal” visual tree is broken, for example, by splitting the normal items in several parts, it is necessary to expose the new parts. I’ve seen this in some controls where the content of the standard content presenter was taken apart to be placed in another place of the Visual Tree. As you will see, this is done in the GetChildrenCore
method of the new AutomationPeer
.
How to Make Your Controls Available to the UI Automation Client
The AutomationPeers
are created and retrieved for each control in the method named OnCreateAutomationPeer
. The custom control must override this method to provide an AutomationPeer
related to its specific implementation.
If the control is built from scratch and its base class is Control
, there is no built-in automation peer because there is simply none at all for the Control
class. However, the FrameworkElementAutomationPeer
class can be used as a base class.
If the controls inherits from another standard one like TabControl
, it’s possible to inherit from its corresponding AutomationPeer
and customize its behavior. This is especially needed if new controls are added to the template of the control because the basic AutomationPeer
(let’s say TabControlAutomationPeer
) does not know about them. By convention, the automation peer class name starts with the name of the related control and ends with AutomationPeer
.
There are several methods in the AutomationPeer
that can be overridden to make it work the right way. The methods in bold are the ones which are required at the least:
GetClassNameCore
: It returns the name of the class the AutomationPeer
is built for.
GetAutomationControlTypeCore
: Returns an enum
telling which behavior implements the automated control. For example, it can a be Button
, Tab
, Window
, etc.
GetPattern
: Tells which kind of functional behavior is fulfilled by the control: IRangeValueprovider
, ITextProvider
, IValueProvider
, etc.
GetChildrenCore
: This method returns a list of AutomationPeers
which are related to the children of your control. If you add a new visual part to a standard control, this is where you must add the new child.
IsControlElementCore
and IsContentElementCore
: Tell the UI Automation client which type of automated control it is: for reading or for interactive purpose. These can improve the performance when being used as a filter.
Also, you can use the static
method CreatePeerForElement
of the UIElementAutomationPeer
class to create an AutomationPeer
of a known control. This is especially helpful when a new control has to be added.
Finally, the UI Automation clients do not follow the changes in the UI and they have to be noticed manually. This is done via the RaisePropertyChanged
on the relative automation peer and has to be done on every change. This is very similar to what is necessary to be done in the INotifyPropertyChanged
to notify the binding of a change.
Here is an example of an AutomationPeer
for the HeaderedControl in the Amazing WPF control library.
1: public class HeaderControlAutomationPeer : FrameworkElementAutomationPeer
2: {
3: public HeaderControlAutomationPeer(FrameworkElement owner)
4: : base(owner)
5: {
6: if (!(owner is HeaderedControl))
7: throw new ArgumentOutOfRangeException();
8: }
9:
10: protected override string GetNameCore()
11: {
12: return "HeaderContentControl";
13: }
14:
15: protected override AutomationControlType GetAutomationControlTypeCore()
16: {
17: return AutomationControlType.Header;
18: }
19:
20: protected override System.Collections.Generic.List<AutomationPeer>
GetChildrenCore()
21: {
22: List<AutomationPeer> peers = base.GetChildrenCore();
23: var peer = UIElementAutomationPeer
24: .CreatePeerForElement(MyOwner.PART_Header);
25: if (peer != null) peers.Add(peer);
26: return peers;
27: }
28:
29: private HeaderedControl MyOwner
30: {
31: get { return (HeaderedControl)base.Owner; }
32: }
33: }
And here is an example of code you can implement to raise the property changed event on a peer:
1: if (AutomationPeer.ListenerExists(AutomationEvents.PropertyChanged))
2: {
3: HeaderControlAutomationPeer peer =
4: UIElementAutomationPeer.FromElement(PART_Header)
5: as HeaderControlAutomationPeer;
6:
7: if (peer != null)
8: {
9: peer.RaisePropertyChangedEvent(
10: RangeValuePatternIdentifiers.ValueProperty,
11: (double)oldValue,
12: (double)newValue);
13: }
14: }
Interesting Links
Please let me know if you have any questions!
You can also read this article on my blog.