Command binding to ViewModel with confirmation logic in View

眉间皱痕 提交于 2021-02-07 21:51:36

问题


Looking for the most elegant solution to bind a button command to a ViewModel ICommand property, while allowing confirmation in the View.

What I would like to do:

  1. Only allow a user to click a button when he/she should
  2. When the button is clicked, ask a confirmation
  3. If comfirmed, do work in the ViewModel, otherwise cancel
  4. Do not break MVVM architecture

The confirmation requirement can be fulfilled by showing a messagebox from the ViewModel. However, I don't think this is the way to go. Doesn't it break MVVM? What if CanExecute depends on the state of both UI (code-behind) and ViewModel? Also, what about testability when having a messagebox pop-up from the ViewModel?

Another thing I tried is binding both OnClick (to View) and Command (to ViewModel). Although the Event is always executed before the Command, there seems to be no way to cancel the command from being executed. Also, the execution order seems to be an undocumented feature, so something that you should not rely on. Besides this, it still does not allow CanExecute to take into account View logic.

Next I came up with the following solution:

View (XAML)

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Button Content="Do Work" Command="{Binding Path=ViewModel.SaveCommand}"/>
    </Grid>
    <SelectTemplateUserControl Visibility="Collapsed" OnTemplateSelected="SelectTemplate_OnTemplateSelected"/>
</Window>

View (Code-Behind)

public partial class MainWindow : Window
{
    private readonly MainViewModel _viewModel = new MainViewModel();

    public MainWindow()
    {
        InitializeComponent();
    }

    public MainViewModel ViewModel { get { return this._viewModel; } }    

    public ICommand SaveCommand { get { return new RelayCommand<int>(this.Save,                                 this.CanSave);} }

    private bool CanSave(int templateId)
    {
        return this._viewModel.SaveCommand.CanExecute(null);
    }

    private void Save(int templateId)
    {
        var messageBoxResult = MessageBox.Show("Do you want to overwrite?",               "Overwrite?", MessageBoxButton.OKCancel);

        if (messageBoxResult == MessageBoxResult.Cancel)
            return;

        // Call method to hide main Grid and set SelectTemplateUserControl to visible..
    }

    private void SelectTemplate_OnTemplateSelected(object sender, int templateId)
    {
        this._viewModel.SaveCommand.Execute(templateId);
    }
}

ViewModel

public class MainViewModel : ViewModelBase
{
    public ICommand SaveCommand { get { return new RelayCommand<int>(this.Save,              this.CanSave); } }

    private bool CanSave(int templateId)
    {
        // Can Save Logic, returning a bool
    }

    private void Save(int templateId)
    {
        // Save Logic....
    }
}

I think it follows the MVVM pattern nicely, it also achieves Single Responsiblity. But is this the best way of doing it? Are there other possibilities?


回答1:


The confirmation requirement can be fulfilled by showing a messagebox from the ViewModel. However, I don't think this is the way to go. Doesn't it break MVVM?

One way of preserving an MVVM style while using view-related dependencies like "MessageBox", is to encapsulate and inject them into the view-model. So, you might express the dependency by asking for an IDialogService in the constructor:

public class MainViewModel : ViewModelBase
{
    private readonly IDialogService _dialog;

    public MainViewModel(IDialogService dialog)
    {
        _dialog = dialog;
    }
}

Then you pass the implementation in from the view:

private readonly MainViewModel _viewModel = new MainViewModel(new DialogService());

The interface encapsulates whatever functionality you need, so maybe "Alert", "Confirm", etc.

public interface IDialogService
{
    bool Confirm(string message, string caption = "Confirm");
}

And implement it using MessageBox, or any other approach (and switch out a dummy implementation for unit testing):

public class DialogService : IDialogService
{
    public bool Confirm(string message, string caption)
    {
        return MessageBox.Show(message, caption, MessageBoxButton.OKCancel) == MessageBoxResult.OK;
    }
}

That way you can move all the confirmation logic from the view to the view-model, where the "Save" method would just look like this:

private void Save()
{
    if (!_dialog.Confirm("Do you want to overwrite?", "Overwrite?"))
        return;

    this.SaveCommand.Execute(null);
}

What if CanExecute depends on the state of both UI (code-behind) and ViewModel?

If you are concerned about testing, then nothing that CanExecute depends on should be in the code-behind -- you should move anything like that to the view-model.



来源:https://stackoverflow.com/questions/24091634/command-binding-to-viewmodel-with-confirmation-logic-in-view

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!