对话框或Window 通常与视图相关。如果要实现 MVVM,则必须将 View 与 View Model 分开。 MVVM 将帮助您完成此任务。
MVVM 依赖图和职责概述
依赖图显示视图依赖于视图模型。为了解耦,这种依赖是单向的。这仅是由于数据绑定机制的缘故。
需要强调的是,MVVM 是一种应用程序架构设计模式。它从 component 的角度而不是类的角度来查看应用程序。显然,将数据绑定的源类命名为“ViewModel”的广泛传播做法非常具有误导性。事实上,由于 View 由许多类(例如控件)组成,因此 View Model 也是如此。它是一个组件。
由于对话框是视图的一部分,视图模型调用的Window.Show() 将添加一个非法箭头,该箭头从视图模型指向视图。这意味着视图模型现在依赖于视图。
现在,您已经创建了对 View 的显式依赖,您将遇到新问题(MVVM 最初试图解决的问题):如果您决定显示不同的窗口类型或用弹出窗口替换对话框(或者换句话说,任何时候您修改视图),那么您将不得不修改视图模型。这正是 MVVM 旨在避免的。
解决方案是让 View 自己处理。当 View 需要显示一个对话框时,它必须自己做。
对话框是一种提供用户交互的方式。用户交互不是
View Model 的业务。
如果您需要一个专门的服务来处理显示 GUI,那么它必须完全在视图中运行 - 因此它不能被视图模型引用。
由于 View Model 与数据相关(数据表示),它只能标记 View 可以触发的与数据相关的状态(例如,数据验证错误,推荐的方法是在 View Model 上实现INotifyDataErrorInfo)。
我的建议是让您的 View Model 摆脱与 View 相关的责任,并将其业务只专注于模型数据呈现 - 或者放弃 MVVM 并回到最初的解耦问题。
解决方案 1
最简单的方法是从代码隐藏或路由事件处理程序中显示对话框。它可以由 View Model 抛出或引发的异常或事件触发。例如,如果写入文件失败,View Model 可以引发 FileError 事件,View 会监听并做出反应,例如,通过向用户显示对话框。
然后通过使用ICommand 或更新数据绑定将收集的数据(如果这是一个输入对话框)传递给视图模型(如果需要)。
代码隐藏不违反 MVVM,因为 MVVM 是基于组件的,而代码隐藏是一种 C# 语言,MVVM 的概念是未知的。设计模式的一个要求是它必须与语言无关。在 MVVM 的定义中,代码隐藏没有任何作用——它没有被提及。
解决方案 2
也可以通过实现 ContentControl(或 UserControl)来设计您自己的对话框。这样的控件完美地融入了 WPF 框架,并允许编写符合 MVVM 的代码。您现在可以使用数据绑定和数据触发器来显示和隐藏控件/对话框。
本机 Windows 对话框不能很好地集成到 WPF 框架中。 Window 无法使用触发器显示。我们必须调用Show() 或DialogShow() 方法。这就是最初的问题“如何以符合 MVVM 的方式显示对话框” 的根源。
这是一个模态对话框示例,只能使用 XAML 显示和隐藏。不涉及 C#。它使用事件触发器为对话框网格的Visibility 设置动画(或者为Opacity 设置动画)。该事件由Button 触发。对于不同的场景,Button 可以简单地替换为DataTrigger 或使用BooloeanToVisibilityConverter 的绑定:
<Window>
<Window.Triggers>
<EventTrigger RoutedEvent="Button.Click" SourceName="OpenDialogButton">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="ExampleDialog"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid SnapsToDevicePixels="True" x:Name="Root">
<!-- The example dialog -->
<Grid x:Name="ExampleDialog" Visibility="Hidden" Panel.ZIndex="100" VerticalAlignment="Top">
<!-- The Rectangle stretches over the whole window area -->
<!-- and covers all window child elements except the dialog -->
<!-- This prevents user interaction with the covered elements -->
<!-- and adds modal behavior to the dialog -->
<Rectangle
Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=ActualHeight}"
Fill="Gray" Opacity="0.7" />
<Grid Width="400" Height="200" >
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<Border Grid.RowSpan="2" Background="LightGray" BorderBrush="Black" BorderThickness="1">
<Border.Effect>
<DropShadowEffect BlurRadius="5" Color="Black" Opacity="0.6" />
</Border.Effect>
</Border>
<TextBlock Grid.Row="0" TextWrapping="Wrap"
Margin="30"
Text="I am a modal dialog and my Visibility or Opacity property can be easily modified by a trigger or a nice animation" />
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right" Height="50" >
<Button x:Name="OkButton"
Content="Ok" Width="80" />
<Button x:Name="CancelButton" Margin="30,0,30,0"
Content="Cancel" Width="80" />
</StackPanel>
</Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExampleDialog"
Storyboard.TargetProperty="Visibility"
Duration="0">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
<! The actual control or page content -->
<StackPanel>
<TextBlock Text="This is some page content" />
<!-- The button to open the dialog. This can be replaced by a DataTrigger -->
<Button x:Name="OpenDialogButton" Content="ShowDialog" Width="100" Height="50" />
</StackPanel>
</Grid>
</Window>
这是对话框:
您可以封装对话框并将实现移动到专用的Control,例如DialogControl,在整个应用程序中更容易使用(没有重复代码,改进了处理)。您可以在对话框中添加常用的窗口镶边,如标题栏、图标和镶边按钮,以控制对话框的状态。
编辑以显示错误以及“对话服务”为何违反/消除 MVVM
View Model 组件引用的所有内容要么也是 View Model 的一部分,要么是 Model 的一部分。上面的 MVVM 依赖关系图很好地表明了 View 组件对于 View Model 组件是完全未知的。现在,View Model 知道的对话服务如何像对话一样显示 View 的模块,不违反 MVVM 模式?显然,要么是 View Model 组件知道 View 组件的模块,要么是 View Model 包含非法职责。不管怎样,对话服务显然不能解决问题。将代码从名为 ...ViewModel 的类移动到名为 ...Service 的类中,其中原始类仍然具有对新类的引用,这在架构方面没有任何作用。代码仍然在同一个组件中,由 View Model 类引用。除了显示对话框的类的名称外,没有任何变化。给一个类命名并不会改变它的性质。例如,将我的数据绑定源命名为 MainView 而不是 MainViewModel 不会使 MainView 成为视图的一部分。
就 MVVM 而言,一般的类命名或命名约定在架构方面绝对不相关。职责和依赖关系是感兴趣的问题。
以下是由由操作视图模型的对话服务引入的依赖项:
如您所见,我们现在有一个箭头(依赖关系),它从 View Model 指向 View。现在视图中的更改将反映到视图模型并影响实现。这是因为视图模型现在涉及到视图逻辑——在这种特殊情况下是用户交互逻辑。用户交互逻辑是GUI,是View。从视图模型控制这个逻辑尖叫“MVVM 违规”...
如果您能接受这种违规行为,那很好。但它违反了 MVVM 设计模式,不能作为“显示对话框的 MVVM 方式”出售。至少我们不应该买它。