События UI Automation поднимаются дважды

У меня проблемы с прослушиванием событий автоматизации изнутри процесса. Я написал пример ниже, где у меня есть простое приложение WPF с одной кнопкой. Добавлен обработчик автоматизации для события Invoke в окне TreeScope: Потомки.

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

Почему вызванное событие обрабатывается дважды?

Если я удаляю Task.Run, я получаю его только один раз, как хочу, но я прочитал несколько мест, в которых нельзя вызывать код автоматизации из потока пользовательского интерфейса (например, https://msdn.microsoft.com/en-us/library/ms788709(v=vs.110).aspx). Это также непрактично для меня в реальном коде.

В этом примере я использую библиотеку UIAComWrapper, но я получаю одинаковое поведение как с управляемой, так и с COM-версией библиотеки UIAutomationClient.

2 ответа

Сначала я подумал, что это может быть какое-то всплывающее событие, которое мы видим, поэтому переменная с s отлитый как AutomationElement был добавлен в лямбду-обработчик, чтобы показать, происходит ли второй вызов также от кнопки (согласно комментарию @Simon Mourier, результат: значения yes идентичны), а не от его составляющей метки или чего-либо еще вверх или вниз по визуальному дерево.

После того, как это было исключено, более внимательный взгляд на стеки вызовов двух обратных вызовов выявил нечто, что поддерживает гипотезу, связанную с потоками. Я загрузил UIAComWrapper из git, скомпилировал из исходного кода и отладил с использованием символов исходного сервера и встроенного.

Это стек вызовов в первом обратном вызове:

стек вызовов при первом вызове

Это показывает, что отправной точкой является насос сообщений. Ядро WndProc запутывается в этом невероятно толстом слое фреймворка, почти в кратком изложении всех версий Windows, старательно декодируя его левой мышкой, пока он не окажется в OnClick() обработчик класса кнопки, откуда происходит подписанное событие автоматизации и направляется к нашей лямде. Пока ничего неожиданного.

И это стек вызовов во втором обратном вызове:

стек вызовов во втором callbacl

Это показывает, что второй обратный вызов является артефактом UIAutomationCore, И еще: он запускается в пользовательском потоке, а не в потоке пользовательского интерфейса. Таким образом, очевидно, существует механизм, который гарантирует, что каждый поток, который подписался, получает копию, а поток пользовательского интерфейса всегда делает.

К сожалению, все аргументы, которые попадают в лямбду, идентичны для первого и второго вызова. И сравнение стеков вызовов, хотя и возможно, будет решением даже хуже, чем хронометраж / подсчет событий.

Но: Вы можете фильтровать события по потокам и использовать только одно из них:

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://stackru.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

Я не уверен, что это решит эту конкретную проблему, но, возможно, стоит отметить, что в Windows 10 теперь есть IUIAutomation6, и в частности IUIAutomation6::put_CoalesceEvents, который утверждает, что обнаруживает и фильтрует повторяющиеся события.

Другие вопросы по тегам