WPF: MVVM Friendly User Notification
This article demonstrates several MVVM friendly approaches a user can use from a ViewModel to let the user know something happened.
Getting the User's Attention
Somewhere way back in their origins, human beings lived in an environment where they were prey. Back then you were either quick or you were dead. Selection ensured that people are hard wired to particularly notice movement. Even once "civilisation" became fashionable man has reinforced this selection by weeding out the slow and unobservant. Whether via war or accident those that live on have sudden movement right up at the top of the list of attention grabbers along with loud bangs.
Various brighter colours such as red also tend to get attention. High contrast is also good which presents a bit of a problem for red text. Unless you have bold or large fonts the lines that make up text are pretty thin so a mid tone colour tends to be difficult to read. Dark red text is good, or you can make an area containing text stand out by using a bright background such as yellow.
Colours work in a bit of a strange way and complimentary contrast has arguably more effect than tonal contrast. These particular topics are mentioned primarily as pointers so the reader can research more detail if they are interested. This article will focus more on WPF mechanics which use these principles.
Before You Start
The sample enables you to easily experiment with the various effects and see what the variations look like running. That way you know first hand what each of them looks like and can guide users in a more practical direction when they ask for something weird like dancing spinning text.
It is therefore advisable you download the sample from here and give each of the effects a go.
The sample looks pretty boring as a static screen shot.
In that screen shot you can see the Ripple effect frozen in time with the lighter part of the linear gradient maybe 80% of the way across the text.
MVVM
The sample is built using the MVVM pattern. As such, an instance of a ViewModel is set as the DataContext of the view - MainWindow and MainWindowViewModel. This is done in the xaml for MainWindow.
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
A lot of the code is in MainWindowViewModel. In this particular sample that ViewModel contains a number of Public Properties which hold the various pieces of text. There is also a RelayCommand per button - this is the thing which does stuff as you click the button.
The RelayCommand is from the MVVMLight framework which you can obtain using Nuget. The framework is also used to implement INotifyPropertyChanged. MainWIndowViewModel inherits from the MVVMLight class ViewModelBase for this purpose.
Immobile Text
The first set of techniques have static text and apply some sort of animation to the foreground brush for the text. The brush controls the colour and opacity of the characters and one like a LinearGradientBrush applied as the foreground of a TextBlock applies that gradient across the entire TextBlock.
Common Mechanisms
The Immobile Text approaches all have a number of things in common, this section runs through these to avoid repeating the same in the description of each approach.
One thing common to the entire Window is FontSize. Some of these effects are fairly subtle, so you can see everything that's going on clearly the FontSize of the window is set to 18.
You need some event to start the storyboard and these all rely on the Binding.TargetUpdated event. This is raised when you change the target property in the viewmodel so long as you have the binding explicitly set to NotifyOnTargetUpdated=True; In the sample these are changed just by setting a bound property directly. In practice you might well want to display messages in one window that come from any code you run. In that case you would use the MVVM Light Messenger approach that the Toast approach uses.
All these textblocks are stacked on top of one another in the view in the sample. You would only really use one of these options in a real application.
All the animations work for a while before fading the text to opacity zero. You might prefer to leave them visible with a low opacity like 0.2 so the user can still refer to the last notification if they are interrupted for some reason.
The buttons for these:
<Button Command="{Binding FadeCommand}">
Fade</Button>
All bind to similar RelayCommands exposed from the ViewModel.
private RelayCommand _fadeCommand;
public RelayCommand FadeCommand
{
get
{
return _fadeCommand
?? (_fadeCommand = new RelayCommand(
() =>
{
FadeMessage = textBoxMsg;
}));
}
}
Once you get past the stuff which is the RelayCommans, this simply sets a property to textBoxMsg which is bound to the TextBox you can see in the View. You can therefore edit that text and see a different message from the default "I want to tell you something".
The property set
public string FadeMessage
{
get { return fadeMessage; }
set
{
fadeMessage = value;
RaisePropertyChanged();
}
}
Raises the InotifyPropertyChanged notification event which tells the UI to get the new value and will raise the event which starts the Storyboard applied to that TextBox in the view.
The Storyboards vary and are really the primary focus of any effect so they will be explained in the section for each of the effects.
Fade
The Fade technique shows a piece of text and then slowly fades it to zero opacity after a few seconds. This is a simple mechanism but not particularly eye catching. On the plus side, it's not going to annoy any but the most unreasonable user. This is best suited to showing a message the user is expecting. For example, they hit a button and the changes are committed, they are then reassured to notice a discrete "Database update successful." message.
<TextBlock Text="{Binding FadeMessage, NotifyOnTargetUpdated=True}"
HorizontalAlignment="Right" VerticalAlignment="Top">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
Duration="0:0:8" From="1" To="0">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
Here you can see the TextBlock is bound to the FadeMessage public string property. The binding is set to notify when the target property is updated.
If you ran the app then you will not be surprised to see that message is positioned top right in the View.
The Storyboard will start when the value of the target ( the viewmodel property ) of the binding changes.
The target object of the animation is implicitly the object containing it,. In order to do the fading the Opacity of the TextBlock is animated from 1 to 0 over 8 seconds.
The EasingFunction CubicEase with the EaseOut EasingMode makes the fading follow a curve which makes the last part of the fade to zero slower.
The reader who is unfamiliar with EasingFunctions is advised to read up on the various options.
Try it with a PowerEase
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
Duration="0:0:8" From="1" To="0">
<DoubleAnimation.EasingFunction>
<PowerEase EasingMode="EaseIn"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
And notice the difference.
Flash
Fading isn't exactly a dramatic effect and of course as the text fades out it becomes harder to read as well. How about something which is REALLY noticeable like flashing or blinking text? There is a bit of a problem inherent with this one though. You can have too much of a good thing and if you make something very noticeable you are quite likely to annoy users. Not to mention potential complaints about epileptic fits or migraines.
If you choose a flashing effect then you want it to be fairly subtle.
How do you do subtle flashing?
You flash by animating the opacity of your text from 1 normal to 0 transparent. You can do that subtly by animating between 1 and 0.5. You avoid being blamed for epileptic fits by changing it fairly slowly.
<TextBlock Text="{Binding FlashMessage, NotifyOnTargetUpdated=True}"
Foreground="DarkRed"
HorizontalAlignment="Right" VerticalAlignment="Top">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
Duration="0:0:.4" From="1" To=".5"
AutoReverse="True"
RepeatBehavior="5"/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
BeginTime="0:0:3"
Duration="0:0:3" To="0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
This TextBlock is similar to the previous one. Notice that the Foreground is used to set the text to DarkRed. This colour is eyecatching because it's a red colour but more legible than mid red because it's darker, giving better tonal contrast.
There are two animations in this storyboard.
The first cycles from Opacity 1 to .5 and back in 0.8 seconds. This will be repeated 5 times.
After 3 seconds the second animation fades the text out entirely over 3 seconds.
Wipe
Maybe flashing is a bit too extreme even if you make it subtle. The Wipe approach shows a piece of text then animates a linear gradient across it. This is a subtle effect and the transition is just enough to be noticeable and thus eye catching without being annoyingly intrusive. You could argue it is a bit too subtle but by being so it is of course low on the annoyance scale.
<TextBlock Text="{Binding WipeMessage, NotifyOnTargetUpdated=True}" HorizontalAlignment="Right" VerticalAlignment="Top">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush>
<GradientStop Color="DarkRed" Offset="0.0" />
<GradientStop Color="DarkRed" />
<GradientStop Color="Tomato" Offset="1.0" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</TextBlock.Style>
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Foreground.(GradientBrush.GradientStops)[1].(GradientStop.Offset)"
Duration="0:0:2.5" From="0" To="1"/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
BeginTime="0:0:3"
Duration="0:0:3" To="0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
This is a lot easier to understand if you look at it working a couple of times.
The way the effect works is that the text uses a LinearGradientBrush for it's foreground. This has 3 stops. It will become Dark Red but starts with a gradient from dark red to Tomato ( a light red ).
The rather strange notation on that first animation allows the stop to be referenced from the array of GradientStops in the LinearGradientBrush. The Offset (which is animated) is how far along the extent of the Brush and hence the TextBlock the Stop is. It varies from 0 on the left to 1 on the right.
The opacity is first set to 1 so it's reset after any previous animation.
After 3 seconds the opacity is animated to fade the message out.
Ripple
This animates the light area in a LinearGradientBrush backwards and forwards along the text. It's usually rather less annoying than flashing but over use will still cause complaints. One of the things to be careful with is the contrast between the dark main part of the LinearGradientBrush and the light part. Another is the speed. Experiment with higher and lower durations to get a feel for which you prefer.
<TextBlock Text="{Binding RippleMessage, NotifyOnTargetUpdated=True}" HorizontalAlignment="Right" VerticalAlignment="Top">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush>
<GradientStop Color="Purple" Offset="0.0" />
<GradientStop Color="LightBlue" />
<GradientStop Color="Purple" Offset="1.0" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</TextBlock.Style>
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Foreground.(GradientBrush.GradientStops)[1].(GradientStop.Offset)"
Duration="0:0:.5" From="0" To="1" AutoReverse="True" RepeatBehavior="8"/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
To="1"/>
<DoubleAnimation
Storyboard.TargetProperty="Opacity"
BeginTime="0:0:2"
Duration="0:0:5" From="1" To="0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
The Ripple markup is pretty similar to that of the Wipe. In this case the two outer GradientStops are dark and the one to be animated is light.
Experiment with both the colours - try Violet for the light stop for lower contrast and make the Duration of that GradientStop 1 second rather than 0.5 for a subtler effect.
For a very showy effect you could try complimentary colours. Make the LightBlue Stop Yellow instead.
Moving Text
Making the text or it's container move is another way to get someone's attention.
Toast
Many readers will already be familiar with the Toast effect since it is quite a commonly used pattern. For those that don't "just know" Toast notifications are a type of panel or window which appears vertically and usually in the right corner of a display. The effect is named after the action of an electric toaster which pops up the toast after it completes toasting. There will therefore usually be some animation which increases the height of such a panel.
Like the examples above, toast messages appear for a while then they disappear.
The approach taken in the sample produces a rather more dramatic result than the previous mechanisms.
It also copes with multiple active notifications.
The approach works using a ListBox which is in a Usercontrol called ToastList.
Let's look at how that works by starting from the bottom up and look at the ViewModel
ToastListViewModel
This is decoupled from the rest of the application using MVVM Light Messenger and the pattern explained in this article. This approach is recommended for most user notifications because you can just new up and send a message from any part of the application and it will be handled by the Registered method in the MainViewModel.
The class for the message to send is contained in the ViewModel file, since it's only going to be used when you use the ToastList.
public class Message4ListBox : ViewModelBase
{
public String Msg { get; set; }
private bool isGoing;
public bool IsGoing
{
get { return isGoing; }
set
{
isGoing = value;
RaisePropertyChanged();
}
}
}
Looking at that, you can see the Msg property which will contain the message to be displayed. There is also the rather mysterious "IsGoing" boolean property which is used in the ListBox removal animation. That aspect will become clearer shortly.
public class ToastListViewModel : ViewModelBase
{
public ObservableCollection<Message4ListBox> Messages { get; set; }
public ToastListViewModel()
{
Messages = new ObservableCollection<Message4ListBox>();
Messenger.Default.Register<UserNotificationMessage>(this, (action)=>AddMessage(action));
}
private async void AddMessage(UserNotificationMessage msg)
{
Message4ListBox message4ListBox = new Message4ListBox { Msg = msg.Message, IsGoing = false };
Messages.Insert(0, message4ListBox);
await Task.Delay(new TimeSpan(0, 0, msg.SecondsToShow));
// You can't animate on removal event since there's nothing there to animate.
// Therefore, a datatrigger is used to drive the removal animation.
message4ListBox.IsGoing = true;
await Task.Delay(new TimeSpan(0,0,0,1,300));
Messages.Remove(message4ListBox);
}
}
Looking at the ViewModel you will notice that the ObservableCollection exposed to bind the ListBox ItemsSource to is actually a collection of the Message4ListBox.
The ViewModel subscribes to Message4ListBox and when it receives one, it'll add it to Messages which will notify the view it's got a new item in it's collection and show it.
The new message is added to the collection by inserting it at index zero. This makes newer messages appear at the top.
You will notice that AddMessage is async. That allows the handler to fire and delay parts of it's processing for a while. That mechanism is what the await Task.Delay does.
It allows the message to be shown for a while.
As the comment explains, you cannot animate a ListBoxItem on a removed event, so you need to animate it and then remove it after it's finished animating. This is the purpose of IsGoing. Setting IsGoing to true will fire a DataTrigger in the view which will animate it so it's height is reduced.
Another await delays processing for a while.
Then the Message is actually removed out Messages.
Let;s take a look at the View for those animations.
ToastList
There's not a huge amount of markup for ToastList so this is all of it:
<UserControl x:Class="wpf_Notify_User.ToastList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:local="clr-namespace:wpf_Notify_User">
<UserControl.DataContext>
<local:ToastListViewModel/>
</UserControl.DataContext>
<Grid>
<ListBox ItemsSource="{Binding Messages}" BorderBrush="Transparent" Background="LightGray">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="LayoutTransform">
<Setter.Value>
<ScaleTransform/>
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1.2" FillBehavior="Stop" />
<DoubleAnimation Storyboard.TargetProperty="LayoutTransform.ScaleY" From="0" Duration="0:0:1.2" FillBehavior="Stop">
<DoubleAnimation.EasingFunction>
<BounceEase Bounces="2" Bounciness="6"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<DataTrigger Binding="{Binding IsGoing}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:1.2" />
<DoubleAnimation Storyboard.TargetProperty="LayoutTransform.ScaleY" To="0" Duration="0:0:1.2"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Yellow"
CornerRadius="3"
BorderThickness="1"
Margin="4" Padding="4">
<Border.BitmapEffect>
<DropShadowBitmapEffect ShadowDepth="4" Color="Purple" />
</Border.BitmapEffect>
<TextBlock Text="{Binding Msg}" FontWeight="SemiBold" TextWrapping="Wrap" MaxWidth="200"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
ToastList uses xaml to instantiate it's ToastListViewModel as a private member.
The ItemsSource of a ListBox is bound to the Messages property we discussed earlier. Some minor styling gives the ListBox a light grey background. This will appear as a panel behind the items and serve to improve contrast.
The Template of the ListBoxItem is replaced in that style. This is a minor thing in this instance but if you comment it out temporarily and click an item once it appears you will notice it gets the blue background of a selected ListBoxItem. Replacing the style obviates that.
The next piece of layout is a LayoutTransform whose scaleTransform will be used to animate an item into view and back again. The toast popping up out the toasted effect, as it were.
Next up is a RoutedEvent Loaded trigger which will fire when a new message is added to the bound Messages.
This will transform the height - the ScaleY of that LayoutTransform and make the height grow.
To make this a bit more interesting a BounceEase is used to make it appear to bounce back twice, just a little. The higher the bounciness the less noticeable that bounce.
If you set it to 1 temporarily you will see it bounces back a lot more with a rather extreme effect.
The item is also faded into view which has quite a subtle blurring effect adding to the illusion it is a panel flipping up forward and bouncing into position rather than just straight up. The effect is very subtle and you might not particularly notice it consciously but your brain is very good at spotting these sort of things sub consciously and using them as visual cues.
This illusion is because the brain is used to interpreting input from the outside world where atmospheric haze means distant things are fuzzy.
As mentioned earlier, a routed event isn't going to work to animate on removal of an item because once it's gone there's nothing to animate.
Change of IsGoing is used to fire a Datatrigger which reverses the animation which popped our toast up. Except this time without any bounce.
If you have seen this working then you will be expecting most of the content of the ItemTemplate. This has a Border with a Yellow Background. The corner radius is set to 3, giving a subtly rounded corner which reduces the sharpness a little.
This has a Purple DropShadowBitMapEffect which adds some complimentary colour contrast to make the panel stand out more and look a bit more interesting.
Withing that is a TextBlock which is bound to Msg. This has MaxWidth and TextWrapping set just in case a long message is displayed. Without these you would find the item can potentially fill the width of the window.
This approach is of course not so great if you need to show an awfully long message. You could perhaps increase the sophistication and include a link to more information or a different page/usercontrol. That way if you pop up a message that product x is out of stock you could include a link to the place order page or some such.
You could have different datatemplates used in the ListBox and show links or buttons for some.
Alternative Option
You could use a pop up with the ListBox in it rather than a control directly in the parent window. You can calculate the width of the monitor and position your popup bottom right in the monitor. This is rather more like an operating system Toast Notification but can confuse some users who don't realise that message is coming out of your particular application.
There are also possible complications to this approach for users who have multiple monitors.
Marquee
Anyone who ever watched a news channel has probably noticed the Marquee animation. It's difficult to miss. This is the name for the way the text is scrolled across right to left continuously, with the headlines.
If you want to get someone's attention then scrolling text across the bottom of their window repeatedly should do the job. This will be great for some applications, terrible in many.
The way this works is that two textblocks are positioned in a panel with one off screen to the right and the other to the left most of the panel. The panel is then animated across the window by the width of it, repeatedly.
In this way, as each character disappears off the left of the window it appears on the right. Like it was sort of going round and round.
Here's the markup for the panel.
<Grid x:Name="MarqueeContainer" VerticalAlignment="Bottom">
<Grid.RenderTransform>
<TranslateTransform x:Name="Xmarquee" X="0"/>
</Grid.RenderTransform>
<Canvas Height="24"
TextBlock.Foreground="Red">
<TextBlock Text="{Binding MarqueeMessage, NotifyOnTargetUpdated=True}" Canvas.Left="0">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard Storyboard="{StaticResource SBmarquee}" />
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<TextBlock Text="{Binding MarqueeMessage}"
Foreground="Red"
Canvas.Left="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type Canvas}}}"/>
</Canvas>
</Grid>
A canvas is used here because it allows you to position the rightmost TexBlock outside itself. That RenderTransform on the Grid is going to be used to drive the animation - the X measures how far it is from the left of it's parent. When you animate that to a negative number, it will slide over to the left and everything you see there "in" it will go with it.
Like with the static text techniques, the Binding.TargetUpdated is used to start a Storyboard. Unlike them, that storyboard is a resource of the Window. This is partly to demonstrate you can organise your storyboards separately if you feel they make it easier to read your markup.
<Window.Resources>
<!-- "To" of this is set in code because of the window resizing -->
<Storyboard x:Key="SBmarquee">
<DoubleAnimation From="0"
Duration="00:00:8"
Storyboard.TargetProperty="X"
Storyboard.TargetName="Xmarquee"
RepeatBehavior="3"/>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="MarqueeContainer"
To="1"/>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="MarqueeContainer"
BeginTime="0:0:20"
Duration="0:0:4" To="0"/>
</Storyboard>
</Window.Resources>
The opacity animations should be familiar by now, they reset the opacity to 1 then fade everything in the container out.
More interesting is how the TransLateTransform.X is animated between 0 and a negative number. Particularly since there is no "To" value there at all. That's a bit weird.
The reason for that is that you need a negative number and it must match the width of the panel. In order to ensure that, some code behind is used:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private Storyboard SBMarquee;
private DoubleAnimation XAnimation;
private void Window_ContentRendered(object sender, EventArgs e)
{
SBMarquee = this.Resources["SBmarquee"] as Storyboard;
XAnimation = SBMarquee.Children[0] as DoubleAnimation;
XAnimation.To = MarqueeContainer.ActualWidth * -1;
this.SizeChanged += Window_SizeChanged;
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
XAnimation.To = MarqueeContainer.ActualWidth * -1;
MarqueeContainer.Visibility = Visibility.Hidden;
SBMarquee.Begin();
MarqueeContainer.Visibility = Visibility.Visible;
}
}
What that does is wait until the visual tree is completely built and it attaches an event handler to Window SizeChanged.
It also dips into the Window.Resources to get a reference to the Storyboard and animation.
This lets us set the "To" property on that animation to -1 times the width of that container ( which will be the width of the content area of the window).
The container is hidden and shown again, the storyboard re-started so the text doesn't look weird as the user drags the width backwards and forwards.
Getting Confirmation
Sometimes it's not enough to tell the user something, you need to get up in their face and insist they confirm they read your message or you need to get them to make a decision.
A requirement which is particularly tricky for the new MVVM developer is obtaining Message Box confirmation from the user from code which is in the ViewModel. In this situation the ViewModel being decoupled from the view introduces a complication. One way round this is to just show a message rather than pop up a messagebox.
The most likely problem scenarios here is when you require confirmation before deleting a record or file. That "Do you really want to do this?" step.
The sample includes the UserInput project which can readily be referenced and re-used in your projects.
UserInput
This project contains a Control ConfirmationRequestor and a class ConfirmationRequestorVM which makes it a bit quicker to use ConfirmationRequestor in your ViewModels.
ConfirmationRequestor
This will show a message box whose parent is set to it's own parent. You initiate this action via binding.
ConfirmationRequestor is a class which inherits from Control so you can easily place it in XAML in the visual tree. It implements ICommandSource so you can bind a command and commandparemeter to it which will be invoked when the user clicks the Yes or OK button.
Let;s take a look at the code for this:
public class ConfirmationRequestor: Control, ICommandSource
{
public bool? ShowConfirmDialog
{
get
{
return (bool?)GetValue(ShowConfirmDialogProperty);
}
set
{
SetValue(ShowConfirmDialogProperty, value);
}
}
public static readonly DependencyProperty ShowConfirmDialogProperty =
DependencyProperty.Register("ShowConfirmDialog",
typeof(bool?),
typeof(ConfirmationRequestor),
new PropertyMetadata(null, new PropertyChangedCallback(ConfirmDialogChanged)));
private static void ConfirmDialogChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool?)e.NewValue != true)
{
return;
}
ConfirmationRequestor cr = (ConfirmationRequestor)d;
Window parent = Window.GetWindow(cr) as Window;
MessageBoxResult result = MessageBox.Show(parent, cr.Message, cr.Caption, cr.MsgBoxButton, cr.MsgBoxImage);
if (result == MessageBoxResult.OK || result == MessageBoxResult.Yes)
{
if (cr.Command != null)
{
cr.Command.Execute(cr.CommandParameter);
}
}
cr.SetValue(ShowConfirmDialogProperty, null);
}
public MessageBoxButton MsgBoxButton
{
get { return (MessageBoxButton)GetValue(MsgBoxButtonProperty); }
set { SetValue(MsgBoxButtonProperty, value); }
}
public static readonly DependencyProperty MsgBoxButtonProperty =
DependencyProperty.Register("MsgBoxButton",
typeof(MessageBoxButton),
typeof(ConfirmationRequestor),
new PropertyMetadata(MessageBoxButton.OK));
public MessageBoxImage MsgBoxImage
{
get { return (MessageBoxImage)GetValue(MsgBoxImageProperty); }
set { SetValue(MsgBoxImageProperty, value); }
}
public static readonly DependencyProperty MsgBoxImageProperty =
DependencyProperty.Register("MsgBoxImage",
typeof(MessageBoxImage),
typeof(ConfirmationRequestor),
new PropertyMetadata(MessageBoxImage.Warning));
public string Caption
{
get { return (string)GetValue(CaptionProperty); }
set { SetValue(CaptionProperty, value); }
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.Register("Caption",
typeof(string),
typeof(ConfirmationRequestor),
new PropertyMetadata(string.Empty));
public string Message
{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register("Message",
typeof(string),
typeof(ConfirmationRequestor),
new PropertyMetadata(string.Empty));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICommand), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter", typeof(object), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
public IInputElement CommandTarget
{
get { return (IInputElement)GetValue(CommandTargetProperty); }
set { SetValue(CommandTargetProperty, value); }
}
public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(ConfirmationRequestor), new UIPropertyMetadata(null));
}
Although there looks like a fair few dependency properties some of these are pretty straight forward. They are all intended to be set by Binding applied to the Control in XAML - although you could alternatively dynamically create Bindings in code if you prefer.
There are 4 DependencyProperties associated with the look of the messagebox:
- Caption
- Message
- MsgBoxButton
- MsgBoxImage
If MessageBox is familiar to you you will immediately recognise what all these properties are. The Message is the message to be shown, the MsgBoxButton is an enum specifying what buttons will appear and the Image is the like of a stop / warning sign. For further details you can read the documentation on MessageBox.
ICommandSource might not be immediately familiar to a WPF developer but it's the interface a button uses. Satisfying that are 3 Dependency Properties:
- Command
- CommandParameter
- CommandTarget
Command is the ICommand which will be fired if the user clicks a Yes or OK button in the messagebox shown.
CommandParameter is the parameter which will be handed to that ICommand.
CommandTarget will have no significant effect but needs to be there to satisfy the interface.
That covers how the MessageBox will look and what the Yes/OK button will do.
Showing the MessageDialog is controlled by the bool? ShowConfirmDialog. You initiate that by setting it to true, which means the change callback will run:
private static void ConfirmDialogChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool?)e.NewValue != true)
{
return;
}
ConfirmationRequestor cr = (ConfirmationRequestor)d;
Window parent = Window.GetWindow(cr) as Window;
MessageBoxResult result = MessageBox.Show(parent, cr.Message, cr.Caption, cr.MsgBoxButton, cr.MsgBoxImage);
if (result == MessageBoxResult.OK || result == MessageBoxResult.Yes)
{
if (cr.Command != null)
{
cr.Command.Execute(cr.CommandParameter);
}
}
cr.SetValue(ShowConfirmDialogProperty, null);
}
This is the business end of the whole thing and will reset the property value to null, show the messagebox using all those properties to control the look and will execute any bound command if Yes/OK are clicked.
If you are, for example, deleting some record the code that does the deletion would go in a command you bind to ConfirmationRequestor.
Notice how a reference is obtained to the Window containing the ConfirmationRequestor. This is then used in the MessageBox.Show - ensuring the MessageBox appears on top of the Window. The ease of finding the parent window is one of the advantages in using a Control over some sort of regular class that wouldn't be in the Visual Tree.
Usage
You first add a reference to the UserInput dll/project to your own project. You can then add an xmlns to your window to reference it there:
xmlns:input="clr-namespace:UserInput;assembly=UserInput"
You can then add a control and bind all those properties already discussed.
<input:ConfirmationRequestor
ShowConfirmDialog="{Binding ShowConfirmation, Mode=TwoWay}"
MsgBoxImage="{Binding confirmer.MsgBoxImage}"
MsgBoxButton="{Binding confirmer.MsgBoxButton}"
Message="{Binding confirmer.Message}"
Caption="{Binding confirmer.Caption}"
Command="{Binding OkCommand}"
/>
The control can go anywhere you like inside your main Grid or whatever is the main container in your window, you won't actually see anything in the designer or when it runs.
Note the above markup doesn't bind the CommandParameter for the Command, that's another option and works like any Button.
You will of course need some properties in your ViewModel to bind to. Some of these are implemented for in ConfirmationRequestorVM:
public class ConfirmationRequestorVM : INotifyPropertyChanged
{
private string caption;
public string Caption
{
get { return caption; }
set
{
caption = value;
NotifyPropertyChanged();
}
}
private string message;
public string Message
{
get { return message; }
set
{
message = value;
NotifyPropertyChanged();
}
}
private MessageBoxButton msgBoxButton;
public MessageBoxButton MsgBoxButton
{
get { return msgBoxButton; }
set
{
msgBoxButton = value;
NotifyPropertyChanged();
}
}
private MessageBoxImage msgBoxImage;
public MessageBoxImage MsgBoxImage
{
get { return msgBoxImage; }
set
{
msgBoxImage = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In case you want to switch out what the command and CommandParameter do, these are left for you to implement in your own ViewModel. You can therefore have a simple RelayCommand as in the sample or a more complicated one where you inject or substitute an Action to dynamically change exactly what the Yes/OK will do.
In most views you only have one thing requires such confirmation but maybe you are working on something unusual.
MainWindowViewModel
This is the part of the window's ViewModel which relates to the ConfirmationRequestor:
private RelayCommand _confirmCommand;
public RelayCommand ConfirmCommand
{
get
{
return _confirmCommand
?? (_confirmCommand = new RelayCommand(
() =>
{
confirmer.Caption = "Please Confirm";
confirmer.Message = "Are you SURE you want to delete this record?";
confirmer.MsgBoxButton = MessageBoxButton.YesNo;
confirmer.MsgBoxImage = MessageBoxImage.Warning;
OkCommand = new RelayCommand(
() =>
{
// You would do some actual deletion or something here
UserNotificationMessage msg = new UserNotificationMessage { Message = "OK.\rDeleted it.\rYour data is consigned to oblivion.", SecondsToShow = 5 };
Messenger.Default.Send<UserNotificationMessage>(msg);
});
RaisePropertyChanged("OkCommand");
showConfirmation = true;
RaisePropertyChanged("ShowConfirmation");
}));
}
}
// The OK command fires when you choose yes or ok.
// This isn't in ConfirmationRequestorVM because you might prefer to use some other icommand helper rather
// than mvvm light's relaycommand.
public RelayCommand OkCommand { get; set; }
private bool? showConfirmation;
public bool? ShowConfirmation
{
get { return showConfirmation; }
set
{
showConfirmation = value;
RaisePropertyChanged();
}
}
private string textBoxMsg = "I need to tell you something";
public string TextBoxMsg
{
get { return textBoxMsg;}
set
{
textBoxMsg = value;
RaisePropertyChanged();
}
}
// This vm just saves you defining 4 inpc properties of your own every time
public ConfirmationRequestorVM confirmer { get; set; }
public MainWindowViewModel()
{
confirmer = new ConfirmationRequestorVM();
}
As already mentioned, there is an instance of ConfirmationRequestorVM which saves you setting up those 4 properties every time you use this control.
The property ShowConfirmation drives showing the MessageBox, it fires that callback which was explained earlier when set to true.
ConfirmCommand is bound to that "Get Confirmation" button visible in MainWindow.
A particularly unusual thing about his command is it news up and sets the OKCommand which will be fired when the user has chosen Yes or OK from the MessageBox. In this way you could set whichever command you need for a given confirmation in case there are more than one in a Window.
You could in use any ICommand implementation you prefer but the sample uses a RelayCommand.
ConfirmCommand also sets the various bound properties which control what the messagebox looks like then starts the ball rolling by setting ShowConfirmation to true.
Alternative
If you wanted a more sophisticated interaction with a user filling fields or were just wondering about an alternative, you could use an event aggregator such as MVVM Light Messenger. You would message the view to show a dialog or some such, the view would then message any results back to the ViewModel as explained in detail in this article.
The advantage of the approach presented here is that there is no code behind as there would be with Messenger. One re-usable control can be tested once and used many times. You could maybe write a base window which had the messenger implementation in it but then you have to work out which open window is specified etc. A control is slightly more elegant.