При создании приложений MVVM вы обычно разбиваете сложные экраны информации на набор родительских и дочерних представлений, где дочерние представления содержатся в родительских представлениях на панелях или элементах управления контейнера и сами формируют иерархию использования.
-
После декомпозиции сложных представлений это не означает, что каждый фрагмент дочернего контента, который вы разделяете в свой собственный файл XAML, обязательно должен быть представлением MVVM.
-
Кусок контента просто обеспечивает структуру для визуализации чего-либо на экране и не поддерживает ввод или манипулирование пользователем для этого контента.
-
Возможно, он не нуждается в отдельной ViewModel, но это может быть просто фрагмент XAML, который визуализируется на основе свойств, предоставляемых родительской ViewModel.
-
Наконец, если у вас есть иерархия Views и ViewModel, родительский ViewModel может стать концентратором для связи, так что каждый дочерний ViewModel может оставаться отделенным от других дочерних ViewModels и от их родителя, насколько это возможно.
После декомпозиции сложных представлений это не означает, что каждый фрагмент дочернего контента, который вы разделяете в свой собственный файл XAML, обязательно должен быть представлением MVVM.
Кусок контента просто обеспечивает структуру для визуализации чего-либо на экране и не поддерживает ввод или манипулирование пользователем для этого контента.
Возможно, он не нуждается в отдельной ViewModel, но это может быть просто фрагмент XAML, который визуализируется на основе свойств, предоставляемых родительской ViewModel.
Наконец, если у вас есть иерархия Views и ViewModel, родительский ViewModel может стать концентратором для связи, так что каждый дочерний ViewModel может оставаться отделенным от других дочерних ViewModels и от их родителя, насколько это возможно.
Давайте рассмотрим пример, в котором мы определим простую иерархию между различными представлениями. Создайте новый проект приложения WPF MVVMHierarchiesDemo
Шаг 1 — Добавьте три папки (Model, ViewModel и Views) в ваш проект.
Шаг 2. Добавьте классы Customer и Order в папку Model, CustomerListView и OrderView в папку Views, а также CustomerListViewModel и OrderViewModel в папку ViewModel, как показано на следующем рисунке.
Шаг 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. Для этого мы будем использовать метод, вызываемый по навигационной системе, который принимает строку назначения и возвращает свойство 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; } } } }
Получите все ваши ViewModels из класса BindableBase. Когда приведенный выше код скомпилирован и выполнен, вы увидите следующий вывод.
Как вы можете видеть, мы добавили только две кнопки и CurrentViewModel в наше главное окно. Если вы нажмете любую кнопку, она перейдет к этому конкретному представлению. Давайте нажмем кнопку «Клиенты», и вы увидите, что отображается CustomerListView.
Мы рекомендуем выполнить вышеприведенный пример пошагово для лучшего понимания.