Search code examples
c#winformswinapipinvokemstest

Control's parent handle points to WindowsFormsParkingWindow using p/invoke


Consider the following unit test for WinApi functionality:

public class WinApiTest
{
  [TestMethod]
  public void WinApiFindFormTest_SimpleNesting()
  {
    var form = new Form();
    form.Text = @"My form";

    var button = new Button();
    button.Text = @"My button";

    form.Controls.Add(button);
    //with below line commented out, the test fails
    form.Show();

    IntPtr actualParent = WinApiTest.FindParent(button.Handle);
    IntPtr expectedParent = form.Handle;

    //below 2 lines were added for debugging purposes, they are not part of test
    //and they don't affect test results
    Debug.WriteLine("Actual: " + WinApi.GetWindowTitle(actualParent));
    Debug.WriteLine("Expected: " + WinApi.GetWindowTitle(expectedParent));

    Assert.AreEqual(actualParent, expectedParent);
  }

  //this is a method being tested
  //please assume it's located in another class
  //I'm not trying to test winapi
  public static IntPtr FindParent(IntPtr child)
  {
    while (true)
    {
      IntPtr parent = WinApi.GetParent(child);
      if (parent == IntPtr.Zero)
      {
        return child;
      }
      child = parent;
    }
  }
}

Problem is that to make it work, I have to show the form, i.e. do form.Show(), otherwise, it fails with this output:

Actual: WindowsFormsParkingWindow
Expected: My form
Exception thrown: 'Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException' in Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

I read about this mysterious WindowsFormsParkingWindow, and it seems to be relevant only if there was no parent specified. So all controls without a parent live under this window. In my case, however, button was clearly specified to be part of form's controls.

Question: Is there a proper way to make this test pass? I'm trying to test FindParent method. In true spirit of unit tests, nothing should suddenly pop up in front of the user. It's possible to do Show and Hide sequence, but I think it's a rather hack-ish approach to solve the problem.

Code for WinApi class is provided below - it does not add much value to the question, but if you absolutely must see it, here it goes (major portion comes from this answer on SO):

public class WinApi
{
  /// <summary>
  ///  Get window title for a given IntPtr handle.
  /// </summary>
  /// <param name="handle">Input handle.</param>
  /// <remarks>
  ///  Major portition of code for below class was used from here:
  ///  https://stackoverflow.com/questions/4604023/unable-to-read-another-applications-caption
  /// </remarks>
  public static string GetWindowTitle(IntPtr handle)
  {
    if (handle == IntPtr.Zero)
    {
      throw new ArgumentNullException(nameof(handle));
    }
    int length = WinApi.SendMessageGetTextLength(handle, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
    if (length > 0 && length < int.MaxValue)
    {
      length++; // room for EOS terminator
      StringBuilder windowTitle = new StringBuilder(length);
      WinApi.SendMessageGetText(handle, WM_GETTEXT, (IntPtr)windowTitle.Capacity, windowTitle);
      return windowTitle.ToString();
    }
    return String.Empty;
  }

  const int WM_GETTEXT = 0x000D;
  const int WM_GETTEXTLENGTH = 0x000E;

  [DllImport("User32.dll", EntryPoint = "SendMessage")]
  private static extern int SendMessageGetTextLength(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
  [DllImport("User32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
  private static extern IntPtr SendMessageGetText(IntPtr hWnd, int msg, IntPtr wParam, [Out] StringBuilder lParam);
  [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
  public static extern IntPtr GetParent(IntPtr hWnd);
}

Solution

  • When you access the Handle property, the window needs to be created. Child windows need to have a parent window, and if the parent window has not yet been created, the child window is created with the parking window as its parent. Only when the parent window is created, does the child window get re-parented.

    IntPtr actualParent = WinApiTest.FindParent(button.Handle);
    IntPtr expectedParent = form.Handle;
    

    When you access button.Handle, the button's window is created, but since the form's window is not yet created, the parking window is the parent. The simplest way for you to handle this is to ensure that the form's window is created before the buttons's window. Make sure that you refer to form.Handle, before you call GetParent on the button's handle, for example in your test you could reverse the order of assignment:

    IntPtr expectedParent = form.Handle;
    IntPtr actualParent = WinApiTest.FindParent(button.Handle);
    

    Obviously you'd want to comment this code so that a future reader knew that the order of assignment was critical.

    I do wonder however why you feel the need to make a test like this however. I cannot imagine this sort of testing revealing a bug in your code.