Recently, I had a project I was working on where we needed to be able to notify the user of a possible problem during a long running process. The results would not be ruined, or wrong, just likely incomplete. In this particular case we were collecting several pieces of information and sometimes one of those pieces were not available (say, for example the server was down, the file was missing, or it took too long). We wanted to let the user know the process had encountered an issue, but not stop it. Yet we wanted to also give them the option to retry it. So we presented an Abort, Retry, Cancel dialog like this one:
The problem is that with a traditional MessageBox the entire process would be frozen. If this happens immediately after the user heads off to lunch, when they get back there would be no report, no data and most of the process would not have been run yet, for example.
What I needed was for the process to continue, but to allow the user to be able to take action before the process completed on anything that was found. For example, they see this error above and think: “oh crap, I forgot to copy over the report file.” So they put the correct file in place and hit “Retry.”
Therefore, I created the AsyncMessageBox class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Runtime.InteropServices; | |
using System.Threading; | |
using System.Windows.Forms; | |
namespace NonModalMessageBox | |
{ | |
/// <summary> | |
/// Provides an asyncronous MessageBox. You can use this static class to | |
/// present a message to the usser while your code can continue to run. | |
/// You can attach to the AsynMessagebox.MessageboxClosed event to get the | |
/// result from the dialog once the user does close it. | |
/// </summary> | |
public static class AsyncMessageBox | |
{ | |
private static readonly IntPtr HWND_TOPMOST = new IntPtr(–1); | |
private static readonly uint SWP_NOMOVE = 0x0002; | |
private static readonly uint SWP_NOSIZE = 0x0001; | |
private static readonly uint SWP_SHOWWINDOW = 0x0040; | |
[DllImport("user32")] | |
private static extern IntPtr FindWindow(string PstrClassName, string PstrCaption); | |
[DllImport("user32")] | |
private static extern void SetWindowPos(IntPtr PintWnd, IntPtr PintWndInsertAfter, int PintX, int PintY, int PintCx, int PintCy, uint uFlags); | |
public delegate void MessageBoxClosedHandler(object PobjSender, MessageBoxClosedEventArgs PobjEventArgs); | |
public static event MessageBoxClosedHandler MessageBoxClosed; | |
private static bool MbolAlreadyShowing = false; | |
/// <summary> | |
/// Shows an asyncronous dialog | |
/// Fires the MessageBoxClosed event when it is closed. | |
/// This is a static messagebox, so only one can be displayed at a time | |
/// Once called, the event handler is detached | |
/// </summary> | |
/// <param name="PstrText">The message in the message box</param> | |
/// <param name="PstrCaption">The cpation on the message box</param> | |
/// <param name="PobjButtons">The buttons for the message box</param> | |
/// <param name="PobjIcon">The icon for the messafe box</param> | |
/// <param name="PobjDefault">The default button selected in the messagebox</param> | |
/// <returns></returns> | |
public static bool Show(string PstrText, string PstrCaption = "", MessageBoxButtons PobjButtons = MessageBoxButtons.OK, MessageBoxIcon PobjIcon = MessageBoxIcon.None, MessageBoxDefaultButton PobjDefault = MessageBoxDefaultButton.Button1) | |
{ | |
try | |
{ | |
if (MbolAlreadyShowing) return false; // failed – already displayed | |
DialogResult LobjResult = DialogResult.None; | |
// start a thread to show the dialog | |
new Thread(() => { | |
MbolAlreadyShowing = true; | |
LobjResult = MessageBox.Show(PstrText, PstrCaption, PobjButtons, PobjIcon, PobjDefault); | |
MbolAlreadyShowing = false; | |
}).Start(); | |
// start a separate thread to wait for the result from above | |
new Thread(() => { | |
// now make it topmost | |
IntPtr LintHwnd = FindWindow("#32770", PstrCaption); | |
SetWindowPos(LintHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW); | |
// Stay here until we get a result | |
while (LobjResult == DialogResult.None) | |
{ | |
Thread.Sleep(10); | |
Application.DoEvents(); | |
} | |
// create a hidden form so we can invoke back to the UI thread | |
// otherwise anyone attached to the event handler will get an | |
// exception if they try anything with the UI | |
using (Form LobjForm = new Form { ShowInTaskbar = false, Opacity = 0 }) | |
{ | |
LobjForm.Show(); // show hidden form – ui thread | |
// fire the event | |
LobjForm.Invoke(new Action(() => { | |
MessageBoxClosed?.Invoke(new object(), new MessageBoxClosedEventArgs(LobjResult)); | |
MessageBoxClosed = null; // important to release this | |
})); | |
LobjForm.Close(); // close hidden form | |
} | |
}).Start(); | |
return true; // created | |
} | |
catch | |
{ | |
return false; // failed | |
} | |
} | |
} | |
/// <summary> | |
/// Event arguments for an AsyncMessageBox | |
/// </summary> | |
public class MessageBoxClosedEventArgs | |
{ | |
public DialogResult Result { get; private set; } | |
public MessageBoxClosedEventArgs(DialogResult PobjResult) | |
{ | |
Result = PobjResult; | |
} | |
} | |
} |
Under the covers this uses a traditional MessageBox, but on background thread. You need to attach to the MessageBoxClosed event before you make the AsyncMessageBox.Show() call if you want to get the result. Here is how you use this method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
AsyncMessageBox.MessageBoxClosed += (o, e) => | |
{ | |
// handle the user response here | |
if(e.Result == DialogResult.Retry) | |
{ | |
// retry code here | |
} | |
}; | |
AsyncMessageBox.Show("There was an issue accessing 'quaterly report.xlsx'.", | |
"Quarterly Report Generator", | |
MessageBoxButtons.AbortRetryIgnore, | |
MessageBoxIcon.Exclamation); |
Anyway, thought this might be useful, so I have shared it.