MVVM:代码隐藏是邪恶的还是实用主义的?

MVVM: Is code-behind evil or just pragmatic?

假设您想要一个 Save & Close 和一个 Cancel & Close 按钮在您喜欢的 WPF MVVM 上 window?

你会怎么做? MVVM 指示您将按钮绑定到 ICommand,而控制反转指示您的 View 可能知道您的 ViewModel,但反之则不然。

在网上搜索我发现了一个解决方案,它有一个 ViewModel 关闭事件,View 像这样订阅:

private void OnLoaded(Object sender
    , RoutedEventArgs e)
{
    IFilterViewModel viewModel = (IFilterViewModel)DataContext;
    viewModel.Closing += OnViewModelClosing;
}

private void OnViewModelClosing(Object sender
    , EventArgs<Result> e)
{
    IFilterViewModel viewModel = (IFilterViewModel)DataContext;
    viewModel.Closing -= OnViewModelClosing;
    DialogResult = (e.Value == Result.OK) ? true : false;
    Close();
}

但这是代码隐藏与我目前设计得非常好的 MVVM 的混合。

另一个问题是在显示主 window 时显示许可问题消息框。我可以再次像上面那样使用 Window.Loaded 事件,但这也破坏了 MVVM,不是吗?

在这些情况下,是否有一种干净的方法或者应该务实而不是迂腐?

看看一些工具包,例如MVVMLight 具有 EventToCommand,它允许您将命令绑定到事件。我通常会尽量限制 View 中的逻辑,因为它很难测试。

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:command="http://www.galasoft.ch/mvvmlight"

...

<i:Interaction.Triggers>
   <i:EventTrigger EventName="Loaded">
     <command:EventToCommand Command="{Binding YourCommandInVM}"/>
   </i:EventTrigger>
</i:Interaction.Triggers>

使用代码隐藏没有错或对,这主要是基于意见,取决于您的喜好。

example 展示了如何使用无代码隐藏的 MVVM 设计模式关闭 window。

<Button Name="btnLogin" IsDefault="True" Content="Login" Command="{Binding ShowLoginCommand}" CommandParameter="{Binding ElementName=LoginWindow}"/> 
<!-- the CommandParameter should bind to your window, either by name or relative or what way you choose, this will allow you to hold the window object and call window.Close() -->

基本上您将 window 作为参数传递给命令。 IMO 你的视图模型不应该知道控件,所以这个版本不是那么好。我会将 Func<object>/ 一些接口传递给视图模型,以使用依赖注入关闭 window。

有时我会使用变通方法。

假设你有一个 view "MainWindow" 和一个 viewmodel "MainWindowVM".

public class MainWindowVM
{
    private MainWindow mainWindow;
    public delegate void EventWithoudArg();
    public event EventWithoudArg Closed;

    public MainWindowVM()
    {
        mainWindow = new MainWindow();
        mainWindow.Closed += MainWindow_Closed;
        mainWindow.DataContext = this;
        mainWindow.Loaded += MainWindow_Loaded;
        mainWindow.Closing += MainWindow_Closing;
        mainWindow.Show();
    }

    private void MainWindow_Loaded()
    {
        //your code
    }

    private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        //your code
    }

    private void MainWindow_Closed()
    {
        Closed?.Invoke();
    }
}

这里我将我的视图存储在一个私有变量中,以便您在需要时可以访问它。它打破了一点 MVVM。
在我的视图模型中,我创建了一个新的 view 并显示它。
在这里,我还捕获了 view 的关闭事件并将其传递给自己的事件。
您还可以向 view.

.Loaded.Closing 事件添加方法

在 App.xaml.cs 中,您只需创建一个新的 viewmodel 对象。

public partial class App : Application
{
    public App()
    {
        MainWindowVM mainWindowVM = new MainWindowVM();
        mainWindowVM.Closed += Mwvm_Close;
    }

    private void Mwvm_Close()
    {
        this.Shutdown();
    }
}

我创建了一个新的 viewmodel 对象并捕获它自己的关闭事件并将其绑定到 App 的关闭方法。

您的描述表明视图模型是某种文档视图。如果那是正确的,那么我会让 Save, Close, etc. 由文档容器处理,例如应用程序或主要 window,因为这些命令位于文档之上的级别,就像 copy/paste 位于应用程序级别一样。事实上 ApplicationCommands 已经为保存和关闭预定义了命令,这表明框架作者采用了某种方法。

首先,创建一个只包含Close方法的接口:

interface IClosable
{
    void Close();
}

接下来,让您的 window 实现 IClosable:

class MyWindow : Window, IClosable
{
    public MyWindow()
    {
        InitializeComponent();
    }
}

然后让视图将自身作为 IClosable 作为命令参数传递给视图模型:

<Button Command="{Binding CloseCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

最后,命令调用 Close:

CloseCommand = new DelegateCommand<IClosable>( view => view.Close() );

我们现在有什么?

  • 我们有一个按钮可以关闭 window
  • 我们在代码隐藏中没有代码 except , IClosable
  • 视图模型对视图一无所知,它只是获取一个可以关闭的任意对象
  • 命令可以很容易地进行单元测试