Adventures in automation: Dismissing all message boxes from a particular application but only if they say that the operation succeeded


Suppose you have a program that is part of your workflow, and it has the annoying habit of showing a message box when it is finished. You want to automate this workflow, and part of that automation is dismissing the message box.

Let's start by writing the annoying program:

#include <windows.h>

int WINAPI WinMain(
    HINSTANCE hinst, HINSTANCE hinstPrev,
    LPSTR lpCmdLine, int nCmdShow)
{
  Sleep(5000);
  MessageBox(nullptr, GetTickCount() % 1000 < 800 ?
             "Succeeded!" : "Failed!", "Annoying", MB_OK);
  return 0;
}

This annoying program pretends to do work for a little while, and then displays a message box saying whether or not it succeeded. (Let's say it succeeds 80% of the time.)

Our Little Program will automate this task and respond based on whether the operation succeeded. This is just a small extension of our previous program which logs the contents of every message box, except we are paying attention only to one specific message box.

To the rescue: UI Automation.

using System.Windows.Automation;
using System.Diagnostics;
using System.Threading;

class Program
{
 [System.STAThread]
 public static void Main(string[] args)
 {
  int processId = 0;
  bool succeeded = false;
  var resultReady = new ManualResetEvent(false);

  Automation.AddAutomationEventHandler(
   WindowPattern.WindowOpenedEvent,
   AutomationElement.RootElement,
   TreeScope.Children,
   (sender, e) =>
   {
    var element = sender as AutomationElement;

    if ((int)element.GetCurrentPropertyValue(
             AutomationElement.ProcessIdProperty) != processId)
    {
     return;
    }

    var text = element.FindFirst(TreeScope.Children,
      new PropertyCondition(AutomationElement.AutomationIdProperty,
                            "65535"));
    if (text != null && text.Current.Name == "Succeeded!")
    {
     succeeded = true;
     var okButton = element.FindFirst(TreeScope.Children,
       new PropertyCondition(AutomationElement.AutomationIdProperty,
                             "2"));
     var invokePattern = okButton.GetCurrentPattern(
       InvokePattern.Pattern) as InvokePattern;
     invokePattern.Invoke();
    }

    resultReady.Set();
   });

  // Start the annoying process
  Process p = Process.Start("annoying.exe");
  processId = p.Id;

  // Wait for the result
  resultReady.WaitOne();

  if (succeeded)
  {
   Process.Start("calc.exe");
  }

  Automation.RemoveAllEventHandlers();
 }
}

Most of this program you've seen before.

We register an automation event handler for new window creation that ignores windows that belong to processes we don't care about. That keeps us from being faked out by windows that happen to be created while our annoying task is running.

For simplicity's sake, I've removed other sanity checks which verify that the window which appeared is the one we actually care about. In real life, we might check the window class or the window title. I left that out because it's not really relevant to the story.

Once we think we have the window we want, we suck out the text so we can see whether the message was a success or failure message. If it was a success, we dismiss the dialog box by pushing the OK button; otherwise we leave the error message on the screen so the user can see what happened. Either way, we signal the main thread that we have a result.

After registering the event handler, we run the annoying process, tell the event handler the process ID, and wait for the signal. Once the signal arrives, we see whether it declared the operation a success, and if so, we proceed to the next step of, um, say, launching the calculator. (I just picked something arbitrary.)

Comments (13)
  1. John says:

    Is one of those "sanity checks" that is left as an exercise to the reader dealing with the possible race condition between launching the process and when UI Automation registers itself as an event handler? (Is this even an issue?). Consider the possibility of a program that just blows chow right out of the gate.

    I enjoy these UIAutomation Posts keep them coming!

    [Yes, there is a small race window if the program blows chunks before Process.Start returns. -Raymond]
  2. Brian_EE says:

    @John, No… because the program waits on the resultReady, which will only fire when all the conditions (including processId check) have been met.

  3. Joshua says:

    This is what WaitForMultipleObjects was made for.

  4. At work this situation usually results in me having to make a change so the workflow tool doesn't display the dialog, or makes it auto-close after some amount of time.

  5. Anon says:

    @Chris Crowther

    90% of the time these popups are utterly useless UNLESS there's a failure, so you should get rid of them regardless.

  6. JJJ says:

    Ugh, we have internal utilities at my workplace that launch "Done!" message boxes.  The worst is that at least 2 of these utilities are used during software builds.  Apparently nobody in the last 15 years thought it was unusual to have to click OK twice during a build (along with about 5 other manual steps).

    Add a "Done!" message box if you want to broadcast to the world, "Wow, I can't believe I wrote software that works!"

  7. Matt says:

    For the record, if you care about the race-condition, the correct approach is to start the process suspended, and then resume the process (or rather the initial thread of the process) once you've captured the process id.

  8. Random User 943579123 says:

    JJJ: Or, if the target audience explicitly requests it because, "how else are we supposed to tell that it's done?" :-/

  9. cheong00 says:

    @JJJ: Yup, alert users only on unexpected conditions.

    By launching "Done!" you're effectively telling the user the "success" is unexpected.

    @Random User 943579123: Event logs and ordinary log files have a purpose. By the application's nature it can silently goes away like most Windows' builtin scheduled tasks too.

    [Obviously, if there is a /silent flag or some other way to suppress the message on success, you would use it. But if there isn't, this article shows how to automate the dialog box. Because that generalizes to other cases where, say, you want to automate a confirmation dialog or a dialog to start the activity. -Raymond]
  10. ChristianSauer says:

    I wrote a Software which displays "done" – simply because it's writing the results to files and there is no visual clue if the operation finished or not. The "done" message simply tells the user "ok, the next step can be taken" – which is fine when the target of your program is an actual person.

    Mostly, programmers forgot that there are epeople who might use their tools in an automated fashion where an "Done" message is utterly annoying.

    So, for me, the solution is to use a simple configuration setting "unattended" which supresses all message windows.

  11. Anon says:

    @ChristianSauer

    The correct UX for that scenario is a "Done!" or "Operation Completed Successfully" line in the status bar, or presented somewhere else on the application; requiring additional button-/key-presses is always negative.

    (If only MS had learned this lesson with Win8/8.1…)

  12. John says:

    @Matt Thanks! I'm off to read about CreateProcess() (doesn't look like the CREATE_SUSPENDED was exposed in the .NET Framework's Process Class)

  13. Jonathan says:

    Minor programming quip – Rather than:

    ***** var invokePattern = okButton.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;

    I prefer:

    ***** var invokePattern = (InvokePattern)okButton.GetCurrentPattern(InvokePattern.Pattern);

    Why? Because if the result can't be cast, the former will silently produce a null, and later you'll get an unexplained NullReferenceException. The latter will immediately through a much clearer InvalidCastException with "Unable to cast object of type 'x' to type 'y'".

Comments are closed.