Introduction
Displaying a notification or confirmation message box is a very basic thing in majority of the Windows applications. In .NET we have MessageBox class which we can use to display a message box in a single line of code.
However, that's not what we want when we use MVVM patterns. This where this article will help to write a fully MVVM complaint Message Box.
Prerequisite
This articule assumes that you have basic understanding of following concepts:
Background
We can use same System.Windows.Forms.MessageBox class in our WPF applications. However, when we use MVVM patterns, we want a seperation of concerns and use of this class in such a case will violate the rules. Writing a unit test for such a View-Model will also become harder. Should we block the test and wait for the user to press a 'OK' or 'Cancel' button? Nop, that's not the solution.
We have couple of options in this case.
- Interaction Service
- Interaction Request Objects
Interaction Service
In this case the View-Model is dependent on the interaction service to display the message box to the user and get the response. Interaction services can be used to display both Modal and Modaless popup Windows. Once the View-Model have the reference to the interaction service its a very straight forward method.
Following code snippet (taken from here) shows how to display a modal popup message box:
var result =
interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK );
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
And to display a modaless popup message box
interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK,
result =>
{
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
});
Interaction Request Objects
This approach is more consistent with MVVM patterns and Prism uses the same type approach. That's why we are going to use the same approach in this article to display a Message Box.
Prism privides IInteractionRequest<T>
class to achieve this task. This class implements IInteractionRequest
interface. This class has two Raise
methods which allow the View-Model to initiate interaction with the user. Signature of these two methods is given below:
public void Raise(T context);
and
public void Raise(T context, Action<T> callback)
The first method takes the context (Confirmation
or Notification
) as the parameter. Second method takes an additional parameter which is a callback delegate. This method is very useful when we want to do something in response to the user interaction.
To display a notification window following code is sufficient.
var notificationInteractionRequest = new InteractionRequest<Notification>();
notificationInteractionRequest.Raise(
new Notification
{
Title = "Information",
Content = "Operation completed successfully!"
});
Similary to display a confirmation window we can call the raise method with some callback delegate and then in the callback method we can check the value of "Confirmed" property to see what the user response was.
var confirmationInteractionRequest = new InteractionRequest<Confirmation>();
confirmationInteractionRequest.Raise(
new Confirmation
{
Title = "Confirmation",
Content = "Do you want to continue?"
}, OnWindowClosed);
private void OnWindowClosed(Confirmation confirmation)
{
if (confirmation.Confirmed)
{
}
The above mentioned methods are all what we need to display a popup message box. However, we don't have full control over the displayed window like the layout, buttons and icons etc. Following are the default message boxes displayed using Prism.
To overcome this problem this article provides a very simple way to fully control the layout of the message boxes. Sample message boxes created in this article are shown below:
Understanding the code
The message box is implemented using MVVM patterns and the definition of the MessageBoxViewModel (which is a ViewModel for Message Box) is given hereunder:
[Export(typeof(IMessageBoxViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MessageBoxViewModel : INotifyPropertyChanged, IMessageBoxViewModel
{
private string _title;
private string _message;
private MessageBoxButtons _msgBoxButtons;
private MessageBoxIcon _msgBoxIcon;
private Confirmation _confirmation;
public string Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
RaisePropertyChanged(() => Title);
}
}
}
public string Message
{
get { return _message; }
set
{
if (_message != value)
{
_message = value;
RaisePropertyChanged(() => Message);
}
}
}
public MessageBoxButtons MessageBoxButton
{
get { return _msgBoxButtons; }
set
{
if (_msgBoxButtons != value)
{
_msgBoxButtons = value;
RaisePropertyChanged(() => MessageBoxButton);
}
}
}
public MessageBoxIcon MessageBoxImage
{
get { return _msgBoxIcon; }
set
{
if (_msgBoxIcon != value)
{
_msgBoxIcon = value;
RaisePropertyChanged(() => MessageBoxImage);
}
}
}
public string DisplayIcon
{
get
{
switch (MessageBoxImage)
{
case MessageBoxIcon.Information:
return @"../Images/Information_48.png";
case MessageBoxIcon.Error:
return @"../Images/Error_48.png";
case MessageBoxIcon.Question:
return @"../Images/Question_48.png";
case MessageBoxIcon.Exclaimation:
return @"../Images/Exclaimation_48.png";
case MessageBoxIcon.Stop:
return @"../Images/Stop_48.png";
case MessageBoxIcon.Warning:
return @"../Images/Exclaimation_48.png";
default:
return @"../Images/Information_48.png";
}
}
}
public Confirmation Confirmation
{
get { return _confirmation; }
set
{
if (_confirmation != value)
{
_confirmation = value;
RaisePropertyChanged(() => Confirmation);
}
}
}
#region Protected Methods
protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
RaisePropertyChanged(propertyName);
}
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
}
As you can see that it implements some basic properties of the message box like title, message, buttons and icon. It also has a Confirmation
property which is useful only when message box is displayed for confirmation.In other cases it will serve no purpose.
This ViewModel is implements IMessageBoxViewModel
interface which is defined below:
public interface IMessageBoxViewModel
{
MessageBoxButtons MessageBoxButton { get; set; }
MessageBoxIcon MessageBoxImage { get; set; }
string Title { get; set; }
string Message { get; set; }
}
MessageBoxButtons
and MessageBoxIcon
types in this ViewModel are simple enumerations
public enum MessageBoxButtons
{
OK,
OKCancel,
YesNo,
YesNoCancel
}
public enum MessageBoxIcon
{
Information,
Error,
Warning,
Exclaimation,
Question,
Stop
}
There are two different views ConfirmationWindow
and NotificationWindow
. They both look very similiar. The only difference is when the message box is displayed in confirmation mode, we set the value of Confirmed
property of Confirmation
class when user clicks a button on the message box Window.
Event Triggers are used in the view to handle certain events raised from the ViewModel. These triggers can invoke a specific action and they can also set some property values on a particular object. The definition of ConfirmationWindow
view is given below:
<Window x:Class="Com.Controls.MessageBox.Views.ConfirmationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
xmlns:converters="clr-namespace:Com.Controls.MessageBox.Converters"
Title="{Binding Title}"
Width="370"
MinHeight="160"
ResizeMode="NoResize"
ShowInTaskbar="False"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
x:Name="confirmationWindow">
<Window.Resources>
<converters:EnumToVisibilityConverter x:Key="EnumToVisibilityConverter"/>
</Window.Resources>
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source="{Binding DisplayIcon }" Margin="10,10,5,0" Grid.Row="0" Grid.Column="0" Stretch="None" VerticalAlignment="Top"/>
<ContentPresenter Content="{Binding Message}" Margin="10,10,10,10" Grid.Row="0" Grid.Column="1">
<ContentPresenter.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,8">
<Button Content="Yes" Width="75" Height="23" Margin="0,0,10,0"
Visibility="{Binding Path= MessageBoxButton,
Converter={StaticResource EnumToVisibilityConverter},
ConverterParameter='YES'}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction PropertyName="Confirmed" TargetObject="{Binding Confirmation}" Value="True"/>
<ei:CallMethodAction TargetObject="{Binding ElementName=confirmationWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Content="No" Width="75" Height="23" Margin="0,0,10,0"
Visibility="{Binding Path= MessageBoxButton,
Converter={StaticResource EnumToVisibilityConverter},
ConverterParameter='NO'}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction TargetObject="{Binding ElementName=confirmationWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Content="OK" Width="75" Height="23" Margin="0,0,10,0"
Visibility="{Binding Path= MessageBoxButton,
Converter={StaticResource EnumToVisibilityConverter},
ConverterParameter='OK'}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction PropertyName="Confirmed" TargetObject="{Binding Confirmation}" Value="True"/>
<ei:CallMethodAction TargetObject="{Binding ElementName=confirmationWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Content="Cancel" Width="75" Height="23" Margin="0,0,10,0"
Visibility="{Binding Path= MessageBoxButton,
Converter={StaticResource EnumToVisibilityConverter},
ConverterParameter='CANCEL'}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:CallMethodAction TargetObject="{Binding ElementName=confirmationWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</Grid>
</Window>
As you can can see that under every <Button>
tag we have <i:Interaction.Triggers>
block where we listen for "Click" event and then we set the Confirmed
property of Confirmation
class and call the method "Close" for the view.
Similar approach has been used for the NotificationWindow
view. The only major difference is that we don't set the value of property Confirmed
.
There is a MessageContent
helper class which we use to initialize the Confirmation
.Content
and Notification
.Content
properties.
public class MessageBoxConent
{
public string Message { get; set; }
public MessageBoxButtons MessageBoxButton { get; set; }
public MessageBoxIcon MessageBoxImage { get; set; }
public bool IsModalWindow { get; set; }
public Window ParentWindow { get; set; }
}
The final thing we need is a class inherited from TriggerAction
<FrameworkElement
>.
We have two classes ConfirmationMessageAction
and NotificationMessageAction
which inherit from this class.
These classes override Invoke
method which is called in response to an EventTrigger action.
In the Invoke
method passed arguments are retrieved and message box ViewModel (i.e MessageBoxViewModel) is initialized and assigned to the relevant view. Finally the view is displayed to the user.
Definition of the ConfirmationMessageAction
is given below:
public class ConfirmationMessageBoxAction : TriggerAction<FrameworkElement>
{
protected override void Invoke(object parameter)
{
var args = parameter as InteractionRequestedEventArgs;
if (args != null)
{
var confirmation = args.Context as Confirmation;
if (confirmation != null)
{
var content = confirmation.Content as MessageBoxConent;
var window = new ConfirmationWindow
{
DataContext = new MessageBoxViewModel
{
Message = (content != null ? content.Message : ""),
MessageBoxButton = (content != null ? content.MessageBoxButton : MessageBoxButtons.OK),
MessageBoxImage = (content != null ? content.MessageBoxImage : MessageBoxIcon.Information),
Title = confirmation.Title,
Confirmation = confirmation
},
Owner = (content != null ? content.ParentWindow : null),
Icon = ((content != null && content.ParentWindow != null) ? content.ParentWindow.Icon : null)
};
EventHandler closeHandler = null;
closeHandler = (sender, e) =>
{
window.Closed -= closeHandler;
args.Callback();
};
window.Closed += closeHandler;
if (content != null && content.IsModalWindow)
{
window.ShowDialog();
}
else
{
window.Show();
}
}
}
}
}
Using the code
Using the library code attached with this article is very simple and straight forward.
- Add a reference to the
Com.Controls.MessageBox
library in your application.
- In your ViewModel class declare and initialize the
InteractionRequest
variables as shown below.
-
var confirmationInteractionRequest = new InteractionRequest<Confirmation>();
var notificationInteractionRequest = new InteractionRequest<Notification>();
public IInteractionRequest ConfirmationInteractionRequest
{
get { return _confirmationInteractionRequest; }
}
public IInteractionRequest NotificationInteractionRequest
{
get { return _notificationInteractionRequest; }
}
- In the view (i.e. xaml) file add the following interaction triggers block.
-
<i:Interaction.Triggers>
<!-- Trigger listening for the "Raised" event on the source object (of type IInteractionRequest) -->
<i:EventTrigger EventName="Raised" SourceObject="{Binding ConfirmationInteractionRequest}">
<i:EventTrigger.Actions>
<actions:ConfirmationMessageBoxAction />
</i:EventTrigger.Actions>
</i:EventTrigger>
<i:EventTrigger EventName="Raised" SourceObject="{Binding NotificationInteractionRequest}">
<i:EventTrigger.Actions>
<actions:NotificationMessageBoxAction />
</i:EventTrigger.Actions>
</i:EventTrigger>
</i:Interaction.Triggers>
- Finally when you want to display the popup message box call the
Raise
method as shown below.
-
_confirmationInteractionRequest.Raise(
new Confirmation
{
Title = "Question",
Content = new MessageBoxConent
{
Message = "Do you want to continue?",
MessageBoxButton = MessageBoxButtons.YesNo,
MessageBoxImage = MessageBoxIcon.Question,
IsModalWindow = true,
ParentWindow = Application.Current.MainWindow
}
},
delegate(Confirmation confirmation)
{
if (confirmation.Confirmed)
{
}
});
_notificationInteractionRequest.Raise(
new Notification
{
Title = "Information",
Content = new MessageBoxConent
{
Message = "Operation completed successfully!",
MessageBoxButton = MessageBoxButtons.OK,
MessageBoxImage = MessageBoxIcon.Information,
IsModalWindow = true,
ParentWindow = Application.Current.MainWindow
}
},
delegate
{
});
However, there is problem with this technique. It requires redundant code in every ViewModel and View file where we want to display message box.
Other Option
Other option is to add the InteractionRequest variables (added in 2nd step) in the ViewModel of your Shell. Then add the xaml code (3rd step) in the View file of your Shell.
Use the EventAggregator to subscriber to some event say MessageBoxEvent in your shell class like below:
EventAggregator.GetEvent<MessageBoxEvent>().Subscribe(ShowMessageBox, ThreadOption.UIThread, false);
Then in ShowMessageBox
function handle the event and call the Raise
method based on the event parameters like below:
private void ShowMessageBox(MessageBoxEventArgs args)
{
switch (args.MessageType)
{
case MessageType.INFO:
_notificationInteractionRequest.Raise(
new Notification
{
Title = args.Title,
Content = new MessageBoxConent
{
Message = args.Message,
MessageBoxButton = MessageBoxButtons.OK,
MessageBoxImage = MessageBoxIcon.Information,
IsModalWindow = args.IsModalMessage,
ParentWindow = Application.Current.MainWindow
}
},
delegate
{
if (args.CallbackAction != null)
{
args.CallbackAction(true);
}
});
break;
case MessageType.ERROR:
_notificationInteractionRequest.Raise(
new Notification
{
Title = args.Title,
Content = new MessageBoxConent
{
Message = args.Message,
MessageBoxButton = MessageBoxButtons.OK,
MessageBoxImage = MessageBoxIcon.Error,
IsModalWindow = args.IsModalMessage,
ParentWindow = Application.Current.MainWindow
}
},
delegate
{
if (args.CallbackAction != null)
{
args.CallbackAction(true);
}
});
break;
case MessageType.QUESTION:
_confirmationInteractionRequest.Raise(
new Confirmation
{
Title = args.Title,
Content = new MessageBoxConent
{
Message = args.Message,
MessageBoxButton = MessageBoxButtons.YesNo,
MessageBoxImage = MessageBoxIcon.Question,
IsModalWindow = args.IsModalMessage,
ParentWindow = Application.Current.MainWindow
}
},
delegate(Confirmation confirmation)
{
if (args.CallbackAction != null)
{
args.CallbackAction(confirmation.Confirmed);
}
});
break;
default:
break;
}
}
Finally from your ViewModels simplay publish the event with right arguments like below:
EventAggregator.GetEvent<MessageBoxEvent>().Publish(
new MessageBoxEventArgs
{
Title = "Question",
CallbackAction = ResponseCallback,
IsModalMessage = true,
Message = "Do you want to continue?",
MessageType = MessageType.QUESTION
});
For a complete example see the sample application attached with the source code.
References
Magnus Montin's blog.
Prism Library 5.0