Application design and development is an art. It requires to be adept in tools of the trade. For Xaml technologies, the better you are in writing XAML and underlying framework, the better designer you are. For developing WPF applications, very commonly, we use custom controls, control templates and data templates but they make it really difficult to automate our applications. I thought it would be a good idea to write a two-part series to discuss the roadblocks of automating UIs and their possible solutions. This is the first part of the series. Here we are discussing how to expose elements from a ControlTemplate.
For this post, we will be assuming that you have Windows SDK installed on your machine. We would be using UI Automation Verify tool from the SDK. You can download the SDK from Microsoft's download page.
Let's consider a very simple application. It just consists of a button shown in the middle. The requirements suggested that it has to be elliptical and painted as green. It has to have some text at its center with some border around. In the bottom, it should have a copyright text. As ugly as it can look, it is shown as below :)
This is the XAML used for creating the above window. Here we are using a custom button. We are not adding anything else to the button but we are setting some properties. The underlying button should use them.
Since this greatest button is not available off-the-shelf, we need to update the existing button's ControlTemplate. Here we are adding Ellipse, Border and TextBlocks to the template. In order to respect the properties defined in parent control, here we are using TemplateBinding.
Here UIAutomationButton is derived from Button. Let's keep its definition empty for now. Let us run the application now. When we try to view the hierarchy in UI Automation, it is surprising to see that we see no elements under button. We know that they are in the ControlTemplate. Now your automation guys would complain that they cannot see any elements under the button, so they cannot automate this application.
Now previously we have discussed that Microsoft has provided UIAutomation API for nearly all of its UI technologies including WPF. For exposing elements for automation, we introduce AutomationPeer for elements. Generally, there is a 1:1 correspondence between a control type and its automation peer. It is the responsibility of control authors to provide automation peer for the controls for automation. We have discussed about AutomationPeer here [Discussion about AutomationPeer].
In order to expose the elements defined under the control template, we need to introduce a new AutomationPeer for the new button type. The type inherits from ButtonAutomationPeer defined under System.Windows.Automation.Peers namespace in PresentationFramework assembly. In order to provide the automation peers for its children, an AutomationPeer needs to override GetChildrenCore method. The method returns a list of child automation peers for its children. Now don't go recursive, it just needs to do it for its immediate children.
Just look at how it is finding the expected children from Owner's Template using FrameworkTemplate.FindName() method [MSDN].
There are no AutomationPeer(s) provided for the types which are not expected to be automated. They include shapes , decorators and panels [MSDN Reference]. In this example, I have included all three of them i.e. Border(Decorator), DockPanel(Panel) and Ellipse(Shape). It is possible that we want to automate and verify some of them. In the above example, we have assumed that we don't have to expose DockPanel but we do need to do it for Border and Ellipse. Since there is no custom automation peer for them, we have just used FrameworkElementAutomationPeer for Ellipse.
Since we have to expose a child TextBlock from Border, we need to define a custom AutomationPeer for border. Here we have defined UIAutomationBorderAutomationPeer for this purpose. Since the TextBlock is directly a child of Border, we can access that using FrameworkElement.FindName() method [MSDN].
Now the control needs to tell automation framework about AutomationPeer it prefers for automating itself. It does so by returning an instance of the intended automation peer in an overridden OnCreateAutomationPeer() method [MSDN].
Now let's run it again and try to see how it has affected the hierarchy for automation. Here we notice that although the Ellipse and Border is available but there are no TextBlock(s). Now we know we have two TextBlocks in our design. But why are they not visible here?
Actually TextBlocks are not always visible for automation. I have no idea why Microsoft decided to hide TextBlocks from automation when they are part of a ControlTemplate. They are visible only when they are part of a ContentPresenter in a DataTemplate. Since Microsoft has opened up its Source code for easy reference, you can verify this from here:
Since TextBlocks are defined under a ControlTemplate, they are not available for Automation. In order to fix this, we need to add a new AutomationPeer for our TextBlocks. You don't need to provide a derived type for TextBlock. Only a child TextBlockAutomationPeer with overridden IsControlElementCore() should be enough.
We also need to update the other Automation peers to use this instead of TextBlockAutomationPeer wherever we need to expose TextBlocks.
Now the UI Automation Verify should be able to show the hierarchy as follows:
Now we just need to assign AutomationIds to these controls in order for them to be automated. Zindabad!!!
Get It from GitHub
What about Template Parts [Part_SOMETHINGS]
Now let me give you an exercise to practice what you have learned here. As an example we have this Custom ListBox with Header as a TemplatePart. We are naming it as Part_Header.
And we have the ControlTemplate for the ListBox as follows:
We are using the HeaderedListBox as follows to create our UI.
But when we look at the UI Automation Verify. The whole hierarchy under the list box is empty.
Let’s see if you can create an automation peer for this. Keep me posted.