(Print this page)

Custom MessageBox in WPF
Published date: Thursday, February 26, 2015
On: Moer and Éric Moreau's web site

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:

  • Custom labels
  • Buttons only available after a delay (to force the user to read)
  • An auto-closing dialog if no action after a given delay

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.


(Print this page)