Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UI Automation events is raised twice

I'm having trouble listening to automation events from the inside a process. I've written a sample below where I have a simple WPF application with a single button. An automation handler is added for the Invoke event on the window with TreeScope: Descendants.

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

When I click the button, this is what I get:

Invoked:20009
Clicked!
Invoked:20009

Why is the Invoked event handled twice?

If I remove the Task.Run I only get it once like I want, but I've read several places that you should not call automation code from the UI thread (e.g. https://msdn.microsoft.com/en-us/library/ms788709(v=vs.110).aspx). It's also impractical for me to do so in the real code.

I use the UIAComWrapper library in this sample, but I get the same behavior with both the managed and COM version of the UIAutomationClient library.

like image 916
jan Avatar asked Jan 18 '17 13:01

jan


1 Answers

At first I thought it may be some kind of event bubbling we see, so a variable with s casted as AutomationElement has been added inside the handler lambda to show whether the second invokation also comes from the button (according to the comment of @Simon Mourier, result: yes values are identical) and not from its constituent label, or anything else up or down the visual tree.

After that was ruled out, a closer look at the call stacks of the two callbacks revealed something, that supports the thread-related hypothesis. I downloaded UIAComWrapper from git, compiled from source and debugged with source server symbols and native on.

This is the call stack in the first callback:

call stack in first invokation

It shows, that the point of origin is the message pump. The core WndProc bubbles it up through that incredibly thick layer of framework stuff, almost in a recapitulation of all Windows versions ever, drudgingly decoding it as a left mouse up, until it ends up in the OnClick() handler of the button class, from where the subscribed automation event is raised and directed towards our lamda. Nothing unexpected so far.

And this is the call stack in the second callback:

call stack in second callbacl

This reveals that the second callback is an artifact of the UIAutomationCore. And: it runs on the user thread, not on the UI thread. So there is apparently a mechanism which ensures that every thread that subscribed, gets a copy, and the UI thread always does.

Unfortunately, all the arguments that end up in the lambda, are identical for the first and the second call. And comparing call stacks, albeit possible, would be a solution even worse than timing/counting events.

But: You can filter the events by thread, and consume only one of them:

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

Result in output window:

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
like image 104
Cee McSharpface Avatar answered Oct 20 '22 19:10

Cee McSharpface