一段时间后,UI自动化事件在监视应用程序后停止接收,然后在一段时间后重新启动

o_w*_*man 11 c# ui-automation microsoft-ui-automation wpf-4.0

我们正在使用Microsoft的UIAutomation框架来开发一个监视特定应用程序事件的客户端,并以不同的方式响应它们.我们已经开始使用该框架的托管版本,但由于延迟问题,转移到UIACOMWrapper包含的本机版本.在我们(大规模)WPF应用程序中出现更多性能问题后,我们决定将其移至单独的终端应用程序(通过UDP将事件传输到我们的WPF应用程序),这似乎解决了所有性能问题.唯一的问题是,似乎每隔几分钟,TabSelection,StructureChanged,WindowOpened和WindowClosed的事件就会被捕获几分钟.令人惊讶的是,当发生这种情况时,仍会接收和处理PropertyChanged事件.我将发布我们的事件监视器的相关代码,但这可能无关紧要,因为我们在使用Microsoft自己的AccEvent实用程序时已经看到类似的行为.我不能发布受监控应用程序的代码,因为它是专有的和保密的,我可以说它是一个承载WPF窗口的WinForms应用程序,也非常庞大.有没有人在使用UI自动化框架时看到过这种行为?感谢您的时间.

这是监视器代码(我知道事件处理在这里的UI自动化线程上,但是将它移动到专用线程并没有改变任何东西):

        public void registerHandlers()
    {
        //Register on structure changed and window opened events 
        System.Windows.Automation.Automation.AddStructureChangedEventHandler(
            this.getMsAutomationElement(), System.Windows.Automation.TreeScope.Subtree, this.handleStructureChanged);
        System.Windows.Automation.Automation.AddAutomationEventHandler(
            System.Windows.Automation.WindowPattern.WindowOpenedEvent,
            this.getMsAutomationElement(),
            System.Windows.Automation.TreeScope.Subtree,
            this.handleWindowOpened);
        System.Windows.Automation.Automation.AddAutomationEventHandler(
            System.Windows.Automation.WindowPattern.WindowClosedEvent,
            System.Windows.Automation.AutomationElement.RootElement,
            System.Windows.Automation.TreeScope.Subtree,
            this.handleWindowClosed);

        this.registerValueChanged();
        this.registerTextNameChange();
        this.registerTabSelected();
        this.registerRangeValueChanged();
    }

    private void registerRangeValueChanged()
    {
        if (this.getMsAutomationElement() != null)
        {
            System.Windows.Automation.Automation.AddAutomationPropertyChangedEventHandler(
                    this.getMsAutomationElement(),
                    System.Windows.Automation.TreeScope.Subtree, this.handlePropertyChange,
                    System.Windows.Automation.RangeValuePattern.ValueProperty);
        }
    }

    private void unregisterRangeValueChanged()
    {
        System.Windows.Automation.Automation.RemoveAutomationPropertyChangedEventHandler(
                this.getMsAutomationElement(),
                this.handlePropertyChange);
    }

    private void registerValueChanged()
    {
        if (this.getMsAutomationElement() != null)
        {
            System.Windows.Automation.Automation.AddAutomationPropertyChangedEventHandler(
                this.getMsAutomationElement(),
                System.Windows.Automation.TreeScope.Subtree, this.handlePropertyChange,
                System.Windows.Automation.ValuePattern.ValueProperty);
        }
    }

    private void unregisterValueChanged()
    {
        System.Windows.Automation.Automation.RemoveAutomationPropertyChangedEventHandler(
                            this.getMsAutomationElement(),
                            this.handlePropertyChange);
    }

    private void registerTextNameChange()
    {
        if (this.getMsAutomationElement() != null)
        {
            System.Windows.Automation.Automation.AddAutomationPropertyChangedEventHandler(
            this.getMsAutomationElement(),
            System.Windows.Automation.TreeScope.Subtree, this.handlePropertyChange,
                System.Windows.Automation.AutomationElement.NameProperty);
        }
    }

    private void unregisterTextNameChange()
    {
        System.Windows.Automation.Automation.RemoveAutomationPropertyChangedEventHandler(
        this.getMsAutomationElement(),
        this.handlePropertyChange);
    }
    private void handleWindowOpened(object src, System.Windows.Automation.AutomationEventArgs e)
    {
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.WriteLine(DateTime.Now.ToShortTimeString() + " " + "Window opened:" + " " + 
            (src as System.Windows.Automation.AutomationElement).Current.Name);

        System.Windows.Automation.AutomationElement element = src as System.Windows.Automation.AutomationElement;
        //this.sendEventToPluginQueue(src, e, element.GetRuntimeId(), this.getAutomationParent(element).GetRuntimeId());
        //Fill out the fields of the control added message
        int[] parentId = this.getAutomationParent(element).GetRuntimeId();
        this.copyToIcdArray(parentId,
            this.protocol.getMessageSet().outgoing.ControlAddedMessage.Data.controlAdded.parentRuntimeId);
        this.copyToIcdArray(element.GetRuntimeId(),
            this.protocol.getMessageSet().outgoing.ControlAddedMessage.Data.controlAdded.runtimeId);
        //Send the message using the protocol
        this.protocol.send(this.protocol.getMessageSet().outgoing.ControlAddedMessage);
    }

    private void copyToIcdArray(int[] runtimeId, ICD.UI_AUTOMATION.RuntimeId icdRuntimeId)
    {
        icdRuntimeId.runtimeIdNumberOfItems.setVal((byte)runtimeId.Count());
        for (int i = 0; i < runtimeId.Count(); i++)
        {
            icdRuntimeId.runtimeIdArray.getElement(i).setVal(runtimeId[i]);
        }
    }

    private void handleWindowClosed(object src, System.Windows.Automation.AutomationEventArgs e)
    {
        if (src != null)
        {
            Console.ForegroundColor = ConsoleColor.Cyan;
            Console.WriteLine(DateTime.Now.ToShortTimeString() + " " + "Window closed:" + " " +
                (src as System.Windows.Automation.AutomationElement).GetRuntimeId().ToString());

            System.Windows.Automation.AutomationElement element = src as System.Windows.Automation.AutomationElement;
            this.copyToIcdArray(element.GetRuntimeId(),
                this.protocol.getMessageSet().outgoing.ControlRemovedMessage.Data.controlRemoved.runtimeId);
            //Send the message using the protocol
            this.protocol.send(this.protocol.getMessageSet().outgoing.ControlRemovedMessage);

            //this.sendEventToPluginQueue(src, e, element.GetRuntimeId());
        }
    }
Run Code Online (Sandbox Code Playgroud)

编辑:我忘了提到我强烈怀疑问题是UI-Automation事件处理程序线程之一以某种方式卡住了.我相信这个的原因是,当我的显示器出现问题时,我启动了一个AccEvent实例,它收到了我的显示器没有得到的所有丢失的事件.这意味着事件被触发但未传递给我的监视器.

编辑2:我忘了提到这种情况发生在Windows 8中运行的特定目标应用程序,我没有在我自己的Windows 7机器上看到这种现象与其他应用程序.另一个有趣的事情是,它似乎或多或少地定期发生,但无论何时我订阅事件,即它几乎可以在订阅之后立即发生,但随后需要几分钟才能重现.

小智 5

恐怕我不知道你看到的延迟的原因,但这里有一些关于这个的想法......

我在下面所说的一切都与 Windows 中的本机 UIA API 有关,而不是托管的 .NET UIA API。近年来对 UIA 的所有改进都是针对 Windows UIA API 进行的。因此,每当我编写 UIA 客户端 C# 代码时,我都会通过使用 tlbimp.exe SDK 工具生成的托管包装器调用 UIA。

也就是说,我首先使用如下命令生成包装器...

“C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\x64\tlbimp.exe”c:\windows\system32\uiautomationcore.dll /out:Interop.UIAutomationCore。 dll

然后我在我的 C# 项目中包含对 Interop.UIAutomationCore.dll 的引用,添加“使用 Interop.UIAutomationCore;” 到我的 C# 文件,然后我可以做这样的事情......

IUIAutomation uiAutomation = new CUIAutomation8();

IUIAutomationElement rootElement = uiAutomation.GetRootElement();

uiAutomation.AddAutomationEventHandler(
    20016, // UIA_Window_WindowOpenedEventId
    rootElement,
    TreeScope.TreeScope_Descendants,
    null,
    this);
Run Code Online (Sandbox Code Playgroud)

...

public void HandleAutomationEvent(IUIAutomationElement sender, int eventId)
{
    // Got a window opened event...
}
Run Code Online (Sandbox Code Playgroud)

在 Windows 7 中,UIA 事件处理程序有一些重要的限制。编写不考虑这些约束的事件处理程序很容易,这可能会导致与 UIA 交互时的长时间延迟。例如,重要的是不要在事件处理程序内部添加或删除 UIA 事件处理程序。所以当时,我故意从我的事件处理程序内部根本不进行 UIA 调用。相反,我会向自己发布一条消息或向队列添加一些操作,允许我的事件处理程序返回,并在不久之后在另一个线程上采取我想要的任何操作来响应该事件。这需要我做更多的工作,但我不想冒延误的风险。我创建的任何线程都将在 MTA 中运行。

上述操作的一个示例是在https://code.msdn.microsoft.com/windowsapps/Windows-7-UI-Automation-6390614a/sourcecode?fileId=21469&pathId=715901329 上的旧焦点跟踪示例中。文件 FocusEventHandler.cs 创建 MTA 线程并对消息进行排队以避免在事件处理程序内进行 UIA 调用。

从 Window 7 开始,我知道 UIA 中与线程和延迟相关的限制已经放宽,遇到延迟的可能性也降低了。最近,Windows 8.1 和 Windows 10 在这方面有了一些改进,所以如果在 Windows 10 上运行你的代码是可行的,那么看看延迟是否仍然存在会很有趣。

我知道这很耗时,但您可能有兴趣在事件处理程序中删除与 UIA 的交互并查看延迟是否消失。如果他们这样做了,那就是确定哪个操作似乎触发了问题,并查看是否有其他方法可以实现您的目标,而无需在事件处理程序中执行 UIA 交互。

例如,在您的事件处理程序中,您调用...

this.getAutomationParent(element).GetRuntimeId();

我希望这会导致两次调用返回到生成事件的提供程序应用程序。第一个调用是获取源元素的父元素,第二个调用是获取该父元素的 RuntimeId。因此,当 UIA 正在等待您的事件处理程序返回时,您已经两次调用回 UIA。虽然我不知道这是一个问题,但我会避免它。

有时,您可以通过将一些感兴趣的数据与事件本身一起缓存来避免对提供程序进程的跨进程回调。例如,假设我知道我将需要引发 WindowOpened 事件的元素的 RuntimeId。当我注册事件时,我可以要求 UIA 将这些数据与我收到的事件一起缓存。

int propertyRuntimeId = 30000; // UIA_RuntimeIdPropertyId
Run Code Online (Sandbox Code Playgroud)

...

IUIAutomationCacheRequest cacheRequestRuntimeId = uiAutomation.CreateCacheRequest();
cacheRequestRuntimeId.AddProperty(propertyRuntimeId);

uiAutomation.AddAutomationEventHandler(
    20016, // UIA_Window_WindowOpenedEventId
    rootElement,
    TreeScope.TreeScope_Descendants,
    cacheRequestRuntimeId,
    this);
Run Code Online (Sandbox Code Playgroud)

...

public void HandleAutomationEvent(IUIAutomationElement sender, int eventId)
{
    // Got a window opened event...

    // Get the RuntimeId from the source element. Because that data is cached with the
    // event, we don't have to call back through UIA into the provider process here.
    int[] runtimeId = sender.GetCachedPropertyValue(propertyRuntimeId);
}
Run Code Online (Sandbox Code Playgroud)

附带说明一下,在可行的情况下,我总是在通过 UIA 处理事件或访问元素时缓存数据,(通过使用诸如 FindFirstBuildCache() 之类的调用),因为我想避免尽可能多的跨进程调用。

所以我的建议是:

  1. 将本机 Windows UIA API 与由 tlbimp.exe 生成的托管包装器一起使用。
  2. 使用事件缓存尽可能多的数据,以避免以后不必要地回调提供程序进程。
  3. 避免从 UIA 事件处理程序内部回调到 UIA。

谢谢,

盖伊


Dor*_*onG 2

我在我的项目中看到了这种行为。解决方案是使用计时器取消订阅并重新订阅事件。此外,我在新任务(在 STA 线程池中运行)中启动事件后的任何操作。