События 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()
обработчик класса кнопки, откуда происходит подписанное событие автоматизации и направляется к нашей лямде. Пока ничего неожиданного.
И это стек вызовов во втором обратном вызове:
Это показывает, что второй обратный вызов является артефактом 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
, который утверждает, что обнаруживает и фильтрует повторяющиеся события.