我应该如何定义自定义控件以启用 UI Automation 和 TestStack White?

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

我开始使用 TestStack White(UI 自动化)在 WPF 现有应用程序中自动化测试。使用标准控件时一切正常。但是,我 运行 在尝试与自定义控件交互时遇到了问题。

例如,我有一个LabeledComboBox,它实际上是一个TextBlock加上一个ComboBox。这被定义为从 Control 派生的 class 加上 XAML:

中的 ControlTemplate

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>

此控件有效,但如果您 运行 UI Automation 验证 UI Automation 唯一可见的部分是 ComboBox,而无法访问 TextBlock。

但是,如果您使用 XAML 和代码隐藏将其创建为 UserControl,则 TextBox 和 ComboBox 都对 UI Automation 正确可见。

我已尝试为我的控件创建一个 AutomationPeer (FrameworkElementAutomationPeer),但到目前为止我无法使 TextBlock 对 UI Automation 可见。一个有趣的结果是 FrameworkElementAutomationPeer::GetChildrenCore() 正确 returns 2 个自动化对等点的列表,一个用于 TextBlock,一个用于 ComboBox。

我应该如何更改我的自定义控件,以便使用 UI Automation 和 White 对其进行正确测试?

如果 TextBlock (TextBlockAutomationPeer) 的默认自动化对等点是 ControlTemplate 的一部分,则它会从 UI 树中删除相应的所有者。

您可以在这里找到相关代码: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
 }

因此,要解决此问题,您必须声明 TextBlock 不在 ControlTemplate 中,或者使用类似这样的代码(很难推广到整个应用程序...):

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";
        }
    }
}

另一种解决方案是创建您自己的 TextBlock 控件 class(从 TextBlock 派生)并将 OnCreateAutomationPeer 覆盖为 return 自定义控件。