将View接口实例传递给ViewModel

本文关键字:ViewModel 实例 View 接口 | 更新日期: 2023-09-27 18:17:14

在WPF/MVVM中,您有时需要ViewModel来触发视图层的事情,例如显示MessageBox,打开新窗口,关闭当前窗口,或基于ViewModel中的某些条件状态启动动画。

MVVM纯粹主义者似乎同意ViewModel不应该知道View。因此,为了解决上述场景,除了一些技巧来解决一些简单的场景之外,一个常见的范例是使用消息传递。想象一下,使用一个消息传递系统只是为了显示一个消息框——MVVM可以使琐碎的事情变得相当复杂。

让我们考虑一个不同的方法。首先,我让我的视图实现一个接口:

public class MyWindow : IClosableView

然后,在ViewModel的构造函数中,我让它接受该接口的一个实例作为参数:

public class MyWindowViewModel(IClosableView view)

当我在View的构造函数中设置DataContext时,我只需要传入View本身:

public MyWindow()
{
    InitializeComponents();
    this.DataContext = new MyWindowViewModel(this);
}

这使得ViewModel通过View做我们上面提到的事情非常简单:

public void Close()
{
    this.view.Close();
}

现在,在你们所有的MVVM纯粹主义者开始向我随意扔易碎的东西之前,让我们看看我们在这里做什么。ViewModel将接口获取到视图,而不是视图本身。这意味着尽管ViewModel知道视图,

  1. 为了触发必要的视图端操作(如果使用消息传递方法,它无论如何都需要这样做),它只知道它真正需要的数量。
  2. 它不依赖于任何特定的视图;它只要求使用它的视图提供某些功能,在本例中是关闭该视图的能力。
  3. ViewModel仍然是完全可测试的,因为视图可以通过在另一个类中实现IClosableView接口并在单元测试中通过来模拟。
基于这个推理,我很好奇:为什么消息传递比这种简单有效的方法更好?

Edit:为了让事情更清楚,正如我在这个问题的开头所说的,我谈论的是视图操作依赖于ViewModel状态的情况。让一个按钮关闭一个窗口非常简单,只需在代码隐藏中连接它。但如果它依赖于ViewModel中的某些状态呢?

将View接口实例传递给ViewModel

我认为MVVM纯度的主要焦点是您的第2点,它不知道视图,但期望提供一组已定义的功能。

这本身就开始建立一个依赖关系,即视图模型只能与某些视图一起使用。

这可能在你的应用程序中工作,但它不是模式。这是一个不同的解决方案,如果你能让它工作,那就去做吧。

通常是倒置依赖关系。ViewModel不知道任何视图的任何信息。对于ViewModel,视图是可替换的,甚至不需要工作。因此ViewModel被传递给View,或者在这里被实例化。View与ViewModel通信,例如,从代码隐藏(事件),通过命令,绑定或触发器。

当ViewModel包含业务逻辑时,视图只实现表示逻辑。所以显示对话框不是ViewModel的工作。视图本身会通过触发器、绑定或事件(例如Clicked)来显示对话框。为了绑定目的,ViewModel通常实现INotifyPropertyChanged,或者是DependencyObject的后代。

假设你点击退出按钮关闭应用程序,然后视图将订阅UIElements(按钮的)Clicked事件并调用this.Close()关闭(或启动一个对话框)。ViewModel没有以主动的方式参与其中,因为它不知道任何视图。

<!--  View.xaml  -->
<Window.Resources>
     <viewModel:MainViewModel x:Key="MyViewModel">
</Window.Resources>
...
<Button x:Name="ExitButton" Clicked="CloseApp_OnClicked">
// View.xaml.cs (code-behind)
public void CloseApp_OnClicked(object sender, MouseEventArgs e)
{
    // Check if the ViewModel's data is saved before closing the app (state check)
    var theViewModel = this.Resources["MyViewModel"] as MainViewModel;
    if ( (theViewModel != null) && (theViewModel.DataIsSaved) )
        this.Close();
}

请求的例子:一个动画是由一个ViewModels属性值触发的。当value为true时,动画被踢出。在这个例子中,图像的不透明度是动画的。触发器使用绑定来观察源和触发器的指定值,并附加到您想要动画的元素上,在本例中为image:

// ViewModel
class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private bool credentialsAreValid;
    public bool CredentialsAreValid 
    { 
        get { return this.credentialsAreValid; }
        set 
        {
             this.credentialsAreValid = value; 
             OnPropertyChanged(); // Not implemented.
        }
    }
}
XAML:

<!-- View -->
<Window.Resources>
    <viewModel:ViewModel x:Key="MyViewModel">
</Window.Resource>
<Window.DataContext>
    <Binding Source="{staticResource MyViewmodel}">
</Window.DataContext>
<Image x:Name="AnimatedImage">
    <Image.Style>
        <Style x:Name="ToggleAnimationStyle" TargetType=Image>
            <Style.Triggers>
                <DataTrigger x:Name="ValidCredentialsTrigger Binding={Binding CredentialsAreValid} Value="True">
                    <DataTrigger.EnterActions>
                         <BeginStoryboard>
                              <Storyboard x:Name="FadeInStoryBoard">
                                     <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" FillBehavior="HoldEnd" BeginTime="0:0:0" Duration="0:0:3"/>
                               </Storyboard>
                          </BeginStoryboard>
                      </DataTrigger.EnterActions>
                      <DataTrigger.ExitActions>
                        <BeginStoryboard>
                            <Storyboard x:Name="FadeOutStoryBoard">
                                  <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" FillBehavior="HoldEnd" BeginTime="0:0:0" Duration="0:0:10">
                                         <DoubleAnimation.EasingFunction>
                                              <ExponentialEase EasingMode="EaseIn"/>
                                         </DoubleAnimation.EasingFunction>
                                   </DoubleAnimation>
                             </Storyboard>
                         </BeginStoryboard>
                      </DataTrigger.ExitActions>
                 </DataTrigger>
          </Style.Triggers>
        </Style>
    </Image.Style>
</Image>