Monday, February 8, 2010

AutomationElement.SetFocus() is Unreliable

Microsoft's UiAutomation framework has been fairly useful in my quest to automate all our GUI testing.  My previous solution involved Ruby, WinUser32Ruby, and RSpec (which was a fun solution), but I got tired of try to wrap Win32 with Ruby.

UiAutomation seemed to solve all my problems right up until I tried to bring an AutomationElement to the foreground with a call to AutomationElement.SetFocus().  It would randomly fail.  About 90% of the time the window in question would pop up and the test would pass with flying colors.  But every once in a while the test would fail.  Some quick debugging lead me to question the reliability of SetFocus().

Rather than monkey around with SetFocus(), Full Trust, and the .NET Security Framework I decide to rely on good old Win32.  For everyone who's being driven crazy by SetFocus(), all three of us, here is my win32 based SetForegroundWindow() method.

public bool SetForegroundWindow (AutomationElement elm, uint retries)
{
    Util.ValidateNotNull ("elm", elm);

    try
    {
        if (retries < RETRY_LIMIT)
        {
            // Using Win32 to set foreground window because
            // AutomationElement.SetFocus() is unreliable

            // Get handle to the element
            IntPtr other = FindWindow (null, elm.Current.Name);

            // Get the Process ID for the element we are trying to
            // set as the foreground element
            int other_id = GetWindowThreadProcessId (
                other, IntPtr.Zero);

            // Get the Process ID for the current process
            int this_id = GetWindowThreadProcessId (
                Process.GetCurrentProcess ().Handle, IntPtr.Zero);

            // Attach the current process's input to that of the 
            // given element. We have to do this otherwise the
            // WM_SETFOCUS message will be ignored by the element.
            bool success = 
                AttachThreadInput (this_id, other_id, true);

            // Make the Win32 call
            IntPtr previous = SetForegroundWindow (other);

            if (IntPtr.Zero.Equals(previous))
            {
                // Trigger re-try
                throw new Exception(
                    "SetForegroundWindow failed");
            }
            else
            {
                Log ("  focus set");
            }

            return true;
        }

        // Exceeded retry limit, failed!
        return false;
    }
    catch
    {
        retries++;
        uint time = retries * RETRY_FACTOR;
        Log ("  Could not SetFocus(), retry in {0} ms", time);

        Thread.Sleep ((int)time);

        return SetForegroundWindow (elm, retries);
    }
}
This method relies on Win32 calls, so you must import the requisite Win32 functions.
using System.Runtime.InteropServices;

...

[DllImport ("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow (
  string lpClassName, string lpWindowName);

[DllImport ("user32.dll", CharSet = CharSet.Auto)]
static extern bool AttachThreadInput (
    int idAttach, int idAttachTo, bool fAttach);

[DllImport ("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowThreadProcessId (
    IntPtr hWnd, IntPtr lpdwProcessId);

[DllImport ("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SetForegroundWindow (IntPtr hWnd);

1 comment:

  1. Thank you so much! This saved me tons of headache with SetFocus().

    ReplyDelete