qouoww

质量管理+软件开发=聚焦管理软件的开发与应用

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

ViewModel-first方法对Stylet的架构至关重要,但如果你以传统的View-first方式学习MVVM,那么这种方法就不直观了。

希望本文能把一切都说清楚。

视图优先方法

让我们从定义视图优先方法开始。MVVM 声明 ViewModel 应该对 View 一无所知,反过来说View应该知道 ViewModel。将View与ViewModel绑定在一起的最简单方式是将ViewModel放置在View的Codebehind里,类似下面的代码:

public partial class MyView : Window
{
   public MyView()
   {
      InitializeComponent();
 
      this.DataContext = new MyViewModel();
   }
}

当然视图还可以创建和拥有其他视图,可以将多个视图构成视图树,所有这些都还好。

但是像下面这样的情况,

<!-- This is a window which contains a top bar and another page -->
<Window x:Class="MyNamespace.ShellView" ....>
   <StackPanel>
      <my:TopBarView/>
      <Frame x:Name="navigationFrame"/>
   </StackPanel>
</Window>

这里的TopBarView有其ViewModel,TopBarViewModel。

假定TopBarView里有一个字段的数据想要去更新,比如当前页面的标题。现在,ShellViewModel知晓哪一个Page是当前页面,但是TopBarViewModel不知道。怎么办,只好在TopBarView中暴露一个依赖属性,然后缄定到ShellViewModel,如下所示:


<Window x:Class="MyNamespace.ShellView" .... x:Name="rootObject">
   <StackPanel>
      <my:TopBarView CurrentPageTitle="{Binding CurrentPageTitle, ElementName=rootObject}"/>
      <Frame x:Name="navigationFrame"/>
   </StackPanel>
</Window>

这真的不够优雅。

另一个主要问题是显示窗口和对话框。在传统的MVVM中,这有点痛苦。一种选择是从 ViewModel 内部实例化和显示 View(using Show()或 ShowDialog()),这使其或至少其中的一部分无法测试)。更好的选择是在视图的codebehind中实例化,然后在那里显示。这意味着您需要建立告诉View显示此对话框的方法,以及将对话框的结果返回到 ViewModel 的方法。

实际上,设置上述Frame内容需要实例化视图以放入其中。这具有相同的困境 - 要么 ViewModel 实例化它(使其不可测试),要么在视图实例化它(导致通信痛苦)。

无论哪种方式,这种方法都不太优雅。

ViewModel优先的实践

ViewModel优先的模式使得ViewModel与View相互之间独立存在,实现了完美的分离。取而代之的是采用第三方的服务来建立View与ViewModel之间的关系,配置其相应的DataContext。

默认的实现是使用命名约定来建立联系,对于一个给定的ViewModel,将其变量名中的“ViewModel”替换为“View”即可。更多细节参见ViewManager。

这使得ViewModel可以由其他ViewModel创建,也允许组合ViewModel的属性。

还是举一个例子:


public class ShellViewModel
{
   public TopBarViewModel TopBar { get; private set; }
   // Stuff to instantiate and assign TopBarViewModel
}

<Window x:Class="MyNamespace.ShellView"
        xmlns:s="https://github.com/canton7/Stylet" .....>
   <StackPanel>
      <ContentControl s:View.Model="{Binding TopBar}"/>
      <!-- ... -->
   </StackPanel>
</Window>

View.Model附加属性从其ViewModel的绑定中获取ViewModel(此例中是TopBarViewModel的一个实例),然后定位到正确的View上(TopBarView)。通过这种方式实例化,将内容设置到ContentControl中。

此例中,TopBarView即可以从其TopBarViewModel中获取当前页面的名称,也可通过ShellViewModel获得页面名称的通知,问题得到了解决!

同样,ContentControl在Navigation中也工作得很好:

<Window x:Class="MyNamespace.ShellView"
        xmlns:s="https://github.com/canton7/Stylet" .....>
   <StackPanel>
      <ContentControl s:View.Model="{Binding TopBar}"/>
      <ContentControl s:View.Model="{Binding CurrentPage}"/>
   </StackPanel>
</Window>

ShellViewModel通过实例化一个页面的ViewModel导航到一个新的页面中,然后将此实例分配给属性CurrentPage。注意ShellViewModel不再需要知道任何关于视图(views)的信息,没必要再去实例化一个单独的view了,这一点非常重要,也非常有用。

对话框(Dialogs)和窗体(Windows)也可以通过WindowManager用同样的方法处理。只需要传递给出的ViewModel实例,对话框或窗体的View就会显示出来。

删除Code-Behind!

通过这一系列操作,没必要再写codebehind的代码了。通过使用Actions(处理事件),Converters,附加属性和附件行为,删除Code-Behind完全可以!

posted on 2022-01-10 21:40  qouoww  阅读(1284)  评论(0编辑  收藏  举报