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!

CustomTabControlSample.zip