Search code examples
c#wpfmvvmuser-controlsicommand

How to implement commands on a user control without resorting to code-behind?


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...


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();
            }
        }
    
        ...
    }