Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., CTO at artsy.net, fun at playplay.io, NYC

Email Twitter LinkedIn Github

In a previous post I had implemented clicking through menus with White. Someone pointed out that this was a solved problem and I didn’t need to write any code.

Window mainWindow = installerEditor.GetWindow("Installer Editor", InitializeOption.NoCache);
MenuBar mainWindowMenuBar = mainWindow.MenuBar;
mainWindowMenuBar.MenuItem("File", "Save");

What I noticed next is that it would take anywhere from 3 to 10 seconds to resolve mainWindow.MenuBar. I looked at the implementation and it does a descendents (DFS?) search. The menu bar is a child of the window, so we can avoid the recursion altogether.

private static DictionaryMappedItemFactory _factory = new DictionaryMappedItemFactory();

public static T Find<T>(UIItem item, string name) where T : UIItem
{
    AutomationElementFinder finder = new AutomationElementFinder(item.AutomationElement);
    AutomationElement found = finder.Child(AutomationSearchCondition.ByName("Application"));
    return (T) _factory.Create(found, item.ActionListener);
}

Generalizing this further, a simple tree walker with a depth limit seems to be much more efficient for both scenarios of deep and wide trees. The following code comes from this thread, I didn’t write it.

private static AutomationElement Find(AutomationElement element, string name, int maxDepth)
{
    if (maxDepth == 0)
    {
        return null;
    }
    TreeWalker walker = TreeWalker.RawViewWalker;
    AutomationElement current = walker.GetFirstChild(element);
    while (current != null)
    {
        if ((string)current.GetCurrentPropertyValue(AutomationElement.NameProperty) == name)
        {
            return current;
        }
        current = walker.GetNextSibling(current);
    }
    current = walker.GetFirstChild(element);
    while (current != null)
    {
        AutomationElement found = Find(current, name, maxDepth - 1);
        if (found != null)
        {
            return found;
        }
        current = walker.GetNextSibling(current);
    }
    return null;
}

private static UIItem Find(UIItem item, string name, int maxDepth)
{
    AutomationElement element = Find(item.AutomationElement, name, maxDepth);
    if (element == null) return null;
    return (UIItem)_factory.Create(element, item.ActionListener);
}

public static T Find<T>(UIItem item, string name) where T : UIItem
{
    return (T)Find(item, name, 4);
}

The new test code executes magnitudes faster to click File –> Save.

Window mainWindow = installerEditor.GetWindow("Installer Editor", InitializeOption.NoCache);
UIAutomation.Find<MenuBar>(mainWindow, "Application").MenuItem("File", "Save").Click();

Another interesting aspect of this menu bar is that it is virtualized. This means that while you may be holding an instance of a MenuBar, it may have been hidden by another UI element at some point and will now throw an exception with the UIA_E_ELEMENTNOTAVAILABLE error code if you try to use it. This is annoying: if I need to click on 10 menu items, I have to do 10 searches, starting from the top every time! To solve this, .NET 4.0 has introduced a new IUIAutomationVirtualizedItemPattern that has a Realize method to materialize the object again.