且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

在 View 中使用确认逻辑命令绑定到 ViewModel

更新时间:2022-10-21 09:53:27

可以通过显示来自 ViewModel 的消息框来满足确认要求.但是,我不认为这是要走的路.它不会破坏MVVM吗?

在使用与视图相关的依赖项(如MessageBox")时保留 MVVM 样式的一种方法是将它们封装并注入到视图模型中.因此,您可以通过在构造函数中请求 IDialogService 来表达依赖关系:

公共类 MainViewModel : ViewModelBase{私有只读 IDialogService _dialog;公共 MainViewModel(IDialogService 对话框){_dialog = 对话;}}

然后你从视图中传入实现:

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

该接口封装了您需要的任何功能,例如警报"、确认"等

公共接口 IDialogService{bool Confirm(string message, string caption = "Confirm");}

并使用 MessageBox 或任何其他方法实现它(并为单元测试切换一个虚拟实现):

公共类 DialogService : IDialogService{public bool Confirm(字符串消息,字符串标题){返回 MessageBox.Show(message, caption, MessageBoxButton.OKCancel) == MessageBoxResult.OK;}}

这样你就可以将所有的确认逻辑从视图移动到视图模型,其中保存"方法看起来像这样:

private void Save(){if (!_dialog.Confirm("你想覆盖吗?", "覆盖?"))返回;this.SaveCommand.Execute(null);}

如果 CanExecute 取决于 UI(代码隐藏)和 ViewModel 的状态会怎样?

如果您关心测试,那么 CanExecute 所依赖的任何东西都不应该在代码隐藏中——您应该将类​​似的任何东西移到视图模型中.

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?

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.