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完全可以!