Thursday, June 13, 2019

Responding to another application's dialog box in code

I am researching automated testing of WPF MVVM classes and ran into a problem with ViewModels that popup dialog boxes. When I instantiate the view model and execute a routed command, it may popup up a dialog box or a message box that the user has to respond to in order to continue. I normally know that a dialog box will be displayed and I know something about it.

For example, in the application I'm testing, I know that if I execute the CloseCommand routed command on the transmittal management view model it will display a confirmation message box like this. It has a known caption and I know the buttons it will display. With this information I can find the window and close it.


I will have to use unmanaged code to do this.

Step one is to use EnumWindows to find all open windows.
Step two is to use GetWindowText to get each window's caption.
Step three is to use EndDialog to close the target window.

My UI thread is busy waiting for the window to close so I need to find and close it on a different thread. BackgroundWorker is the obvious solution.

I originally thought I would solve this problem using SendKey, but that proved to be a dead-end. Nevertheless, the project is called SendKey.

Start a new C# WPF project called SendKey. I'm using Visual Studio 2019. You can target any framework you want,

Add a new window called TestWindow. MainWindow will launch TestWindow's view model and execute a routed command. That command will display a dialog box. At the same time MainWindow will launch a background worker that monitors windows until it finds the dialog box. It will then close the dialog box and exit.

TestWindow.xaml simply defines a routed command.


<Window x:Class="SendKey.TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SendKey"
        mc:Ignorable="d"
        Title="TestWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="PromptCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource PromptCommand}" Executed="CommandBinding_Executed"/>
    </Window.CommandBindings>
    <Grid/>
</Window>

TestWindow.xaml.cs defines an Executed method that displays a dialog box and makes sure it is closed with a result of "Yes".


using System.Windows;
using System.Windows.Input;

namespace SendKey
{
    public partial class TestWindow : Window
    {
        public TestWindow()
        {
            InitializeComponent();
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBoxResult result;
            result = MessageBox.Show("Are you sure?", "Test", MessageBoxButton.YesNo);
            if (result != MessageBoxResult.Yes)  MessageBox.Show(result.ToString());
        }
    }
}

MainWindow.xml only contains a loaded event definition. All the fun is in the code.


<Window x:Class="SendKey.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SendKey"
        mc:Ignorable="d"
        Loaded="Window_Loaded"
        Title="MainWindow" Height="450" Width="800">
 </Window>

Here's the interesting bit.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Input;

namespace SendKey
{
    public partial class MainWindow : Window
    {
        protected delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        protected static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount);
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        protected static extern int GetWindowTextLength(IntPtr hWnd);
        [DllImport("user32.dll")]
        protected static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
        [DllImport("user32.dll")]
        protected static extern bool IsWindowVisible(IntPtr hWnd);
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool EndDialog(IntPtr hWnd, int Result);

        Dictionary<IntPtr, String> WindowList = new Dictionary<IntPtr, string>();

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Create the background worker and start it
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += Bw_DoWork;
            bw.RunWorkerAsync();

            // Instantiate the TestWindow and test the PromptCommand functionality
            TestWindow tw = new TestWindow();
            RoutedCommand rc = (RoutedCommand)tw.FindResource("PromptCommand");
            rc.Execute(null, tw);

            // The PromptCommand returned - which is a good thing
            this.Content = "Done";
        }

        // Bw_DoWork and EnumTheWindows would normally be in a core library somewhere
        private void Bw_DoWork(object sender, DoWorkEventArgs e)
        {
            Stopwatch sw = new Stopwatch();
            bool IsClosed = false;

            sw.Start();
            while (sw.ElapsedMilliseconds < 60000 && !IsClosed)
            {
                // Get a list of all windows
                WindowList.Clear();
                EnumWindows(new EnumWindowsProc(EnumTheWindows), IntPtr.Zero);
                // Look for the window with the desired caption
                foreach (KeyValuePair<IntPtr, String> oKVP in WindowList)
                {
                    if (oKVP.Value == "Test")
                    {
                        // and try to close it in the desired way
                        if (EndDialog(oKVP.Key, (int)System.Windows.Forms.DialogResult.Yes))
                        {
                            IsClosed = true;
                            return;
                        }
                    }
                }
                // Didn't find the window? Wait one second and try again
                System.Threading.Thread.Sleep(1000);
            }
            // Couldn't close the window in one minute? Give up
            Console.WriteLine("Gave up");
        }

        // EnumTheWindows has an odd design. It calls this method for each window found
        // I grab the caption and hWnd and store them in a global dictionary for later evaluation
        private bool EnumTheWindows(IntPtr hWnd, IntPtr lParam)
        {
            int size = GetWindowTextLength(hWnd);
            if (size++ > 0 && IsWindowVisible(hWnd))
            {
                StringBuilder sb = new StringBuilder(size);
                GetWindowText(hWnd, sb, size);
                WindowList.Add(hWnd, sb.ToString());
            }
            return true;
        }
    }
}

This code takes advantage of the fact that message boxes are top-level windows, not a child of the calling window, so it's immediately returned by EnumWindows.

Here's the application in action.



No comments:

Post a Comment