I’m a fan of the MVVM pattern and recently needed to present a dialog (actually a MessageDialog in WinRT) to the user if they were about to leave the data entry window but hadn’t saved their changes. I initially thought that the view model would need to create a dialog, however, I thought about it some more and came to the conclusion that actually the view should be responsible for this – the view model only needs to let the view know that it has unsaved changes and provide commands to the view (which will expose them to the user) for it to either save and close or close without saving.
So the next thing I needed was to be able to define a dialog in XAML, as I wanted to bind the buttons on the dialog to the commands in the view model. As well as defining it in the XAML, I wanted to either launch it in response to the user clicking a button or, if the dialog isn’t required (i.e., there are no unsaved changes in my scenario) make the button perform the default action without prompting the user. To do this, I’ve used an attached property which, when set on a button, will subscribe to the Click
event and show the dialog in the handler, if required, or perform the default action if the PromptUser
property is set to false
. The resulting XAML looks something like this (note that the MessageDialogTemplate
can be moved to a resource, I’ve put it inline here to make the code snippet shorter and easier to follow):
<AppBarButton Icon="Cancel"
Label="Close">
<dlg:MessageDialogTemplate.ShowOnClick>
<dlg:MessageDialogTemplate Content="You have unsaved changes to the document."
ShowDialog="{Binding HasUnsavedChanges}">
<dlg:DialogCommand Command="{Binding SaveAndClose}"
Label="Save changes" />
<dlg:DialogCommand Command="{Binding Cancel}"
Label="Discard changes" />
-->
-->
<dlg:DialogCommand Label="Cancel" />
</dlg:MessageDialogTemplate>
</dlg:MessageDialogTemplate.ShowOnClick>
</AppBarButton>
So, that’s its usage. For the above, there are two classes required:
namespace DialogHelper
{
using System.Windows.Input;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Markup;
[ContentProperty(Name = "Label")]
public sealed class DialogCommand : FrameworkElement
{
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register("CommandParameter",
typeof(object), typeof(DialogCommand), new PropertyMetadata(null));
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command",
typeof(ICommand), typeof(DialogCommand), new PropertyMetadata(null));
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register("Label", typeof(string),
typeof(DialogCommand), new PropertyMetadata(string.Empty));
public ICommand Command
{
get { return (ICommand)this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); }
}
public string Label
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
internal void Invoke(IUICommand command)
{
if (this.Command != null)
{
this.Command.Execute(this.CommandParameter);
}
}
}
}
Nothing too special about this class, apart from it inherits from FrameworkElement
so that it gets the inheriting data context scaffolding, which means you can use bindings for the commands and they will pick it up from the button the dialog is attached to. Also worth noting is that originally I tried to make the class implement the IUICommand
interface, however, because the dialog is running on a different thread to what the dependency properties were created on, there were some exceptions when the MessageDialog
would try to read the values from the properties. Instead, as shown in the next class, these commands are translated into UICommand
s.
namespace DialogHelper
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Markup;
[ContentProperty(Name = "Commands")]
public sealed class MessageDialogTemplate : Panel
{
public static readonly DependencyProperty CancelCommandIndexProperty =
DependencyProperty.Register("CancelCommandIndex",
typeof(int), typeof(MessageDialogTemplate), new PropertyMetadata(-1));
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(string),
typeof(MessageDialogTemplate), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty DefaultCommandIndexProperty =
DependencyProperty.Register("DefaultCommandIndex",
typeof(int), typeof(MessageDialogTemplate), new PropertyMetadata(-1));
public static readonly DependencyProperty PromptUserProperty =
DependencyProperty.Register("PromptUser", typeof(bool),
typeof(MessageDialogTemplate), new PropertyMetadata(true));
public static readonly DependencyProperty ShowOnClickProperty =
DependencyProperty.RegisterAttached("ShowOnClick",
typeof(MessageDialogTemplate), typeof(MessageDialogTemplate),
new PropertyMetadata(null, OnShowOnClickChanged));
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string),
typeof(MessageDialogTemplate), new PropertyMetadata(string.Empty));
private readonly ObservableCollection<DialogCommand> commands =
new ObservableCollection<DialogCommand>();
public MessageDialogTemplate()
{
this.commands.CollectionChanged += OnCommandsCollectionChanged;
}
public int CancelCommandIndex
{
get { return (int)this.GetValue(CancelCommandIndexProperty); }
set { this.SetValue(CancelCommandIndexProperty, value); }
}
public IList<DialogCommand> Commands
{
get { return this.commands; }
}
public string Content
{
get { return (string)this.GetValue(ContentProperty); }
set { this.SetValue(ContentProperty, value); }
}
public int DefaultCommandIndex
{
get { return (int)this.GetValue(DefaultCommandIndexProperty); }
set { this.SetValue(DefaultCommandIndexProperty, value); }
}
public bool PromptUser
{
get { return (bool)this.GetValue(PromptUserProperty); }
set { this.SetValue(PromptUserProperty, value); }
}
public string Title
{
get { return (string)this.GetValue(TitleProperty); }
set { this.SetValue(TitleProperty, value); }
}
public static MessageDialogTemplate GetShowOnClick(DependencyObject obj)
{
return (MessageDialogTemplate)obj.GetValue(ShowOnClickProperty);
}
public static void SetShowOnClick(DependencyObject obj, MessageDialogTemplate value)
{
obj.SetValue(ShowOnClickProperty, value);
}
private static void OnButtonClick(object sender, RoutedEventArgs e)
{
MessageDialogTemplate template = GetShowOnClick(sender as DependencyObject);
if (template != null)
{
template.OnButtonClick();
}
}
private static void OnButtonDataContextChanged
(FrameworkElement sender, DataContextChangedEventArgs args)
{
MessageDialogTemplate template = GetShowOnClick(sender);
if (template != null)
{
template.DataContext = sender.DataContext;
}
}
private static void OnShowOnClickChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var button = d as ButtonBase;
if (button != null)
{
button.Click += OnButtonClick;
button.DataContextChanged += OnButtonDataContextChanged;
}
}
private void ExecuteDefaultCommand()
{
int index = this.DefaultCommandIndex;
if (index == -1)
{
index = 0;
}
if (index < this.commands.Count)
{
DialogCommand command = this.commands[index];
if ((command != null) && (command.Command != null))
{
command.Command.Execute(command.CommandParameter);
}
}
}
private void OnCommandsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.Children.Clear();
foreach (DialogCommand command in this.commands)
{
this.Children.Add(command);
}
}
private void OnButtonClick()
{
if (this.PromptUser)
{
this.ShowMessageDialog();
}
else
{
this.ExecuteDefaultCommand();
}
}
private async void ShowMessageDialog()
{
var dialog = new MessageDialog(this.Content, this.Title);
foreach (DialogCommand command in this.commands)
{
dialog.Commands.Add(
new UICommand(command.Label, command.Invoke));
}
dialog.CancelCommandIndex = (uint)this.CancelCommandIndex;
dialog.DefaultCommandIndex = (uint)this.DefaultCommandIndex;
await dialog.ShowAsync();
}
}
}
It may seem strange to not bind the DataContext
to the DataContext
of the button, however, I was bit by the bug where changes to the parent data context are not propagated through the binding (see this blog for more details) – strangely enough changes to the parent’s data context do cause the buttons DataContextChanged
event to fire, which makes it even more confusing the binding doesn’t work!?
I’m quite pleased with the result; it’s nice to define the dialog layout in the XAML with the rest of the UI and the view model doesn’t need to raise some event that the view has to listen for (which seems to be the way for it to show a dialog in most examples I’ve seen.)