9

I am a user of NordVPN and using it without any issue. Now for some requirements I need to set some of its properties like protocol (checkbox) and clicking on buttons from some other application.

But that area of the application looks like a custom control and UIAutomation is not able to drill down into it.

Elements inside that custom control does not have any automation id.

So I need to know how we could traverse through user controls in wpf applications like other parts of application window using UIAutomation and White Framework.

What I have tried so far is

  1. using TreeWalker (not able to read all elements)

  2. And try to get the element from its location AutomationElement.FromPoint(), but it gives the whole custom control (determine from its bounds) again on which I can't traverse yet.

Any suggestion on how could I drill into custom control from UIAutomation.

enter image description here

For the record, snoop can read the elements but VisualUIAVerify.exe is not.

Manvinder
  • 4,495
  • 16
  • 53
  • 100
  • did you use inspect.exe from Windows SDK? – kurakura88 Aug 24 '17 at 05:14
  • Apparently NordVPN has not exposed the inner details of each tab to UI Automation (I have just tested this with inspect.exe). You will have to try a different tool (maybe one that uses image recognition like Sikuli or QTP) or contact NordVPN's support to ask if they have any suggestions. – o_weisman Aug 24 '17 at 08:16
  • @o_weisman Snoop and Wpf inspector can look into it, I believe they have easier approach – Manvinder Aug 24 '17 at 08:44
  • I'm not sure to what extent they can manipulate the actual controls. I also find it very odd that WPF controls are not exposed to UI Automation. WPF exposes controls by default unless the controls are entirely custom. – o_weisman Aug 24 '17 at 11:02
  • This app seems based on http://caliburnmicro.com/, etc. maybe this doesn't support UI automation well (it does a lot of native tricks to produce that look). – Simon Mourier Aug 28 '17 at 07:50
  • @SimonMourier I have developed apps using Caliburn Micro and it was not this that is blocking us view the controls. Moreover apart from this section rest of the app is pretty easy to automate like switching between tabs and searching etc – Manvinder Aug 29 '17 at 10:29
  • 1
    This may work - try to attach to an external app the way Snoop does (https://stackoverflow.com/questions/24045183/what-technique-does-snoop-uses-to-inspect-a-wpf-application), and when you have the app window - traverse the visual tree. Once you find the controls you need to automate - copy x:Name to x:AutomationProperties.Name (or generate the name yourself). – Maciek Świszczowski Aug 29 '17 at 11:07

1 Answers1

3

EDIT 1 - Clarification: For me a control is not visible in automation API - if there is no ability to uniquely identify them to automate on. The identification factor can be anything from name, id, sibling, or parent.


As expected, the absence of automation-ids have resulted in the controls to be not visible in UI Automation tree APIs.

In order to work around that, and knowing that they are visible in Snoop application - you can use the underlying logic (that Snoop uses) to programmitically automate these controls.

Steps

  1. Download the binaries for SnoopUI, and add them to your project. Make sure to keep the compile option as 'None' and are copied to output directory.

    enter image description here

  2. Next step would be add a helper method, that uses these binaries to inject your dll with automation logic into target application (which is NordVPN) in this case. Once the dll is injected into target process, the ManagedInjector also invokes the method that is sent as parameter.

    public class Helper
    {
        public static void Inject(IntPtr windowHandle, Assembly assembly, string className, string methodName)
        {
            var location = Assembly.GetEntryAssembly().Location;
            var directory = Path.GetDirectoryName(location);
            var file = Path.Combine(directory, "HelperDlls", "ManagedInjectorLauncher" + "64-4.0" + ".exe");
    
            Debug.WriteLine(file + " " + windowHandle + " \"" + assembly.Location + "\" \"" + className + "\" \"" + methodName + "\"");
            Process.Start(file, windowHandle + " \"" + assembly.Location + "\" \"" + className + "\" \"" + methodName + "\"");
        }
    }
    
  3. After the automation dll is injected in the application, the access to Visual Tree is pretty simple using Dispatcher and PresentationSources.

    public class Setup
    {
        public static bool Start()
        {
            Dispatcher dispatcher;
            if (Application.Current == null)
                dispatcher = Dispatcher.CurrentDispatcher;
            else
                dispatcher = Application.Current.Dispatcher;
    
            dispatcher.Invoke(AutomateApp);
            return true;
        }
    
        public static void AutomateApp()
        {
            Window root = null;
            foreach (PresentationSource presentationSource in PresentationSource.CurrentSources)
            {
                root = presentationSource.RootVisual as Window;
    
                if (root == null)
                    continue;
    
                if ("NordVPN ".Equals(root.Title))
                    break;
            }
    
  4. Getting access to VisualTree is easy, but identifying the controls is not that simple, as there are no automation-id(s), or name(s) that can uniquely identify these controls. But fortunately, as they are using MVVM, it is possible to identify them using the binding(s) attached with them.

    public static T GetChildWithPath<T>(this DependencyObject depObj, DependencyProperty property = null, string pathName = null) where T : DependencyObject
    {
        T toReturn = null;
    
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);
            bool pathNameMatch = (child is T) && child.IsPathNameMatch<T>(property, pathName);
            if (pathNameMatch)
            {
                toReturn = child as T;
                break;
            }
            else
                toReturn = GetChildWithPath<T>(child, property, pathName);
    
            if (toReturn != null)
                break;
        }
        return toReturn;
    }
    
  5. Once you have access to the controls, it is now possible to either manipulate their properties directly, or access their corresponding automation peers, and providers to automate these controls.

    var checkBoxNames = new[]
    {
        "CyberSec", "AutomaticUpdates", "AutoConnect",
        "StartOnStartup", "KillSwitch", "ShowNotifications",
        "StartMinimized", "ShowServerList", "ShowMap",
        "UseCustomDns", "ObfuscatedServersOnly"
    };
    
    foreach(var path in checkBoxNames)
    {
        var chkBox = settingsView.GetChildWithPath<CheckBox>(CheckBox.IsCheckedProperty, path);
        if(chkBox != null && chkBox.IsEnabled)
            chkBox.SimulateClick();
    }
    

A complete working sample has been uploaded at Github repository.

enter image description here

Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
  • 1
    I think your approach is very interesting, just have to wonder about your statement that absence of AutomationId (which is the Name property in WPF) causes the control to be invisible to UI Automation clients. I see many Automation controls with empty AutomationIds when the developers do not bother to assign names to the controls. – o_weisman Sep 17 '17 at 07:06
  • @o_weisman: I guess I could have chosen the words better. What I meant was the ability to uniquely identify or control them using automation APIs. Although, WPF is known to intentionally hide some controls from the automation tree, if these properties are absent, for performance. – Sharada Gururaj Sep 17 '17 at 11:57
  • In this case - I had to resort to visual tree because the only identification factor was the binding associated with the checkbox. – Sharada Gururaj Sep 17 '17 at 12:03