Question

I just managed to get my WPF custom message window to work as I intended it... almost:

    MessageWindow window;

    public void MessageBox()
    {
        var messageViewModel = new MessageViewModel("Message Title",
            "This message is showing up because of WPF databinding with ViewModel. Yay!",
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec fermentum elit non dui sollicitudin convallis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Integer sed elit magna, non dignissim est. Morbi sed risus id mi pretium facilisis nec non purus. Cras mattis leo sapien. Mauris at erat sapien, vitae commodo turpis. Nam et dui quis mauris mattis volutpat. Donec risus purus, aliquam ut venenatis id, varius vel mauris.");
        var viewModel = new MessageWindowViewModel(messageViewModel, BottomPanelButtons.YesNoCancel);
        window = new MessageWindow(viewModel);
        viewModel.MessageWindowClosing += viewModel_MessageWindowClosing;
        window.ShowDialog();

        var result = viewModel.DialogResult;
        System.Windows.MessageBox.Show(string.Format("result is {0}", result));
    }

    void viewModel_MessageWindowClosing(object sender, EventArgs e)
    {
        window.Close();
    }

Under the hood, there's a "BottomPanel" user control that merely creates a bunch of buttons with their "Visibility" attribute controlled by the MessageWindowViewModel (via property getters such as "IsOkButtonVisible", itself determined by the value of the "BottomPanelButtons" enum passed to the viewmodel's constructor).

While this fulfills my requirement of being able to display a message window with collapsible details and a configurable set of buttons at the bottom, I'm disappointed with the way I had to put all the functionality I originally wanted in the BottomPanel control (or rather, into its viewmodel), into the MessageWindowViewModel class:

    public MessageWindowViewModel(MessageViewModel messageViewModel, BottomPanelButtons buttons)
    {
        _messageViewModel = messageViewModel;
        _abortCommand = new DelegateCommand(ExecuteAbortCommand, CanExecuteAbortCommand);
        _applyCommand = new DelegateCommand(ExecuteApplyCommand, CanExecuteApplyCommand);
        _cancelCommand = new DelegateCommand(ExecuteCancelCommand, CanExecuteCancelCommand);
        _closeCommand = new DelegateCommand(ExecuteCloseCommand, CanExecuteCloseCommand);
        _ignoreCommand = new DelegateCommand(ExecuteIgnoreCommand, CanExecuteIgnoreCommand);
        _noCommand = new DelegateCommand(ExecuteNoCommand, CanExecuteNoCommand);
        _okCommand = new DelegateCommand(ExecuteOkCommand, CanExecuteOkCommand);
        _retryCommand = new DelegateCommand(ExecuteRetryCommand, CanExecuteRetryCommand);
        _yesCommand = new DelegateCommand(ExecuteYesCommand, CanExecuteYesCommand);
        Buttons = buttons;
    }

    /// <summary>
    /// Gets/sets a value that determines what buttons appear in the bottom panel.
    /// </summary>
    public BottomPanelButtons Buttons { get; set; }

    public bool IsCloseButtonVisible { get { return Buttons == BottomPanelButtons.ApplyClose || Buttons == BottomPanelButtons.Close; } }
    public bool IsOkButtonVisible { get { return Buttons == BottomPanelButtons.Ok || Buttons == BottomPanelButtons.OkCancel; } }
    public bool IsCancelButtonVisible { get { return Buttons == BottomPanelButtons.OkCancel || Buttons == BottomPanelButtons.RetryCancel || Buttons == BottomPanelButtons.YesNoCancel; } }
    public bool IsYesButtonVisible { get { return Buttons == BottomPanelButtons.YesNo || Buttons == BottomPanelButtons.YesNoCancel; } }
    public bool IsNoButtonVisible { get { return IsYesButtonVisible; } }
    public bool IsApplyButtonVisible { get { return Buttons == BottomPanelButtons.ApplyClose; } }
    public bool IsAbortButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore; } }
    public bool IsRetryButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore || Buttons == BottomPanelButtons.RetryCancel; } }
    public bool IsIgnoreButtonVisible { get { return Buttons == BottomPanelButtons.AbortRetryIgnore; } }

    public ICommand AbortCommand { get { return _abortCommand; } }
    public ICommand ApplyCommand { get { return _applyCommand; } }
    public ICommand CancelCommand { get { return _cancelCommand; } }
    public ICommand CloseCommand { get { return _closeCommand; } }
    public ICommand IgnoreCommand { get { return _ignoreCommand; } }
    public ICommand NoCommand { get { return _noCommand; } }
    public ICommand OkCommand { get { return _okCommand; } }
    public ICommand RetryCommand { get { return _retryCommand; } }
    public ICommand YesCommand { get { return _yesCommand; } }

    public string AbortButtonText { get { return resx.AbortButtonText; } }
    public string ApplyButtonText { get { return resx.ApplyButtonText; } }
    public string CancelButtonText { get { return resx.CancelButtonText; } }
    public string CloseButtonText { get { return resx.CloseButtonText; } }
    public string IgnoreButtonText { get { return resx.IgnoreButtonText; } }
    public string NoButtonText { get { return resx.NoButtonText; } }
    public string OkButtonText { get { return resx.OkButtonText; } }
    public string RetryButtonText { get { return resx.RetryButtonText; } }
    public string YesButtonText { get { return resx.YesButtonText; } }

    private ICommand _abortCommand; 
    private ICommand _applyCommand; 
    private ICommand _cancelCommand; 
    private ICommand _closeCommand; 
    private ICommand _ignoreCommand; 
    private ICommand _noCommand; 
    private ICommand _okCommand; 
    private ICommand _retryCommand; 
    private ICommand _yesCommand;

And there's even more code below that - the actual Execute and CanExecute handlers, which all do the same thing: set the DialogResult property and raise MessageWindowClosing event:

    private void ExecuteCloseCommand(object commandArgs)
    {
        DialogResult = DialogResult.Close;
        if (MessageWindowClosing != null) MessageWindowClosing(this, EventArgs.Empty);
    }

    private bool CanExecuteCloseCommand(object commandArgs)
    {
        return true;
    }

Now this works, but I find it's ugly. I mean, what I'd like to have, is a BottomPanelViewModel class holding all the BottomPanel's functionality. The only thing I like about this, is that I have no code-behind (other than a constructor taking a MessageViewModel in the MessageView class, setting the DataContext property).

So the question is this: is it possible to refactor this code so that I end up with a reusable BottomPanel control, one that embeds its functionality into its own viewmodel and has its own commands? The idea is to have the commands on the BottomPanel control and the handlers in the ViewModel of the containing window... or is that too much of a stretch?

I've tried many things (dependency properties, static commands, ...), but what I have now is the only way I could manage to get it to work without code-behind. I'm sure there's a better, more focused way of doing things - please excuse my WPF-noobness, this "message box" window is my WPF "Hello World!" first project ever...

Was it helpful?

Solution

Based on my own personal experience, I have a few suggestions.

First, you can create an interface for any view logic that should be executed by a ViewModel.

Second, instead of using *ButtonVisibility in the ViewModel, I have found it better to specify a "Mode" of the ViewModel and use a ValueConverter or a Trigger in the view layer to specify what shows in that mode. This makes it to where your ViewModel can't accidentally (through a bug) get into a state that is invalid by giving a scenerio like

IsYesButtonVisible = true;
IsAbortButtonVisible = true;

I understand that your properties do not have setters, but they could easily be added by someone maintaining code and this is just a simple example.

For your case here, we really only need the first one.

Just create an interface that you would like to use. You can rename these to your liking, but here his an example.

public interface IDialogService
{
    public void Inform(string message);
    public bool AskYesNoQuestion(string question, string title);
}

Then in your view layer you can create an implementation that is the same across your application

public class DialogService
{
    public void Inform(string message)
    {
        MessageBox.Show(message);
    }

    public bool AskYesNoQuestion(string question)
    {
        return MessageBox.Show(question, title, MessageBoxButton.YesNo) ==         
                   MessageBoxResult.Yes
    }
}

Then you could use in any ViewModel like this

public class FooViewModel
{
    public FooViewModel(IDialogService dialogService)
    {
        DialogService = dialogService;
    }

    public IDialogService DialogService { get; set; }

    public DelegateCommand DeleteBarCommand
    {
        get
        {
            return new DelegateCommand(DeleteBar);
        }
    }

    public void DeleteBar()
    {
        var shouldDelete = DialogService.AskYesNoQuestion("Are you sure you want to delete bar?", "Delete Bar");
        if (shouldDelete)
        {
            Bar.Delete();
        }
    }

    ...
}

OTHER TIPS

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!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top