MVVM - иерархии и навигация
При создании приложений MVVM вы обычно разбиваете сложные экраны информации на набор родительских и дочерних представлений, где дочерние представления содержатся внутри родительских представлений в панелях или контейнерных элементах управления, и сами формируют иерархию использования.
После декомпозиции сложных представлений это не означает, что каждый фрагмент дочернего содержимого, который вы разделяете в свой собственный файл XAML, обязательно должен быть представлением MVVM.
Фрагмент контента просто обеспечивает структуру для отображения чего-либо на экране и не поддерживает какой-либо ввод или манипуляции со стороны пользователя для этого контента.
Возможно, ему не нужна отдельная ViewModel, но это может быть просто кусок XAML, который отображается на основе свойств, предоставляемых родительской ViewModel.
Наконец, если у вас есть иерархия представлений и ViewModel, родительская ViewModel может стать центром взаимодействия, так что каждая дочерняя ViewModel может оставаться в максимально возможной степени отделенной от других дочерних ViewModel и их родительских.
Давайте посмотрим на пример, в котором мы определим простую иерархию между различными представлениями. Создайте новый проект приложения WPFMVVMHierarchiesDemo
Step 1 - Добавьте три папки (Model, ViewModel и Views) в свой проект.
Step 2 - Добавьте классы Customer и Order в папку Model, CustomerListView и OrderView в папку Views, а также CustomerListViewModel и OrderViewModel в папку ViewModel, как показано на следующем изображении.
Step 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, потому что это простое приложение. Нам нужен контейнерный элемент управления, в котором мы можем размещать наши представления и переключать их в режиме навигации. Для этого нам нужно добавить ContentControl в наш файл MainWindow.xaml, и мы будем использовать его свойство content и привязать его к ссылке ViewModel.
Теперь определите шаблоны данных для каждого представления в словаре ресурсов. Ниже приведен файл MainWindow.xaml. Обратите внимание, как каждый шаблон данных сопоставляет тип данных (тип ViewModel) с соответствующим представлением.
<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 внутри свойства.
Давайте создадим ViewModel для этого MainWindow под названием MainWindowViewModel. Мы можем просто создать экземпляр нашей ViewModel из XAML и использовать его для установки свойства DataContext окна. Для этого нам нужно создать базовый класс, чтобы инкапсулировать реализацию INotifyPropertyChanged для наших ViewModels.
Основная идея этого класса - инкапсулировать реализацию 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
}
}
Теперь нам нужно настроить навигацию верхнего уровня для них в ViewModels, и логика для этого переключения должна принадлежать MainWindowViewModel. Для этого мы собираемся использовать метод, называемый 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. Когда приведенный выше код скомпилирован и выполнен, вы увидите следующий результат.
Как видите, мы добавили только две кнопки и CurrentViewModel в наш MainWindow. Если вы нажмете любую кнопку, он перейдет к этому конкретному представлению. Нажмите кнопку «Клиенты», и вы увидите, что отображается CustomerListView.
Мы рекомендуем вам выполнить приведенный выше пример пошагово для лучшего понимания.