A long time ago, back in January 2009, I wrote an article titled A custom MessageBox showing how to customize the Windows Forms MessageBox.
Now that I have started WPF, I need to rebuild and revisit some of the helpers I have built over the years.
A few days ago, I needed a custom WPF MessageBox. Therefore, here is something to start you with if you ever need to customize your own.
The demo code
I have created the demo solution with the Visual Studio 2015 CTP 6 (I have to test that thing!) but you can open it with Visual Studio 2013 without any problems.
The downloadable solution provides both VB and C# code.
Figure 1: The demo application in action
Features I wanted
I wanted some feature that were not available:
Instead of trying to hack the message box provided by WPF, I preferred to start my own. It will be easier for you (and I) to continue improving it.
Basic Window
I started with the creation of very basic XAML code to place the controls I wanted as you can see in figure 2 (download the demo to see the full code).
Figure 2: Building the UI of the new MessageBox
In the code behind, I have a couple of properties named Caption, InstructionHeading and InstructionText to fill the related zones.
Because we often use the same set of buttons (Ok, Ok/Cancel, Yes/No, Yes/No/Cancel), I have created a method named SetButtonsPredefined to quickly set them. Later, you will find another method to create custom buttons. This method even have an override to show buttons in French instead of the default English version. The method handles the visibility of the three buttons which are not always all visible. The Tag property of each button stores the value to return to the caller when you click the button.
Here is the code to set the predefined buttons:
Public Sub SetButtonsPredefined(ByVal buttons As EnumPredefinedButtons) SetButtonsPredefined(buttons, EnumLanguages.English) End Sub Public Sub SetButtonsPredefined(ByVal buttons As EnumPredefinedButtons, ByVal language As EnumLanguages) Button1.Visibility = Visibility.Collapsed Button1.Tag = EnumDialogResults.None Button2.Visibility = Visibility.Collapsed Button2.Tag = EnumDialogResults.None Button3.Visibility = Visibility.Collapsed Button3.Tag = EnumDialogResults.None Select Case buttons Case EnumPredefinedButtons.Ok Button1.Visibility = Visibility.Visible Button1.Content = "Ok" Button1.Tag = EnumDialogResults.Ok Case EnumPredefinedButtons.OkCancel Button1.Visibility = Visibility.Visible Button1.Content = If(language = EnumLanguages.French, "Annuler", "Cancel") Button1.Tag = EnumDialogResults.Cancel Button2.Visibility = Visibility.Visible Button2.Content = "Ok" Button2.Tag = EnumDialogResults.Ok Case EnumPredefinedButtons.YesNo Button1.Visibility = Visibility.Visible Button1.Content = If(language = EnumLanguages.French, "Non", "No") Button1.Tag = EnumDialogResults.No Button2.Visibility = Visibility.Visible Button2.Content = If(language = EnumLanguages.French, "Oui", "Yes") Button2.Tag = EnumDialogResults.Yes Case EnumPredefinedButtons.YesNoCancel Button1.Visibility = Visibility.Visible Button1.Content = If(language = EnumLanguages.French, "Annuler", "Cancel") Button1.Tag = EnumDialogResults.Cancel Button2.Visibility = Visibility.Visible Button2.Content = If(language = EnumLanguages.French, "Non", "No") Button2.Tag = EnumDialogResults.No Button3.Visibility = Visibility.Visible Button3.Content = If(language = EnumLanguages.French, "Oui", "Yes") Button3.Tag = EnumDialogResults.Yes End Select End Sub
Finally, handle the click event of each button to store the Tag property into another variable (_customDialogResult) as well as setting the DialogResult property of the window to True to let the dialog close:
Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) Handles Button1.Click _customDialogResult = CType(Button1.Tag, EnumDialogResults) DialogResult = True End Sub Private Sub Button2_Click(sender As Object, e As RoutedEventArgs) Handles Button2.Click _customDialogResult = CType(Button2.Tag, EnumDialogResults) DialogResult = True End Sub Private Sub Button3_Click(sender As Object, e As RoutedEventArgs) Handles Button3.Click _customDialogResult = CType(Button3.Tag, EnumDialogResults) DialogResult = True End Sub
We need to expose that custom dialog result to the caller. A simple property like this one does the job:
Public ReadOnly Property CustomCustomDialogResult() As EnumDialogResults Get Return _customDialogResult End Get End Property
This is about the only code that you need to give a first run to our new dialog. You can test it by instantiating it with something like this:
Dim dialog As New CustomMessagBox With { .Caption = "This is the title of the dialog", .InstructionHeading = "This is what you have to do:", .InstructionText = "Take a deep breath and continue!" } dialog.SetButtonsPredefined(EnumPredefinedButtons.OkCancel) Dim result = dialog.ShowDialog() If (result.HasValue AndAlso result.Value) Then MessageBox.Show("You close the dialog with " + dialog.CustomCustomDialogResult.ToString()) Else MessageBox.Show("dialog was auto-closed!") End If
You can also show the dialog with the French buttons by adding a simple parameter:
dialog.SetButtonsPredefined(EnumPredefinedButtons.OkCancel, EnumLanguages.French)
Custom buttons’ caption
Now that we have the base, we can continue to enhance it.
The real reason I wanted my own dialog is that the question was not properly by Yes/No or Ok/Cancel. I wanted my own captions.
To provide this feature, I have added another method that takes care of the caption and the value I want to return to the caller. Here is the code:
Public Sub SetButtonsCustoms(ByVal captionLeft As String, ByVal captionMiddle As String, ByVal captionRight As String, ByVal resultLeft As EnumDialogResults, ByVal resultMiddle As EnumDialogResults, ByVal resultRight As EnumDialogResults) Button1.Visibility = Visibility.Collapsed Button1.Tag = EnumDialogResults.None Button2.Visibility = Visibility.Collapsed Button2.Tag = EnumDialogResults.None Button3.Visibility = Visibility.Collapsed Button3.Tag = EnumDialogResults.None If (Not String.IsNullOrWhiteSpace(captionRight)) Then Button1.Visibility = Visibility.Visible Button1.Content = captionRight Button1.Tag = resultRight End If If (Not String.IsNullOrWhiteSpace(captionMiddle)) Then Button2.Visibility = Visibility.Visible Button2.Content = captionMiddle Button2.Tag = resultMiddle End If If (Not String.IsNullOrWhiteSpace(captionLeft)) Then Button3.Visibility = Visibility.Visible Button3.Content = captionLeft Button3.Tag = resultLeft End If End Sub
It does much the same thing as the other method offering the standard buttons. It sets the visibility, the caption and the tag of the three buttons. Everything else in the dialog remains the same.
When you want to use your own buttons, you call this new method instead:
dialog.SetButtonsCustoms("Yes Please", "No Thanks", Nothing, EnumDialogResults.Yes, EnumDialogResults.No, EnumDialogResults.None)
Auto closing the dialog
In some circumstances, you want to display a message and it the user does not answer in a given period, close the dialog and continue your operations.
It is surprisingly simple to create such a feature. You first need a timer. I recommend that you use the DispatcherTimer that you declare like this:
Private _timerAutoClose As Windows.Threading.DispatcherTimer
You then create a property like this one:
Public WriteOnly Property AutoCloseDialogTime As Integer Set(value As Integer) _timerAutoClose = New Windows.Threading.DispatcherTimer() AddHandler _timerAutoClose.Tick, AddressOf TimerAutoCloseTick _timerAutoClose.Interval = New TimeSpan(0, 0, 0, value) _timerAutoClose.Start() End Set End Property
This property sets the delay (in seconds) of the timer and starts it. An event handler for the Tick event needs to be created:
Private Sub TimerAutoCloseTick(ByVal sender As Object, ByVal e As EventArgs) _timerAutoClose.Stop() DialogResult = False End Sub
The trick here is to ensure that your timer stops when you close the form because you do not want your Tick event to try to reach an unloaded dialog and trigger an exception because the handler is not in memory anymore. Probably the best place to do it is in the Closing event of the Window:
Private Sub CustomMessagBox_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing If _timerAutoClose IsNot Nothing Then _timerAutoClose.Stop() _timerAutoClose = Nothing End If End Sub
You are now ready to test your new behavior by setting the new property:
Dim dialog As New CustomMessagBox With { .Caption = "This is the title of the dialog", .InstructionHeading = "This is what you have to do:", .InstructionText = "The dialog will automatically close after 5 seconds!", .AutoCloseDialogTime = 5 } dialog.SetButtonsPredefined(EnumPredefinedButtons.Ok)
Enabling the buttons after a delay
I also wanted to enable the buttons of the dialog only after a delay. This is a trick to force your users to read the messages.
To do this, create another property. This property will set the Enabled property of each button to false before starting a timer:
Public WriteOnly Property EnableButtonsAfterTime As Integer Set(value As Integer) Button1.IsEnabled = False Button2.IsEnabled = False Button3.IsEnabled = False _timerEnableButtonsAfter = New Windows.Threading.DispatcherTimer() AddHandler _timerEnableButtonsAfter.Tick, AddressOf _timerEnableButtonsAfter_Tick _timerEnableButtonsAfter.Interval = New TimeSpan(0, 0, 0, value) _timerEnableButtonsAfter.Start() End Set End Property
After the delay has elapsed, you just turn the Enabled property to true:
Private Sub _timerEnableButtonsAfter_Tick(ByVal sender As Object, ByVal e As EventArgs) _timerEnableButtonsAfter.Stop() Button1.IsEnabled = True Button2.IsEnabled = True Button3.IsEnabled = True End Sub
Do not forget to update the Closing event of the Window to ensure you stop this timer as well.
To test it, just set the new property like this:
Dim dialog As New CustomMessagBox With { .Caption = "This is the title of the dialog", .InstructionHeading = "This is what you have to do:", .InstructionText = "The buttons will only be enabled after 3 seconds!", .EnableButtonsAfterTime = 3 }
Conclusion
As you can see, not much code is required to reproduce the old message box dialog and implement a completely new set of great features.
Starting with this simple example, you can finally build a dialog that really fits your needs.