Blog

Asynchronous Message Box in WPF

Asynchronous Message Box in WPF

The message box, in Windows Forms and WPF, is a useful, quick-and-dirty way to send an alert, display an error, and get simple input from the user. The message box is, however, modal.

Recently, I needed to use a message box in WPF but did not want to block the calling thread. The usual searches pointed to creating a message box-like window and calling Show, which returns immediately and allows the calling thread to continue. There is so much built into the message box, though, such as standard icons (information, warning, and error), standard Windows sounds, internationalization of the buttons, and message formatting, that replicating it robustly seemed difficult.

Instead of creating a new window, I used the BeginInvoke method of the delegate type to display the standard message box. For more information on asynchronous programming with the BeginInvoke see this article. In the example code below, MessageBox.Show is wrapped in a method that is run on a separate thread.

 private delegate void ShowMessageBoxDelegate(string strMessage, string strCaption, MessageBoxButton enmButton, MessageBoxImage enmImage);
 // Method invoked on a separate thread that shows the message box.
 private static void ShowMessageBox(string strMessage, string strCaption, MessageBoxButton enmButton, MessageBoxImage enmImage) {
 	MessageBox.Show(strMessage, strCaption, enmButton, enmImage);
 }
 // Shows a message box from a separate worker thread.
 public static void ShowMessageBoxAsync(string strMessage, string strCaption, MessageBoxButton enmButton, MessageBoxImage enmImage) {
 	ShowMessageBoxDelegate caller = new ShowMessageBoxDelegate(ShowMessageBox);
 	caller.BeginInvoke(strMessage, strCaption, enmButton, enmImage, null, null);
 }

When the message box was modal, nothing could occur until it was acknowledged. An asynchronous message box does not block the calling thread, so that thread could create many copies of a message box before the first one is acknowledged. This behavior may be acceptable...no, probably not. However, the programmer may know this would never occur.

If repeated message boxes could occur, I found a simple way to prevent them until the first message box is acknowledged. The BeginInvoke method returns an object of the IAsyncResult type. This interface provides an IsCompleted property that will be set true once the asynchronous operation is completed.

To block repeated asynchronous message boxes, pass by reference a member IAsyncResult variable to the method shown below. This method will only show a new message box once the first one has been closed.

 // Shows a message box from a separate worker thread. The specified asynchronous 
 // result object allows the caller to monitor whether the message box has been 
 // closed. This is useful for showing only one message box at a time. 
 public static void ShowMessageBoxAsync(string strMessage, string strCaption, MessageBoxButton enmButton, MessageBoxImage enmImage, ref IAsyncResult asyncResult) {
 	if ((asyncResult == null) || asyncResult.IsCompleted) {
 		ShowMessageBoxDelegate caller = new ShowMessageBoxDelegate(ShowMessageBox);
 		asyncResult = caller.BeginInvoke(strMessage, strCaption, enmButton, enmImage, null, null);
 	}
 }

Download a sample Visual Studio 2008 solution that shows the methods above and how they might be called in an application. Hope you've found this solution useful and I would appreciate any improvements you might make. Thanks.

Download

Learn more about DMC's software and web development services.

Comments

Jorgitus
# Jorgitus
Thank you very much for sharing this!
Ganesh
hi,
i know this code in windows but my project in wpf if you have any suggestion on wpf costume message box then let me know.
Ron Green
# Ron Green
Very good demo application to illustrating asynchronous non-modal message boxes.

To help me see what threads the message boxes were running on I added Thread.CurrentThread.ManagedThreadId to my copy to see what worker threads the message boxes executed on. One thing I didn't realize is that the ProcessMessageBoxAsync() callback always executes on the same worker thread in which the corresponding message box was created on.

Ron
XueBingZheng
# XueBingZheng
I'm really confused! Why can't i get the MessageBox in a worker thread?
Honza Pačuta
# Honza Pačuta
Thanks, very useful!
Bill
# Bill
One more thing, the exception given was "Object synchronization method was called from an unsynchronized block of code." Again, the problem was that I was calling Monitor.Enter(object) from one thread and Monitor.Exit(object) from a different thread. I didn't find that anywhere on google, so I'm posting this addendum here in case the GoogleGods deem this worthy to help someone else in a similar situation looking for that exception. Cheers.
Bill
# Bill
Howdy, just wanted to say thanks for the code. I didn't really go with yours but it helped me get to mine. I wanted it to match up with the Prism guidelines at http://msdn.microsoft.com/en-us/library/gg405494(v=PandP.40).aspx#sec11 (Prism 4.0, Chapter 6, Advanced MVVM, Using an Interaction Service). The code they give (and give no indication of how to achieve its implementation) is as follows:

interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK,
result =>
{
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
});

So, I needed to have the callback doable with a lambda like that, so here is my code that I came up with (sorry for any formatting issues, I've never posted code online and I don't know how to tag it properly. I work at a grocery store):


//START OF CODE===================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PatchesDrei.Lib.Services;
using System.Windows;
using Microsoft.Practices.Prism.Logging;
using Microsoft.Practices.Prism.Modularity;
using System.Threading;
using System.Runtime.Remoting.Messaging;
using System.ComponentModel.Composition;
using System.Diagnostics;

namespace PatchesDrei.Modules.SimpleUserInteractions
{
[Export(typeof(IUserInteractions))]
public class SimpleUserInteractionsService : IUserInteractions
{
#region ShowMessageBox Sync & Async

private delegate MessageBoxResult ShowMessageBoxDelegate(string messageBoxText,
string caption,
MessageBoxButton button);
///
/// Class variable for the external (calling) callback. This is not to be confused with
/// the IAsyncResult callback that the UserInteractions class uses internally. In this form,
/// you can use the messagebox as is stated in the prism guidelines.
///

private Action _ExternalCallback;
private object _ShowMessageBoxLock = new object();
private bool _ShowMessageBoxIsLocked = false;
private bool TryLockShowMessageBox()
{
bool result = false;
lock (_ShowMessageBoxLock)
{
//If already locked, then we can't lock it again, so result = false;
//This keeps multiple messageboxes from happening
if (_ShowMessageBoxIsLocked)
result = false;
else
{
//_ShowMessageBoxIsLocked == false
_ShowMessageBoxIsLocked = true;
result = true;
}
return result;
}
}
private bool TryUnlockShowMessageBox()
{
bool result = false;
lock (_ShowMessageBoxLock)
{
//if it isn't locked, then we can't unlock it
if (!_ShowMessageBoxIsLocked)
result = false;
else
{
//it is locked, so now we can unlock it
_ShowMessageBoxIsLocked = false;
result = true;
}
return result;
}
}


public MessageBoxResult ShowMessageBoxSync(string messageBoxText, string caption,
MessageBoxButton button)
{
return MessageBox.Show(messageBoxText, caption, button);
}

///
/// Calls MessageBox.Show with a begininvoke and when endinvoke is called,
/// we'll call you back with the callback.
/// This can only be done one at a time, as we are locking on a private lock
/// when we call. This lock is unlocked as WE get the async callback, ie
/// before the "callback" param is called.
///

public void ShowMessageBoxAsync(string messageBoxText, string caption,
MessageBoxButton button, Action callback)
{
//only one go at messageboxasync works. if we are already calling an async messagebox,
//and it hasn't completed yet, then we just log this erroneous messagebox attempt
//and get on with our lives.
Debug.WriteLine("Trying to ENTER lock on thread" +
Thread.CurrentThread.ManagedThreadId + "\n" +
Thread.CurrentThread.Name);
//if (!Monitor.TryEnter(_ShowMessageBoxLock))
if (!TryLockShowMessageBox())
{
//IMPORTANT! LOGGER MUST BE THREADSAFE!
PatchesDrei.Lib.PatchesServices.Logger.Log(
Properties.Resources.ErrorMessageMessageBoxAttemptedButAlreadyInUse, //msg
Microsoft.Practices.Prism.Logging.Category.Warn, //category
PatchesDrei.Lib.Priorities.UIWarning); //priority
return;
}

ShowMessageBoxDelegate showDelegate = new ShowMessageBoxDelegate(ShowMessageBoxSync);
AsyncCallback asyncCallback = new AsyncCallback(ShowMessageBoxAsyncCallback);

_ExternalCallback = callback;
showDelegate.BeginInvoke(messageBoxText, caption, button, asyncCallback, null);
}

///
/// Used in conjunction with above ShowMessageBoxAsync. This is the internal callback
/// using IAsyncResult. This is not to be confused with the _ExternalCallback field, which
/// stores the caller's callback (whew, glad that's not confusing).
///

private void ShowMessageBoxAsyncCallback(IAsyncResult asyncResult)
{
MessageBoxResult msgResult = MessageBoxResult.None;
try
{
AsyncResult result = (AsyncResult)asyncResult;
ShowMessageBoxDelegate showDelegate = (ShowMessageBoxDelegate)result.AsyncDelegate;

msgResult = showDelegate.EndInvoke(result);
}
catch
{
PatchesDrei.Lib.PatchesServices.Logger.Log(
Properties.Resources.ErrorMessageMessageBoxAsyncCallbackProblem, //msg
Microsoft.Practices.Prism.Logging.Category.Warn, //category
PatchesDrei.Lib.Priorities.UIWarning); //priority
}
finally
{
}
_ExternalCallback.Invoke(msgResult);

Debug.WriteLine("Trying to EXIT lock on thread" +
Thread.CurrentThread.ManagedThreadId + "\n" +
Thread.CurrentThread.Name);

///this doesn't work because you can't lock an object on one thread and then unlock it
///from another thread. so, i just encapsulated the behavior to a method that locks
///access to setting a private boolean _ShowMessageBoxIsLocked;
//Monitor.Exit(_ShowMessageBoxLock);
if (!TryUnlockShowMessageBox())
{
PatchesDrei.Lib.PatchesServices.Logger.Log(
Properties.Resources.ErrorMessageMessageBoxAsyncCallbackProblem, //msg
Microsoft.Practices.Prism.Logging.Category.Warn, //category
PatchesDrei.Lib.Priorities.UIWarning); //priority
}
}

#endregion
}
}
//END OF CODE=====================


And here is how I call the messagebox:
//hack: just testing async messagebox service
if (PatchesServices.UserInteractions != null)
{
PatchesServices.UserInteractions.
ShowMessageBoxAsync("testing text here",
"testing caption here",
System.Windows.MessageBoxButton.OKCancel,
result =>
{
if (result == System.Windows.MessageBoxResult.OK)
Console.WriteLine("Hey, we successfully called UserInteractions.ShowMessageBoxAsync, cause I'm the callback function!");
});
PatchesServices.UserInteractions.
ShowMessageBoxAsync("testing text here again this shouldn't show up",
"testing second message box caption here",
System.Windows.MessageBoxButton.OKCancel,
result =>
{
if (result == System.Windows.MessageBoxResult.OK)
Console.WriteLine("Uh oh, we just called a second messagebox in UserInteractions.ShowMessageBoxAsync and it shouldn't have worked. I'm the callback for this, so I should know."
);
});
}



I just wanted to put that up online for anyone else who may have read that prism guideline and wondered how to achieve what they want. It would have been nice to have it actually in the prism guideline (or at least a link). One interesting thing that I learned also is that you cannot lock an objectlock with Monitor.Enter(objectLock) in one thread and then use Monitor.Exit(objectLock) on a different thread. I would have thought that was possible with the Monitor handling the synchronization, but I suppose it isn't.

So, thanks again for the blog. It definitely aided in my code.
Eric Anderson
# Eric Anderson
Sorry for the very late reply. I have updated the sample code to return the result of the asynchronous message box to the originating thread. Please let me know if you have any questions about that. (I will try to respond a little faster.)
Max Pavlov
Hey. What if I need to return MessageBoxResult to a calling thread? Is it possible?
Eric Anderson
# Eric Anderson
Glad you found it useful. I am a lot more comfortable with WPF now and would not mind creating a new custom window to match this application.
miaodadao
It's very useful. Thanks!
:-)

Post a comment

Name (required)

Email (required)

CAPTCHA image
Enter the code shown above: