UI自动化事件被触发两次

8

我在进程内部监听自动化事件时遇到了问题。我写了一个简单的WPF应用程序,其中只有一个按钮。在窗口上添加了一个Automation处理程序,用于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版本时我得到了相同的行为。

我可以重现这个问题。但是,我无法从另一个进程中重现它(只有一个事件被触发)。这可能是一个错误,但UIA是为了处理外部进程而设计的,你为什么要在同一进程中执行此操作呢? - Simon Mourier
1
仅适用于VSTO。Excel API无法让您访问所有按钮点击。自动化可用于检测这些,但它们会出现两次。 - jan
好的。嗯,我能找到的唯一不太好的解决方案是:1)计时事件,2)确定源是否为同一对象(在这里比较事件源的 GetHashCode 似乎是合理的,因为它是 AutomationElement),3)如果两个事件在 x 毫秒内发生,则宣布它是重复的。 - Simon Mourier
是的。我现在处理的方式基本上是每隔一秒过滤一次。它非常可靠,每秒两次。但在代码中看起来不好。 - jan
你可以查看这个线程,它可能会有所帮助。 - ilya korover
2个回答

5

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

排除了那个情况后,更仔细地查看了两个回调的调用堆栈,发现了一些支持与线程相关的假设的东西。我从 git 下载了 UIAComWrapper,从源代码编译并使用源服务器符号和本机调试。

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

call stack in first invokation

这表明,消息泵是起点。核心的WndProc通过那厚厚的框架层向上冒泡,几乎重复了所有Windows版本的解码过程,并将其解码为左键抬起,最终到达按钮类的OnClick()处理程序,从那里订阅的自动化事件被触发并指向我们的lambda函数。到目前为止还没有什么意外的。

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

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: https://dev59.com/wG435IYBdhLWcg3w50j4#14280425 */
                        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

Dispatcher.Invoke是一种调用UI线程的方式,而这正是我想要避免的。 - jan
你说得对 - 那是胡言乱语(因为太渴望奖励了)。我已经更新了答案,希望这次分析更有用。 - Cee McSharpface
很好的发现。线程检查解决了我的问题。我检查了Apartment状态而不是使用Dispatcher技巧,因为这是一个VSTO程序。感谢你的深入探讨。 - jan
@CeeMcSharpface 在C++中有没有类似的方法?比如特定的API调用?谢谢任何信息。 - user13366655
我不知道有没有 - 你在C++/CLI环境中是否遇到了同样的现象?如果是,我建议你提出一个新问题。 - Cee McSharpface

0

我不确定它是否能解决这个特定的问题,但值得注意的是,Windows 10现在有了IUIAutomation6,特别是IUIAutomation6::put_CoalesceEvents,它声称可以检测和过滤重复事件。


如何在.NET中实现此功能? - demberto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接