在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确实了解了一个视图,但
鉴于这种推理,我很好奇:为什么消息传递比这种简单有效的方法更好?
编辑:正如我在本期问题开头所说的那样,只是为了清楚地说明,我正在讨论View操作取决于ViewModel状态的情况。通过在代码隐藏中将其连接起来,使按钮关闭窗口非常容易。但是,什么时候取决于ViewModel中的某些状态?
答案 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>