将View接口实例传递给ViewModel

时间:2014-08-10 07:07:32

标签: c# wpf mvvm

在WPF / MVVM中,有时需要ViewModel在View层触发事物,例如显示MessageBox,打开新窗口,关闭当前窗口或根据ViewModel中的某些条件状态启动动画。

MVVM纯粹主义者似乎同意ViewModel永远不会知道View。因此,要解决上述场景,除了少数hacks来解决一些简单场景之外,常见的范例是使用消息传递。想象一下,使用消息传递系统只是为了显示消息框 - MVVM can make trivial stuff pretty complicated

让我们考虑一种不同的方法。首先,我使我的View实现了一个接口:

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接口并在单元测试中传递它来模拟视图。
  4. 鉴于这种推理,我很好奇:为什么消息传递比这种简单有效的方法更好?

    编辑:正如我在本期问题开头所说的那样,只是为了清楚地说明,我正在讨论View操作取决于ViewModel状态的情况。通过在代码隐藏中将其连接起来,使按钮关闭窗口非常容易。但是,什么时候取决于ViewModel中的某些状态?

2 个答案:

答案 0 :(得分:2)

我认为MVVM纯度的主要焦点是你的观点2,它不知道视图,但期望提供一组定义的特征。

这本身就开始构建一个依赖项,viewmodel只能用于某些视图。

这可能适用于您的应用,但它不是模式。这是一个不同的解决方案,如果你可以开展工作,那就去做吧。

答案 1 :(得分:1)

通常你会反转依赖关系。 ViewModel对任何视图都不了解。对于ViewModel,View是可替换的,甚至不需要工作。因此,ViewModel将传递给View或在此处实例化。 View与ViewModel通信,例如从代码隐藏(事件),通过命令,绑定或触发器。

虽然ViewModel包含业务逻辑,但视图仅实现表示逻辑。因此,显示对话框不是ViewModel的工作。相反,View本身会显示一个对话框,例如通过触发器,绑定或事件(例如Clicked)。出于绑定的目的,ViewModel通常实现INotifyPropertyChanged或者是DependencyObject的后代。

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

<!--  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属性值触发动画。当值为true时,动画会被踢。在此示例中,图像的不透明度是动画的。 触发器使用绑定来观察指定值的源和触发器,并附加到您希望在此示例中为图像设置动画的元素:

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