UI自动化事件提升两次

时间:2017-01-18 13:16:03

标签: c# microsoft-ui-automation white

我无法从流程内部收听自动化事件。我在下面写了一个示例,其中有一个带有单个按钮的简单WPF应用程序。使用TreeScope:Descendants为窗口上的Invoke事件添加自动化处理程序。

public MainWindow()
{
    InitializeComponent();

    Loaded += OnLoaded;
}

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    IntPtr windowHandle = new WindowInteropHelper(this).Handle;
    Task.Run(() =>
    {
        var element = AutomationElement.FromHandle(windowHandle);

        Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, element, TreeScope.Descendants,
            (s, a) =>
            {
                Debug.WriteLine($"Invoked:{a.EventId.Id}");
            });

    });
}

private void button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Clicked!");
}

当我点击按钮时,这就是我得到的:

Invoked:20009
Clicked!
Invoked:20009

为什么Invoked事件被处理两次?

如果我删除了Task.Run,​​我只会按照自己的意愿获取它,但我已经读过几个你不应该从UI线程调用自动化代码的地方(例如https://msdn.microsoft.com/en-us/library/ms788709(v=vs.110).aspx)。在真实的代码中这样做也是不切实际的。

我在此示例中使用了UIAComWrapper库,但是我对UIAutomationClient库的托管版和COM版都有相同的行为。

1 个答案:

答案 0 :(得分:1)

起初我认为它可能是我们看到的某种事件冒泡,因此在处理程序lambda中添加了一个s转换为AutomationElement的变量,以显示第二个invokation是否也来自按钮(根据@Simon Mourier的评论,结果:是值相同),而不是来自其组成标签,或视觉树上或下的任何其他内容。

在排除之后,仔细观察两个回调的调用堆栈就会发现一些支持线程相关假设的东西。我从git下载了UIAComWrapper,从源代码编译并使用源服务器符号和本机调试。

这是第一个回调中的调用堆栈:

call stack in first invokation

它表明,原点是消息泵。核心WndProc通过这个令人难以置信的厚层框架内容将其冒充,几乎在所有Windows版本的重演中,苦苦地将其解码为鼠标左键,直到它最终出现在按钮类的OnClick()处理程序中,从订阅的自动化事件被引发到我们的lamda。到目前为止没有任何意外。

这是第二个回调中的调用堆栈:

call stack in second callbacl

这表明第二个回调是UIAutomationCore的工件。并且:它在用户线程上运行,而不是在UI线程上运行。因此,显然有一种机制可以确保订阅的所有线程,获取副本以及UI线程始终如此。

不幸的是,所有以lambda结尾的参数对于第一次和第二次调用都是相同的。比较调用堆栈虽然可能,但比计时/计数事件更糟糕。

但是:你可以按线程过滤事件,只消耗其中一个:

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Interop;
using System.Threading;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs routedEventArgs)
        {
            IntPtr windowHandle = new WindowInteropHelper(this).Handle;
            Task.Run(() =>
            {
                var element = AutomationElement.FromHandle(windowHandle);
                Automation.AddAutomationEventHandler(InvokePattern.InvokedEvent, element, TreeScope.Descendants,
                    (s, a) =>
                    {
                        var ele = s as AutomationElement;
                        var invokingthread = Thread.CurrentThread;
                        Debug.WriteLine($"Invoked on {invokingthread.ManagedThreadId} for {ele}, event # {a.EventId.Id}");
                        /* detect if this is the UI thread or not,
                         * reference: http://stackoverflow.com/a/14280425/1132334 */
                        if (System.Windows.Threading.Dispatcher.FromThread(invokingthread) == null)
                        {
                            Debug.WriteLine("2nd: this is the event we would be waiting for");
                        }
                        else
                        {
                            Debug.WriteLine("1st: this is the event raised on the UI thread");
                        }
                    });
            });
        }

        private void button_Click(object sender, RoutedEventArgs e)
        {
            Debug.WriteLine("Clicked!");
        }
    }
}

输出窗口中的结果:

Invoked on 1 for System.Windows.Automation.AutomationElement, event # 20009
1st: this is the event raised on the UI thread
Invoked on 9 for System.Windows.Automation.AutomationElement, event # 20009
2nd: this is the event we would be waiting for