Search code examples
c#wpfcustom-controlsmicrosoft-ui-automationwhite-framework

How should I define Custom Controls to enable UI Automation and TestStack White?


I am beginning to use TestStack White (UI Automation) to automate tests in an WPF existing application. When using standard controls everything works fine. However, I run into problems when trying to interact custom controls.

For example, I have a LabeledComboBox which is actually a TextBlock plus a ComboBox. This is defined as a class derived from Control plus a ControlTemplate in XAML:

public class LabeledComboBox : Control
{
    static LabeledComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LabeledComboBox), new FrameworkPropertyMetadata(typeof(LabeledComboBox)));
    }
}

<local:LabeledComboBox>
    <local:LabeledComboBox.Template>
         <ControlTemplate TargetType="{x:Type local:LabeledComboBox}">
             <StackPanel>
                  <TextBlock Text="Text"/>
                  <ComboBox/>
             </StackPanel>
          </ControlTemplate>
    </local:LabeledComboBox.Template>
</local:LabeledComboBox>

This control works, but if you run UI Automation Verify the only part visible to UI Automation is the ComboBox, and the TextBlock cannot be accessed.

However, if you create this as a UserControl using XAML and Code Behind, both the TextBox and the ComboBox are properly visible to UI Automation.

I have tried to create an AutomationPeer (FrameworkElementAutomationPeer) for my control, but I have not been able to make the TextBlock visible to UI Automation so far. One interesting result is that FrameworkElementAutomationPeer::GetChildrenCore() properly returns a list of 2 automation peers, one for the TextBlock and one for the ComboBox.

How should I change my custom control so it is properly testable using UI Automation and White?


Solution

  • The default automation peer for TextBlock (TextBlockAutomationPeer) removes the corresponding owner from the UI tree if it's part of a ControlTemplate, for some reason.

    You can find the relevant code here: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Automation/Peers/TextBlockAutomationPeer.cs,16e7fab76ffcb40a

     override protected bool IsControlElementCore()
     {
        // Return true if TextBlock is not part of a ControlTemplate
        TextBlock tb = (TextBlock)Owner;
        DependencyObject templatedParent = tb.TemplatedParent;
        return templatedParent == null || templatedParent is ContentPresenter; // If the templatedParent is a ContentPresenter, this TextBlock is generated from a DataTemplate
     }
    

    So, to fix this, you'll have to declare the TextBlock not in a ControlTemplate, or workaround with a code like this (difficult to generalize to a whole app...):

    public class LabeledComboBox : Control
    {
        static LabeledComboBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(LabeledComboBox), new FrameworkPropertyMetadata(typeof(LabeledComboBox)));
        }
    
        // define our own peer
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new LabeledComboBoxAutomationPeer(this);
        }
    
        protected class LabeledComboBoxAutomationPeer : FrameworkElementAutomationPeer
        {
            public LabeledComboBoxAutomationPeer(LabeledComboBox owner) : base(owner)
            {
            }
    
            // replace all TextBlockAutomationPeer by our custom peer for TextBlock
            protected override List<AutomationPeer> GetChildrenCore()
            {
                var list = base.GetChildrenCore();
                for (int i = 0; i < list.Count; i++)
                {
                    var tb = list[i] as TextBlockAutomationPeer;
                    if (tb != null)
                    {
                        list[i] = new InteractiveTextBlockAutomationPeer((TextBlock)tb.Owner);
                    }
                }
                return list;
            }
        }
    
        // just do the default stuff, instead of the strange TextBlockAutomationPeer implementation
        protected class InteractiveTextBlockAutomationPeer : FrameworkElementAutomationPeer
        {
            public InteractiveTextBlockAutomationPeer(TextBlock owner) : base(owner)
            {
            }
    
            protected override AutomationControlType GetAutomationControlTypeCore()
            {
                return AutomationControlType.Text;
            }
    
            protected override string GetClassNameCore()
            {
                return "TextBlock";
            }
        }
    }
    

    One other solution is to create your own TextBlock control class (deriving from TextBlock) and override OnCreateAutomationPeer to return the custom one.