对话框或Window
一般来说与视图相关。如果你想实现 MVVM,那么你必须将视图与视图模型分开。 MVVM 将帮助您完成此任务。
MVVM dependency graph and responsibilities overview
依赖关系图显示视图依赖于视图模型。这种依赖是单向的,目的是解耦。这仅是由于数据绑定机制才可能实现的。
需要强调的是,MVVM 是一种应用程序架构设计模式。它从以下位置查看应用程序成分观点而不是阶级观点。显然,将数据绑定的源类命名为“ViewModel”的广泛做法是相当具有误导性的。事实上,由于视图是由许多类(例如控件)组成的,因此视图模型也是如此。它是一个组件。
由于对话框是视图的一部分,因此Window.Show()
由视图模型调用会添加一个非法箭头,从视图模型指向视图。这意味着视图模型现在依赖于视图。
现在,您已经创建了对视图的显式依赖关系,您将遇到新问题(MVVM 最初试图解决的问题):如果您决定显示不同的窗口类型或用弹出窗口替换对话框(或者换句话说,随时)你修改了视图),那么你就必须修改视图模型。这正是 MVVM 设计时要避免的。
解决方案是让 View 自行处理。当视图需要显示对话框时,它必须自行完成。
对话框是提供用户交互的一种方式。用户交互不是
视图模型的业务。
如果您需要专用服务来处理显示 GUI,那么它必须完全在视图中操作 - 因此它不能被视图模型引用。
由于视图模型与数据相关(数据的表示),因此它只能标记视图可以触发的数据相关状态(例如数据验证错误,其中推荐的方法是实现INotifyDataErrorInfo https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifydataerrorinfo?view=netframework-4.8在视图模型上)。
我的建议是让你的 View Model 摆脱 View 相关的责任,并使其业务仅专注于模型数据呈现 - 或者放弃 MVVM 并返回到最初的解耦问题。
解决方案1
最简单的方法是从代码隐藏或路由事件处理程序显示对话框。它可以由视图模型抛出或引发的异常或事件触发。例如,如果写入文件失败,视图模型可以引发 FileError 事件,视图会侦听该事件并做出反应,例如,通过向用户显示对话框。
然后使用以下方法将收集到的数据(如果这是输入对话框)传递到视图模型(如果需要)ICommand
或通过更新数据绑定。
代码隐藏并不违反 MVVM,因为 MVVM 是基于组件的,而代码隐藏是一种 C# 语言,对于 MVVM 的概念来说是未知的。设计模式的一个要求是它必须与语言无关。在 MVVM 的定义中,代码隐藏不起任何作用——它没有被提及。
解决方案2
或者通过实现 ContentControl(或 UserControl)来设计您自己的对话框。这样的控件完美地融入了 WPF 框架,并允许编写 MVVM 兼容的代码。您现在可以利用数据绑定和数据触发器来显示和隐藏控件/对话框。
本机 Windows 对话框不能很好地集成到 WPF 框架中。 AWindow
无法使用触发器显示。我们必须调用Show()
or 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
e.g. DialogControl
,这在整个应用程序中更易于使用(没有重复的代码,改进的处理)。您可以将常见的窗口镶边添加到对话框中,例如标题栏、图标和镶边按钮来控制对话框的状态。
编辑以显示错误以及“对话服务”违反/消除 MVVM 的原因
视图模型组件引用的所有内容要么也是视图模型的一部分,要么是模型的一部分。上面的 MVVM 依赖关系图很好地表明 View 组件对于 View Model 组件是完全未知的。现在,视图模型已知的对话框服务如何像对话框一样显示视图的模块,not违反了 MVVM 模式?显然,要么视图模型组件了解视图组件的模块,要么视图模型包含非法责任。不管怎样,对话服务显然不能解决问题。从名为的类中移动代码...ViewModel
到一个名为...Service
,其中原始类仍然引用新类,在架构方面没有做任何事情。代码仍然位于同一组件中,由视图模型类引用。除了显示对话框的类的名称之外,没有任何更改。给一个类命名并不会改变它的性质。例如,将我的数据绑定源命名为 MainView 而不是 MainViewModel 并不会使 MainView 成为视图的一部分。
一般来说,类命名或命名约定与架构、MVVM 完全无关。责任和依赖性是利益问题。
以下是对话服务引入的依赖项is 经营者 the 查看模型:
正如您所看到的,我们现在有一个从视图模型指向视图的箭头(依赖项)。现在视图中的更改将反映到视图模型并影响实现。这是因为视图模型现在涉及视图逻辑 - 在这种特殊情况下是用户交互逻辑。用户交互逻辑是GUI,是View。从视图模型控制这个逻辑会让人尖叫“MVVM 违规”......
如果你能接受这种违规行为就好了。但这违反了 MVVM 设计模式,不能作为“显示对话框的 MVVM 方式”出售。至少我们不应该买它。