- MVVM 教程
- MVVM - 首页
- MVVM – 简介
- MVVM - 优势
- MVVM - 职责
- MVVM - 第一个应用程序
- MVVM - 连接视图
- MVVM - 连接 ViewModel
- MVVM - WPF 数据绑定
- MVVM - WPF 数据模板
- MVVM - ViewModel 通信
- MVVM – 层次结构与导航
- MVVM - 验证
- MVVM - 依赖注入
- MVVM - 事件
- MVVM - 单元测试
- MVVM - 框架
- MVVM - 面试问题
- MVVM 有用资源
- MVVM - 快速指南
- MVVM - 有用资源
- MVVM - 讨论
MVVM – 层次结构与导航
构建 MVVM 应用程序时,通常会将复杂的信息屏幕分解成一组父视图和子视图,其中子视图包含在面板或容器控件中的父视图内,并形成自身使用的层次结构。
分解复杂的视图后,并不意味着您分离到其自身 XAML 文件中的每个子内容部分都需要成为 MVVM 视图。
内容块仅提供渲染内容到屏幕的结构,并且不支持用户对该内容的任何输入或操作。
它可能不需要单独的 ViewModel,而可能只是一个基于父 ViewModel 公开属性进行渲染的 XAML 块。
最后,如果您具有视图和 ViewModel 的层次结构,则父 ViewModel 可以成为通信中心,以便每个子 ViewModel 尽可能地与其他子 ViewModel 及其父级分离。
让我们来看一个定义不同视图之间简单层次结构的示例。创建一个新的 WPF 应用程序项目 MVVMHierarchiesDemo
步骤 1 − 将三个文件夹(Model、ViewModel 和 Views)添加到您的项目中。
步骤 2 − 在 Model 文件夹中添加 Customer 和 Order 类,在 Views 文件夹中添加 CustomerListView 和 OrderView,在 ViewModel 文件夹中添加 CustomerListViewModel 和 OrderViewModel,如下面的图像所示。
步骤 3 − 在 CustomerListView 和 OrderView 中添加文本块。以下是 CustomerListView.xaml 文件。
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
以下是 OrderView.xaml 文件。
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
现在我们需要一些东西来托管这些视图,在我们的 MainWindow 中这是一个好地方,因为它是一个简单的应用程序。我们需要一个容器控件,我们可以放置我们的视图并在导航方式中切换它们。为此,我们需要在我们的 MainWindow.xaml 文件中添加 ContentControl,我们将使用它的 content 属性并将其绑定到 ViewModel 引用。
现在在资源字典中为每个视图定义数据模板。以下是 MainWindow.xaml 文件。请注意每个数据模板如何将数据类型(ViewModel 类型)映射到相应的 View。
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
每当当前视图模型设置为 CustomerListViewModel 的实例时,它将呈现一个 CustomerListView,其中 ViewModel 已连接。它是一个订单 ViewModel,它将呈现 OrderView 等等。
我们现在需要一个 ViewModel,它具有 CurrentViewModel 属性以及一些逻辑和命令,以便能够切换属性内部的 ViewModel 的当前引用。
让我们为此 MainWindow 创建一个名为 MainWindowViewModel 的 ViewModel。我们可以从 XAML 创建 ViewModel 的实例并使用它来设置窗口的 DataContext 属性。为此,我们需要创建一个基类来封装我们 ViewModel 的 INotifyPropertyChanged 实现。
此类的主要思想是封装 INotifyPropertyChanged 实现并向派生类提供帮助器方法,以便它们可以轻松触发适当的通知。以下是 BindableBase 类的实现。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
现在是时候使用我们的 CurrentViewModel 属性开始进行一些视图切换了。我们只需要某种方式来驱动此属性的设置。我们将使其成为最终用户可以命令转到客户列表或订单视图。首先在您的项目中添加一个新类,该类将实现 ICommand 接口。以下是 ICommand 接口的实现。
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
现在我们需要为这些 ViewModel 设置一些顶级导航,并且切换的逻辑应该属于 MainWindowViewModel 内部。为此,我们将使用一个名为 on navigate 的方法,该方法接受一个字符串目标并返回 CurrentViewModel 属性。
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
为了导航这些不同的视图,我们需要在我们的 MainWindow.xaml 文件中添加两个按钮。以下是完整的 XAML 文件实现。
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
以下是完整的 MainWindowViewModel 实现。
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
从 BindableBase 类派生所有 ViewModel。当上述代码编译并执行时,您将看到以下输出。
如您所见,我们只在 MainWindow 上添加了两个按钮和一个 CurrentViewModel。如果单击任何按钮,它将导航到该特定视图。让我们单击“客户”按钮,您将看到 CustomerListView 显示出来。
我们建议您逐步执行上述示例以更好地理解。