将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知道视图,
- 为了触发必要的视图端操作(如果使用消息传递方法,它无论如何都需要这样做),它只知道它真正需要的数量。
- 它不依赖于任何特定的视图;它只要求使用它的视图提供某些功能,在本例中是关闭该视图的能力。
- ViewModel仍然是完全可测试的,因为视图可以通过在另一个类中实现IClosableView接口并在单元测试中通过来模拟。
Edit:为了让事情更清楚,正如我在这个问题的开头所说的,我谈论的是视图操作依赖于ViewModel状态的情况。让一个按钮关闭一个窗口非常简单,只需在代码隐藏中连接它。但如果它依赖于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>