I ended up using RoutedCommand
, as suggested by @JerKimball. In my searches I've seen dozens of ways to implement this, all probably right but none that left me satisfied.
I'm posting what worked for me as community wiki:
The BottomPanel
control did end up with - minimal - code-behind, because there was no way to bind the CommandBindings
to a ViewModel (because commands are not DependencyProperty
). So the code-behind merely calls into the "host" ViewModel where the actual implementations of Execute
and CanExecute
methods reside:
public partial class BottomPanel : UserControl
{
public BottomPanel()
{
InitializeComponent();
}
private void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
{
if (DataContext == null) return;
var viewModel = ((BottomPanelViewModel)DataContext).Host;
if (viewModel != null) viewModel.ExecuteOkCommand(sender, e);
}
private void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
{
if (DataContext == null) return;
var viewModel = ((BottomPanelViewModel)DataContext).Host;
if (viewModel != null) viewModel.CanExecuteOkCommand(sender, e);
}
...
}
In order to avoid tightly coupling the control with a specific ViewModel, I created an interface:
public interface IHasBottomPanel
{
event EventHandler WindowClosing;
DialogResult DialogResult { get; set; }
BottomPanelViewModel BottomPanelViewModel { get; set; }
void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e);
...
void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e);
...
}
Might be worth noting that the DialogResult
I'm using is my own interpretation of it (closer to what WinForms has to offer), because a simple bool
just doesn't fulfill the needs - the "Undefined" value is returned when the user "X"'s out of the window:
public enum DialogResult
{
Undefined,
Abort,
Apply,
Cancel,
Close,
Ignore,
No,
Ok,
Retry,
Yes
}
So, back to the BottomPanel
control, in the XAML I could define the command bindings as follows:
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static local:BottomPanelViewModel.OkCommand}"
Executed="ExecuteOkCommand"
CanExecute="CanExecuteOkCommand"/>
...
This works because the BottomPanelViewModel
class defines the static commands - I could just as well have defined them elsewhere, but they just seem to feel at home right there:
public static RoutedCommand OkCommand = new RoutedCommand();
...
This ViewModel also contains a Host
property referred to by the code-behind, which indirectly exposes the ViewModel that will handle the commands:
/// <summary>
/// Gets the host view model.
/// </summary>
public IHasBottomPanel Host { get; private set; }
/// Gets a value that determines what buttons appear in the bottom panel.
/// </summary>
public BottomPanelButtons Buttons { get; private set; }
/// <summary>
/// Creates a new ViewModel for a <see cref="BottomPanel"/> control.
/// </summary>
/// <param name="buttons">An enum that determines which buttons are shown.</param>
/// <param name="host">An interface representing the ViewModel that will handle the commands.</param>
public BottomPanelViewModel(BottomPanelButtons buttons, IHasBottomPanel host)
{
Buttons = buttons;
Host = host;
}
At this point everything is in place to get things working; I'm using this BottomPanel control on a MessageWindow
View, and so the MessageWindowViewModel
class implements the IHasBottomPanel
interface (the ViewModelBase
class merely provides a type-safe way to deal with INotifyPropertyChanged
):
public class MessageWindowViewModel : ViewModelBase, IHasBottomPanel
{
/// <summary>
/// Gets/sets ViewModel for the message window's content.
/// </summary>
public MessageViewModel ContentViewModel { get { return _messageViewModel; } }
private MessageViewModel _messageViewModel;
public MessageWindowViewModel()
: this(new MessageViewModel())
{ }
public MessageWindowViewModel(MessageViewModel viewModel)
: this(viewModel, BottomPanelButtons.Ok)
{ }
public MessageWindowViewModel(MessageViewModel messageViewModel, BottomPanelButtons buttons)
{
_messageViewModel = messageViewModel;
// "this" is passed as the BottomPanelViewModel's IHasBottomPanel parameter:
_bottomPanelViewModel = new BottomPanelViewModel(buttons, this);
}
...
public void ExecuteOkCommand(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = DialogResult.Ok;
if (WindowClosing != null) WindowClosing(this, EventArgs.Empty);
}
public void CanExecuteOkCommand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = _messageViewModel.ShowProgressControls
? _messageViewModel.ProgressValue == _messageViewModel.MaxProgressValue
: true;
}
So I get what I wanted: the "host" ViewModel controls Execute
and CanExecute
implementations for all commands in the BottomPanel
, and can be implemented differently on another "host". Here there's a way to configure the ViewModel so that the View displays a ProgressBar control, in which case the "Ok" button is only enabled once the ProgressBar's value has reached the maximum value (the "Cancel" button is enabled meanwhile, and gets disabled when "Ok" gets enabled).
I can then implement my own MsgBox
static class and expose various configurations of buttons and icons for various messages displayed to the user:
public static class MsgBox
{
private static DialogResult MessageBox(MessageViewModel messageViewModel, BottomPanelButtons buttons)
{
var viewModel = new MessageWindowViewModel(messageViewModel, buttons);
var window = new MessageWindow(viewModel);
window.ShowDialog();
return viewModel.DialogResult;
}
/// <summary>
/// Displays an informative message to the user.
/// </summary>
/// <param name="title">The message's title.</param>
/// <param name="message">The message's body.</param>
/// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
public static DialogResult Info(string title, string message)
{
return Info(title, message, string.Empty);
}
/// <summary>
/// Displays an informative message to the user.
/// </summary>
/// <param name="title">The message's title.</param>
/// <param name="message">The message's body.</param>
/// <param name="details">The collapsible message's details.</param>
/// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
public static DialogResult Info(string title, string message, string details)
{
var viewModel = new MessageViewModel(title, message, details, MessageIcons.Info);
return MessageBox(viewModel, BottomPanelButtons.Ok);
}
/// <summary>
/// Displays an error message to the user, with stack trace as message details.
/// </summary>
/// <param name="title">The message's title.</param>
/// <param name="exception">The exception to report.</param>
/// <returns>Returns <see cref="DialogResult.Ok"/> if user closes the window by clicking the Ok button.</returns>
public static DialogResult Error(string title, Exception exception)
{
var viewModel = new MessageViewModel(title, exception.Message, exception.StackTrace, MessageIcons.Error);
return MessageBox(viewModel, BottomPanelButtons.Ok);
}
...
}
And this is where @NickFreeman's comment about this question being a possible better fit for CodeReview becomes an undisputable truth: I'd really like to read what the community thinks of this implementation; maybe I've fallen into some pit traps that will bite me later, or maybe I'm violating principles or patterns I'm not aware of.
This question is begging to be migrated!