【问题标题】:MVVM DialogService alternativesMVVM DialogService 替代品
【发布时间】:2019-07-21 07:25:30
【问题描述】:

在学习如何使用 MVVM 模式进行编程时,我遇到了一个常见问题 - 从 ViewModel 中显示各种对话框。

起初它对我来说看起来很简单。我创建了一个 IWindowService 接口,在 WindowService 类中实现了它。我使用这个类来启动新的视图窗口。

但后来我需要 MessageBox 样式的对话框。所以我创建了一个 IDialogService 和一个 DialogService 类。我对打开/保存文件对话框做了同样的事情。

毕竟我注意到创建 ViewModel 实例变得相当复杂:

ViewModel VM = new ViewModel(Data, AnotherData, MoreData, WindowService, DialogService, FileDialogService, AnotherService);

我尝试将所有服务合并到一个 FrontendService 类中,但这使得维护变得相当困难,并且 IFrontendService 接口变得非常“臃肿”。

现在我正在寻找替代方法。对我来说最好的情况是不需要将实例传递给 ViewModel 构造函数。

【问题讨论】:

标签: c# wpf mvvm


【解决方案1】:

对话框或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 方式”出售。至少我们不应该买它。

【讨论】:

  • 我看不出我是如何违反 MVVM 原则的。我通过为 ViewModel 提供一个正是这样做的服务来保持 ViewModel 不受用户交互的影响。当我决定对我的 ViewModel 进行单元测试时,我可以提供一个什么都不做的虚拟服务。 ViewModel 不关心 Service 做什么。它只调用服务的方法并期待结果。它不关心服务是否显示对话框。
  • 你在隐藏它。仅仅因为视图模型没有明确显示它并没有删除依赖关系。该服务由视图模型使用。该图看起来像这样:视图模型 -> 服务 -> 控制。由于服务在视图域中运行,因此视图模型依赖于视图。您刚刚从视图模型中提取了对话逻辑并将其放入视图模型仍然必须知道的另一个类中。应该是:控制->服务->视图模型。对话逻辑(何时、为什么、哪个以及如何显示)不是视图模型的责任。这是视图逻辑。
  • "ViewModel 不关心服务做什么。它只调用服务的方法并期待结果。"这就是重点:MVVM 对依赖关系的描述如下:视图模型只调用模型上的操作。所以模型是查询数据的唯一目标。除此之外,视图模型是绝对被动的。它等待绑定更新并等待调用命令。想象一下视图和视图模型将驻留在不同的 DLL 中。现在,当您需要显示不同的对话框时,您必须修改“ViewModel.dll”。
  • 那为什么互联网上(包括本站)有这么多文章建议这种方法?
  • 之所以需要使用对话服务,是因为当人们沉迷于 No-Code-Behind 成语时。他们的看法是代码隐藏违反了 MVVM。当 MVVM 是关于依赖关系时,那么代码隐藏如何违反这种模式呢?当您查看 Microsoft 如何实现他们的控件时,您会发现大量的代码隐藏。这是因为 XAML 功能强大,但并未涵盖所有用例。有时您需要定义依赖属性或路由事件。有时您必须实现仅在 XAML 中无法完成的复杂逻辑和行为。 WPF 的设计使 XAML 依赖于 C#。
猜你喜欢
  • 2014-11-04
  • 1970-01-01
  • 1970-01-01
  • 2011-09-09
  • 1970-01-01
  • 2023-04-09
  • 1970-01-01
  • 1970-01-01
  • 2014-04-29
相关资源
最近更新 更多