Учебники

MVVM — Краткое руководство

MVVM — Введение

Хорошо упорядоченный и, возможно, самый повторно используемый способ организации вашего кода — это использование шаблона MVVM. Модель, представление, ViewModel (шаблон MVVM) — это руководство, помогающее вам организовать и структурировать код для написания поддерживаемых, тестируемых и расширяемых приложений.

Модель — она ​​просто хранит данные и не имеет ничего общего с какой-либо бизнес-логикой.

ViewModel — действует как связующее звено между моделью и представлением и делает вещи красивыми.

Представление — оно просто содержит отформатированные данные и по существу делегирует все в модель.

Посмотреть модель

Отдельная презентация

Чтобы избежать проблем, вызванных размещением логики приложения в коде позади или в XAML, лучше всего использовать технику, известную как раздельное представление. Мы пытаемся избежать этого, так как у нас будет XAML и программный код с минимумом, необходимым для непосредственной работы с объектами пользовательского интерфейса. Классы пользовательского интерфейса также содержат код для сложных поведений взаимодействия, логику приложения и все остальное, как показано на следующем рисунке слева.

Отдельная презентация

  • С раздельным представлением класс пользовательского интерфейса намного проще. Конечно, у него есть XAML, но код ниже делает это настолько мало, насколько это практически возможно.

  • Логика приложения принадлежит к отдельному классу, который часто называют моделью.

  • Однако это еще не все. Если вы остановитесь здесь, вы, вероятно, повторите очень распространенную ошибку, которая приведет вас на путь безумия привязки данных.

  • Многие разработчики пытаются использовать привязку данных для непосредственного подключения элементов в XAML к свойствам в модели.

  • Теперь иногда это может быть хорошо, но часто это не так. Проблема в том, что модель целиком связана с тем, что делает приложение, а не с тем, как пользователь взаимодействует с приложением.

  • То, как вы представляете данные, часто несколько отличается от их внутренней структуры.

  • Более того, большинство пользовательских интерфейсов имеют некоторое состояние, которое не относится к модели приложения.

  • Например, если ваш пользовательский интерфейс использует перетаскивание, необходимо что-то отслеживать, например, где находится перетаскиваемый элемент, как должен изменяться его внешний вид при перемещении по возможным объектам перетаскивания, и как эти объекты могут также изменить, как элемент перетаскивается на них.

  • Такое состояние может быть на удивление сложным и нуждается в тщательном тестировании.

  • На практике вы обычно хотите, чтобы какой-то другой класс находился между пользовательским интерфейсом и моделью. Это имеет две важные роли.

    • Во-первых, он адаптирует модель вашего приложения для конкретного представления пользовательского интерфейса.

    • Во-вторых, именно там живет любая нетривиальная логика взаимодействия, и под этим я подразумеваю код, необходимый для того, чтобы ваш пользовательский интерфейс работал так, как вы хотите.

С раздельным представлением класс пользовательского интерфейса намного проще. Конечно, у него есть XAML, но код ниже делает это настолько мало, насколько это практически возможно.

Логика приложения принадлежит к отдельному классу, который часто называют моделью.

Однако это еще не все. Если вы остановитесь здесь, вы, вероятно, повторите очень распространенную ошибку, которая приведет вас на путь безумия привязки данных.

Многие разработчики пытаются использовать привязку данных для непосредственного подключения элементов в XAML к свойствам в модели.

Теперь иногда это может быть хорошо, но часто это не так. Проблема в том, что модель целиком связана с тем, что делает приложение, а не с тем, как пользователь взаимодействует с приложением.

То, как вы представляете данные, часто несколько отличается от их внутренней структуры.

Более того, большинство пользовательских интерфейсов имеют некоторое состояние, которое не относится к модели приложения.

Например, если ваш пользовательский интерфейс использует перетаскивание, необходимо что-то отслеживать, например, где находится перетаскиваемый элемент, как должен изменяться его внешний вид при перемещении по возможным объектам перетаскивания, и как эти объекты могут также изменить, как элемент перетаскивается на них.

Такое состояние может быть на удивление сложным и нуждается в тщательном тестировании.

На практике вы обычно хотите, чтобы какой-то другой класс находился между пользовательским интерфейсом и моделью. Это имеет две важные роли.

Во-первых, он адаптирует модель вашего приложения для конкретного представления пользовательского интерфейса.

Во-вторых, именно там живет любая нетривиальная логика взаимодействия, и под этим я подразумеваю код, необходимый для того, чтобы ваш пользовательский интерфейс работал так, как вы хотите.

MVVM — Преимущества

Шаблон MVVM, в конечном счете, является современной структурой шаблона MVC, поэтому основная цель остается той же, чтобы обеспечить четкое разделение между предметной логикой и уровнем представления. Вот некоторые из преимуществ и недостатков шаблона MVVM.

Ключевым преимуществом является возможность истинного разделения между представлением и моделью, помимо достижения разделения и эффективности, которую вы получаете от этого. В реальном выражении это означает, что когда ваша модель нуждается в изменении, ее можно легко изменить, не обращаясь к представлению, и наоборот.

Есть три важных ключевых момента, вытекающих из применения MVVM, которые заключаются в следующем.

Ремонтопригодность

  • Четкое разделение различных типов кода должно облегчить переход к одной или нескольким более детализированным и целенаправленным частям и вносить изменения, не беспокоясь.

  • Это означает, что вы можете оставаться гибкими и быстро переходить на новые версии.

Четкое разделение различных типов кода должно облегчить переход к одной или нескольким более детализированным и целенаправленным частям и вносить изменения, не беспокоясь.

Это означает, что вы можете оставаться гибкими и быстро переходить на новые версии.

способность быть свидетелем в суде

  • С MVVM каждый фрагмент кода является более детализированным, и если он реализован правильно, ваши внешние и внутренние зависимости находятся в отдельных фрагментах кода от частей с базовой логикой, которую вы хотели бы протестировать.

  • Это значительно облегчает написание модульных тестов на основе базовой логики.

  • Убедитесь, что он работает правильно, когда написано, и продолжает работать, даже когда все меняется в процессе обслуживания.

С MVVM каждый фрагмент кода является более детализированным, и если он реализован правильно, ваши внешние и внутренние зависимости находятся в отдельных фрагментах кода от частей с базовой логикой, которую вы хотели бы протестировать.

Это значительно облегчает написание модульных тестов на основе базовой логики.

Убедитесь, что он работает правильно, когда написано, и продолжает работать, даже когда все меняется в процессе обслуживания.

растяжимость

  • Это иногда пересекается с ремонтопригодностью из-за чистых границ разделения и более детальных фрагментов кода.

  • У вас больше шансов сделать любую из этих частей более пригодной для повторного использования.

  • Он также имеет возможность заменять или добавлять новые фрагменты кода, которые делают подобные вещи в нужных местах в архитектуре.

Это иногда пересекается с ремонтопригодностью из-за чистых границ разделения и более детальных фрагментов кода.

У вас больше шансов сделать любую из этих частей более пригодной для повторного использования.

Он также имеет возможность заменять или добавлять новые фрагменты кода, которые делают подобные вещи в нужных местах в архитектуре.

Очевидная цель паттерна MVVM — абстракция View, который уменьшает объем бизнес-логики в программном коде. Тем не менее, ниже приведены некоторые другие существенные преимущества —

  • ViewModel проще для модульного тестирования, чем код за кодом или событийный код.
  • Вы можете проверить это без неуклюжих автоматизации и взаимодействия пользовательского интерфейса.
  • Уровень представления и логика слабо связаны.

Недостатки

  • Некоторые люди думают, что для простых пользовательских интерфейсов MVVM может быть излишним.
  • Точно так же в больших случаях может быть трудно спроектировать ViewModel.
  • Отладка будет немного сложнее, когда у нас сложные привязки данных.

MVVM — Обязанности

Шаблон MVVM состоит из трех частей — Model, View и ViewModel. Большинство разработчиков с самого начала немного смущаются относительно того, что Model, View и ViewModel должны или не должны содержать, и каковы обязанности каждой части.

В этой главе мы изучим обязанности каждой части шаблона MVVM, чтобы вы могли четко понимать, к какому типу кода относится. MVVM — это действительно многоуровневая архитектура для клиентской стороны, как показано на следующем рисунке.

MVVM Обязанности

  • Уровень представления состоит из представлений.

  • Логическим слоем являются модели представления.

  • Уровень представления представляет собой комбинацию объектов модели.

  • Клиентские службы, которые производят и сохраняют их, либо направляют доступ в двухуровневом приложении, либо через вызовы служб, а затем к вашему приложению.

  • Службы клиента официально не являются частью шаблона MVVM, но они часто используются с MVVM для достижения дальнейшего разделения и предотвращения дублирования кода.

Уровень представления состоит из представлений.

Логическим слоем являются модели представления.

Уровень представления представляет собой комбинацию объектов модели.

Клиентские службы, которые производят и сохраняют их, либо направляют доступ в двухуровневом приложении, либо через вызовы служб, а затем к вашему приложению.

Службы клиента официально не являются частью шаблона MVVM, но они часто используются с MVVM для достижения дальнейшего разделения и предотвращения дублирования кода.

Обязанности модели

В общем, модель является самой простой для понимания. Это модель данных на стороне клиента, которая поддерживает представления в приложении.

  • Он состоит из объектов со свойствами и некоторых переменных для хранения данных в памяти.

  • Некоторые из этих свойств могут ссылаться на другие объекты модели и создавать граф объектов, который в целом является объектами модели.

  • Объекты модели должны вызывать уведомления об изменении свойств, что в WPF означает привязку данных.

  • Последняя ответственность — проверка, которая является необязательной, но вы можете встроить информацию проверки в объекты модели, используя функции проверки привязки данных WPF через такие интерфейсы, как INotifyDataErrorInfo / IDataErrorInfo

Он состоит из объектов со свойствами и некоторых переменных для хранения данных в памяти.

Некоторые из этих свойств могут ссылаться на другие объекты модели и создавать граф объектов, который в целом является объектами модели.

Объекты модели должны вызывать уведомления об изменении свойств, что в WPF означает привязку данных.

Последняя ответственность — проверка, которая является необязательной, но вы можете встроить информацию проверки в объекты модели, используя функции проверки привязки данных WPF через такие интерфейсы, как INotifyDataErrorInfo / IDataErrorInfo

Посмотреть обязанности

Основная цель и обязанности представлений состоит в том, чтобы определить структуру того, что пользователь видит на экране. Структура может содержать статические и динамические части.

  • Статические части — это иерархия XAML, которая определяет элементы управления и макет элементов управления, из которых состоит представление.

  • Динамическая часть похожа на анимацию или изменения состояния, которые определены как часть представления.

  • Основная цель MVVM состоит в том, что в представлении не должно быть никакого кода.

  • Невозможно, чтобы в поле зрения не было кода. По крайней мере, вам нужен конструктор и вызов для инициализации компонента.

  • Идея состоит в том, что код логики обработки событий, действий и манипуляций с данными не должен быть в коде позади в View.

  • Существуют также другие виды кода, которые должны идти в коде за любым кодом, для которого требуется ссылка на элемент пользовательского интерфейса, который является неотъемлемым представлением кода.

Статические части — это иерархия XAML, которая определяет элементы управления и макет элементов управления, из которых состоит представление.

Динамическая часть похожа на анимацию или изменения состояния, которые определены как часть представления.

Основная цель MVVM состоит в том, что в представлении не должно быть никакого кода.

Невозможно, чтобы в поле зрения не было кода. По крайней мере, вам нужен конструктор и вызов для инициализации компонента.

Идея состоит в том, что код логики обработки событий, действий и манипуляций с данными не должен быть в коде позади в View.

Существуют также другие виды кода, которые должны идти в коде за любым кодом, для которого требуется ссылка на элемент пользовательского интерфейса, который является неотъемлемым представлением кода.

ViewModel Обязанности

  • ViewModel является основной целью приложения MVVM. Основная ответственность ViewModel заключается в предоставлении данных для представления, чтобы представление могло выводить эти данные на экран.

  • Это также позволяет пользователю взаимодействовать с данными и изменять данные.

  • Другая ключевая обязанность ViewModel — инкапсулировать логику взаимодействия для представления, но это не означает, что вся логика приложения должна входить в ViewModel.

  • Он должен иметь возможность обрабатывать соответствующую последовательность вызовов, чтобы все происходило правильно в зависимости от пользователя или любых изменений в представлении.

  • ViewModel также должен управлять любой логикой навигации, например, решать, когда пора переходить к другому представлению.

ViewModel является основной целью приложения MVVM. Основная ответственность ViewModel заключается в предоставлении данных для представления, чтобы представление могло выводить эти данные на экран.

Это также позволяет пользователю взаимодействовать с данными и изменять данные.

Другая ключевая обязанность ViewModel — инкапсулировать логику взаимодействия для представления, но это не означает, что вся логика приложения должна входить в ViewModel.

Он должен иметь возможность обрабатывать соответствующую последовательность вызовов, чтобы все происходило правильно в зависимости от пользователя или любых изменений в представлении.

ViewModel также должен управлять любой логикой навигации, например, решать, когда пора переходить к другому представлению.

MVVM — Первое применение

В этой главе мы узнаем, как использовать шаблоны MVVM для простого экрана ввода и приложение WPF, к которому вы, возможно, уже привыкли.

Давайте посмотрим на простой пример, в котором мы будем использовать подход MVVM.

Шаг 1 — Создайте новый проект приложения WPF MVVMDemo.

Первый шаг приложения 1

Шаг 2 — Добавьте три папки (Model, ViewModel и Views) в ваш проект.

Первый шаг приложения 2

Шаг 3 — Добавьте класс StudentModel в папку Model и вставьте приведенный ниже код в этот класс

using System.ComponentModel;

namespace MVVMDemo.Model {
 
   public class StudentModel {}
	
   public class Student : INotifyPropertyChanged {
      private string firstName; 
      private string lastName;
		
      public string FirstName { 
         get { 
            return firstName; 
         }
			
         set { 
            if (firstName != value) { 
               firstName = value; 
               RaisePropertyChanged("FirstName"); 
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
		
      public string LastName { 
         get {return lastName; } 
			
         set {
            if (lastName != value) { 
               lastName = value;
               RaisePropertyChanged("LastName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
		
      public string FullName { 
         get { 
            return firstName + " " + lastName; 
         } 
      }
		
      public event PropertyChangedEventHandler PropertyChanged;
		
      private void RaisePropertyChanged(string property) {
         if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
         } 
      } 
   } 
}

Шаг 4 — Добавьте другой класс StudentViewModel в папку ViewModel и вставьте следующий код.

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
				
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

Шаг 5. Добавьте новый элемент управления пользователя (WPF), щелкнув правой кнопкой мыши папку «Виды» и выберите «Добавить»> «Новый элемент»…

Первый шаг приложения 5

Шаг 6 — Нажмите кнопку Добавить. Теперь вы увидите файл XAML. Добавьте следующий код в файл StudentView.xaml, который содержит различные элементы пользовательского интерфейса.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
		
         <ItemsControl ItemsSource = "{Binding Path = Students}">
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
                  <StackPanel Orientation = "Horizontal">
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/>
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/>
								
                  </StackPanel> 
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
				
         </ItemsControl> 
			
      </StackPanel> 
   </Grid> 
	
</UserControl>

Шаг 7. Теперь добавьте StudentView в файл MainPage.xaml, используя следующий код.

<Window x:Class = "MVVMDemo.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:MVVMDemo"
   xmlns:views = "clr-namespace:MVVMDemo.Views"
   mc:Ignorable = "d"
   Title = "MainWindow" Height = "350" Width = "525">
	
   <Grid>
      <views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
   </Grid>
	
</Window>

Шаг 8 — Вот реализация для события Loaded в файле MainPage.xaml.cs, который обновит View из ViewModel.

using System.Windows;

namespace MVVMDemo {

   /// <summary>
      /// Interaction logic for MainWindow.xaml
   /// </summary>
	
   public partial class MainWindow : Window {
	
      public MainWindow() {
         InitializeComponent();
      }
		
      private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
         MVVMDemo.ViewModel.StudentViewModel studentViewModelObject = 
            new MVVMDemo.ViewModel.StudentViewModel();
         studentViewModelObject.LoadStudents();
			
         StudentViewControl.DataContext = studentViewModelObject;
      }
   }
}

Шаг 9 — Когда вышеприведенный код скомпилирован и выполнен, вы получите следующий вывод в главном окне.

Первый шаг приложения 9

Мы рекомендуем выполнить вышеприведенный пример пошагово для лучшего понимания.

MVVM — Подключение просмотров

В этой главе мы рассмотрим различные способы подключения ваших представлений к ViewModel. Во-первых, давайте посмотрим на конструкцию View first, где мы можем объявить ее в XAML. Как мы видели в примере из предыдущей главы, мы подключили вид из главного окна. Теперь мы увидим другие способы подключения просмотров.

Мы будем использовать тот же пример в этой главе. Ниже приведена та же реализация класса Model.

using System.ComponentModel;

namespace MVVMDemo.Model {
 
   public class StudentModel {} 
	
   public class Student : INotifyPropertyChanged { 
      private string firstName; 
      private string lastName;
		
      public string FirstName { 
         get { return firstName; }
			
         set { 
            if (firstName != value) { 
               firstName = value; 
               RaisePropertyChanged("FirstName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
	
      public string LastName {
         get { return lastName; } 
			
         set { 
            if (lastName != value) { 
               lastName = value; 
               RaisePropertyChanged("LastName");
               RaisePropertyChanged("FullName"); 
            } 
         } 
      }
	
      public string FullName { 
         get { 
            return firstName + " " + lastName; 
         } 
      }
	
      public event PropertyChangedEventHandler PropertyChanged;
	
      private void RaisePropertyChanged(string property) { 
         if (PropertyChanged != null) { 
            PropertyChanged(this, new PropertyChangedEventArgs(property)); 
         } 
      } 
   }  
}

Вот реализация класса ViewModel. На этот раз метод LoadStudents вызывается в конструкторе по умолчанию.

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel{ 

   public class StudentViewModel { 
	
      public StudentViewModel() { 
         LoadStudents(); 
      } 
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

Независимо от того, является ли представление окном, пользовательским элементом управления или страницей, парсер обычно работает сверху вниз и слева направо. Он вызывает конструктор по умолчанию для каждого элемента при его обнаружении. Есть два способа построить представление. Вы можете использовать любой на них.

  • Посмотреть первую конструкцию в XAML
  • Посмотреть первую конструкцию в коде позади

Посмотреть первую конструкцию в XAML

Один из способов — просто добавить вашу ViewModel как вложенный элемент в установщик для свойства DataContext, как показано в следующем коде.

<UserControl.DataContext> 
   <viewModel:StudentViewModel/> 
</UserControl.DataContext>

Вот полный файл View XAML.

<UserControl x:Class="MVVMDemo.Views.StudentView"
   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:MVVMDemo.Views"
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
   mc:Ignorable = "d"
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.DataContext>
      <viewModel:StudentViewModel/>
   </UserControl.DataContext>
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}">
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                      <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate>
				
         </ItemsControl> 
      </StackPanel> 
   </Grid> 
	
</UserControl>

Посмотреть первую конструкцию в коде позади

Другой способ заключается в том, что вы можете получить конструкцию View first, просто создав собственную модель представления в коде вашего View, установив там свойство DataContext вместе с экземпляром.

Как правило, свойство DataContext устанавливается в методе конструктора представления, но вы также можете отложить конструкцию, пока не произойдет событие Load представления.

using System.Windows.Controls;

namespace MVVMDemo.Views {
 
   /// <summary> 
      /// Interaction logic for StudentView.xaml 
   /// </summary> 
	
   public partial class StudentView : UserControl { 
      public StudentView() { 
         InitializeComponent(); 
         this.DataContext = new MVVMDemo.ViewModel.StudentViewModel(); 
      } 
   } 
}

Одна из причин создания модели представления в Code-behind вместо XAML заключается в том, что конструктор модели View принимает параметры, но при разборе XAML элементы могут создаваться только в том случае, если они определены в конструкторе по умолчанию.

Теперь в этом случае XAML-файл View будет выглядеть так, как показано в следующем коде.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" 
   d:DesignWidth = "300">
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
			
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"<
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate>
				
         </ItemsControl> 
      </StackPanel> 
   </Grid>
	
</UserControl>

Вы можете объявить это представление в MainWindow, как показано в файле MainWindow.XAML.

<Window x:Class = "MVVMDemo.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:MVVMDemo" 
   xmlns:views = "clr-namespace:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   Title = "MainWindow" Height = "350" Width = "525"> 
	
   <Grid> 
      <views:StudentView x:Name = "StudentViewControl"/> 
   </Grid> 
	
</Window>

Когда приведенный выше код компилируется и выполняется, вы увидите следующий вывод в главном окне.

Подключение просмотра главного окна

Мы рекомендуем выполнить вышеприведенный пример пошагово для лучшего понимания.

MVVM — Подключение ViewModel

В этой главе мы расскажем, как подключить ViewModel. Это продолжение последней главы, в которой мы обсуждали первую конструкцию View. Теперь следующая форма первой конструкции — это мета-шаблон, известный как ViewModelLocator . Это псевдо паттерн и накладывается поверх паттерна MVVM.

  • В MVVM каждый View должен быть подключен к его ViewModel.

  • ViewModelLocator — это простой подход для централизации кода и дополнительной развязки представления.

  • Это означает, что он не должен явно знать о типе ViewModel и как его конструировать.

  • Существует несколько различных подходов к использованию ViewModelLocator, но здесь мы используем наиболее похожий на тот, который является частью инфраструктуры PRISM.

В MVVM каждый View должен быть подключен к его ViewModel.

ViewModelLocator — это простой подход для централизации кода и дополнительной развязки представления.

Это означает, что он не должен явно знать о типе ViewModel и как его конструировать.

Существует несколько различных подходов к использованию ViewModelLocator, но здесь мы используем наиболее похожий на тот, который является частью инфраструктуры PRISM.

ViewModelLocator предоставляет стандартный, согласованный, декларативный и слабо связанный способ создания первой конструкции View, которая автоматизирует процесс подключения ViewModel к View. На следующем рисунке представлен процесс высокого уровня ViewModelLocator.

Подключение ViewModel

Шаг 1 — выяснить, какой тип представления создается.

Шаг 2 — Определите ViewModel для этого конкретного типа View.

Шаг 3 — Построить эту ViewModel.

Шаг 4 — Установите для DataContext Views значение ViewModel.

Чтобы понять основную концепцию, давайте посмотрим на простой пример ViewModelLocator, продолжая тот же пример из предыдущей главы. Если вы посмотрите на файл StudentView.xaml, вы увидите, что мы статически подключили ViewModel.

Теперь, как показано в следующей программе, прокомментируйте этот код XAML, также удалите код из Code-behind.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>-->
	
   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}">
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal">
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel> 
   </Grid>
	
</UserControl>

Теперь давайте создадим новую папку VML и добавим новый открытый класс ViewModelLocator, который будет содержать одно прикрепленное свойство (свойство зависимости) AutoHookedUpViewModel, как показано в следующем коде.

public static bool GetAutoHookedUpViewModel(DependencyObject obj) { 
   return (bool)obj.GetValue(AutoHookedUpViewModelProperty); 
}

public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) { 
   obj.SetValue(AutoHookedUpViewModelProperty, value); 
}

// Using a DependencyProperty as the backing store for AutoHookedUpViewModel. 
//This enables animation, styling, binding, etc...
 
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
   DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
   typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
   AutoHookedUpViewModelChanged));

И теперь вы можете увидеть базовое определение свойства присоединения. Чтобы добавить поведение к свойству, нам нужно добавить измененный обработчик событий для этого свойства, который содержит автоматический процесс подключения ViewModel для View. Код для этого следующий:

private static void AutoHookedUpViewModelChanged(DependencyObject d, 
   DependencyPropertyChangedEventArgs e) { 
   if (DesignerProperties.GetIsInDesignMode(d)) return; 
   var viewType = d.GetType(); 
   string str = viewType.FullName; 
   str = str.Replace(".Views.", ".ViewModel."); 
	
   var viewTypeName = str; 
   var viewModelTypeName = viewTypeName + "Model"; 
   var viewModelType = Type.GetType(viewModelTypeName); 
   var viewModel = Activator.CreateInstance(viewModelType);
   ((FrameworkElement)d).DataContext = viewModel; 
}

Ниже приводится полная реализация класса ViewModelLocator.

using System; 
using System.ComponentModel; 
using System.Windows;

namespace MVVMDemo.VML { 

   public static class ViewModelLocator { 
	
      public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
         return (bool)obj.GetValue(AutoHookedUpViewModelProperty); 
      }
		
      public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) { 
         obj.SetValue(AutoHookedUpViewModelProperty, value); 
      }
		
      // Using a DependencyProperty as the backing store for AutoHookedUpViewModel. 
		
      //This enables animation, styling, binding, etc...
      public static readonly DependencyProperty AutoHookedUpViewModelProperty =
         DependencyProperty.RegisterAttached("AutoHookedUpViewModel", 
         typeof(bool), typeof(ViewModelLocator), new
         PropertyMetadata(false, AutoHookedUpViewModelChanged));
		
      private static void AutoHookedUpViewModelChanged(DependencyObject d,
         DependencyPropertyChangedEventArgs e) { 
         if (DesignerProperties.GetIsInDesignMode(d)) return; 
         var viewType = d.GetType(); 
			
         string str = viewType.FullName; 
         str = str.Replace(".Views.", ".ViewModel."); 
			
         var viewTypeName = str; 
         var viewModelTypeName = viewTypeName + "Model";
         var viewModelType = Type.GetType(viewModelTypeName); 
         var viewModel = Activator.CreateInstance(viewModelType);
			
        ((FrameworkElement)d).DataContext = viewModel; 
      } 
   } 
}

Первое, что нужно сделать, это добавить пространство имен, чтобы мы могли получить этот тип ViewModelLocator в корне нашего проекта. Затем в элементе маршрута, который является типом представления, добавьте свойство AutoHookedUpViewModel и установите для него значение true.

xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"

Вот полная реализация файла StudentView.xaml.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
   
   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>-->

   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
            <ItemsControl.ItemTemplate> 
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/> 
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/>
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel>
   </Grid> 
	
</UserControl>

Когда приведенный выше код скомпилирован и выполнен, вы увидите, что ViewModelLocator подключает ViewModel для этого конкретного View.

Подключение ViewModel Главное окно

Ключевым моментом, на который следует обратить внимание, является то, что представление больше не связано с тем, каков его тип ViewModel или как он создается. Это все было перемещено в центральное место внутри ViewModelLocator.

MVVM — привязки данных WPF

В этой главе мы узнаем, как привязка данных поддерживает шаблон MVVM. Привязка данных является ключевой функцией, которая отличает MVVM от других шаблонов разделения пользовательского интерфейса, таких как MVC и MVP.

  • Для привязки данных вам необходимо создать представление или набор элементов пользовательского интерфейса, а затем вам нужен какой-то другой объект, на который будут указывать привязки.

  • Элементы пользовательского интерфейса в представлении привязаны к свойствам, предоставляемым ViewModel.

  • Порядок, на котором построены View и ViewModel, зависит от ситуации, так как мы сначала рассмотрели View.

  • View и ViewModel создаются, а DataContext для View устанавливается в ViewModel.

  • Привязки могут быть привязками данных OneWay или TwoWay для передачи данных назад и вперед между View и ViewModel.

Для привязки данных вам необходимо создать представление или набор элементов пользовательского интерфейса, а затем вам нужен какой-то другой объект, на который будут указывать привязки.

Элементы пользовательского интерфейса в представлении привязаны к свойствам, предоставляемым ViewModel.

Порядок, на котором построены View и ViewModel, зависит от ситуации, так как мы сначала рассмотрели View.

View и ViewModel создаются, а DataContext для View устанавливается в ViewModel.

Привязки могут быть привязками данных OneWay или TwoWay для передачи данных назад и вперед между View и ViewModel.

Давайте посмотрим на привязки данных в том же примере. Ниже приведен XAML-код StudentView.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">

   <!--<UserControl.DataContext> 
      <viewModel:StudentViewModel/> 
   </UserControl.DataContext>--> 

   <Grid> 
      <StackPanel HorizontalAlignment = "Left"> 
         <ItemsControl ItemsSource = "{Binding Path = Students}"> 
            <ItemsControl.ItemTemplate>
               <DataTemplate> 
					
                  <StackPanel Orientation = "Horizontal"> 
                     <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
                        Width = "100" Margin = "3 5 3 5"/>
								
                     <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
                        Width = "100" Margin = "0 5 3 5"/> 
								
                     <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
                        Margin = "0 5 3 5"/> 
								
                  </StackPanel> 
						
               </DataTemplate> 
            </ItemsControl.ItemTemplate> 
         </ItemsControl> 
      </StackPanel> 
   </Grid> 

</UserControl>
  • Если вы посмотрите на приведенный выше код XAML, то увидите, что ItemsControl привязан к коллекции учеников, предоставляемой ViewModel.

  • Вы также можете видеть, что свойство модели Student также имеет свои индивидуальные привязки, и они связаны с Textboxes и TextBlock.

  • ItemSource of ItemsControl может связываться со свойством Students, поскольку общий DataContext для View установлен на ViewModel.

  • Отдельные привязки свойств здесь также являются привязками DataContext, но они не привязываются к самой ViewModel из-за способа работы ItemSource.

  • Когда источник элемента связывается со своей коллекцией, он рендерит контейнер для каждого элемента при рендеринге и устанавливает DataContext этого контейнера для элемента. Таким образом, общий DataContext для каждого текстового поля и текстового блока в строке будет отдельным учеником в коллекции. Также вы можете видеть, что эти привязки для TextBox являются привязкой данных TwoWay, а для TextBlock это привязка данных OneWay, поскольку вы не можете редактировать TextBlock.

Если вы посмотрите на приведенный выше код XAML, то увидите, что ItemsControl привязан к коллекции учеников, предоставляемой ViewModel.

Вы также можете видеть, что свойство модели Student также имеет свои индивидуальные привязки, и они связаны с Textboxes и TextBlock.

ItemSource of ItemsControl может связываться со свойством Students, поскольку общий DataContext для View установлен на ViewModel.

Отдельные привязки свойств здесь также являются привязками DataContext, но они не привязываются к самой ViewModel из-за способа работы ItemSource.

Когда источник элемента связывается со своей коллекцией, он рендерит контейнер для каждого элемента при рендеринге и устанавливает DataContext этого контейнера для элемента. Таким образом, общий DataContext для каждого текстового поля и текстового блока в строке будет отдельным учеником в коллекции. Также вы можете видеть, что эти привязки для TextBox являются привязкой данных TwoWay, а для TextBlock это привязка данных OneWay, поскольку вы не можете редактировать TextBlock.

Когда вы снова запустите это приложение, вы увидите следующий вывод.

Главное окно привязок данных WPF

Давайте теперь изменим текст во втором текстовом поле первого ряда с Allain на Upston и нажмите tab, чтобы потерять фокус. Вы увидите, что текст TextBlock также обновляется.

Обновленный текстовый блок

Это связано с тем, что для привязок TextBox задано значение TwoWay, и оно также обновляет модель, а из модели снова обновляется TextBlock.

MVVM — шаблоны данных WPF

Шаблон описывает общий вид и внешний вид элемента управления. Для каждого элемента управления есть шаблон по умолчанию, связанный с ним, который создает видимость этого элемента управления. В приложении WPF вы можете легко создавать свои собственные шаблоны, когда вы хотите настроить визуальное поведение и внешний вид элемента управления. Связь между логикой и шаблоном может быть достигнута с помощью привязки данных.

В MVVM есть еще одна первичная форма, известная как первая конструкция ViewModel.

  • Первый метод построения ViewModel использует возможности неявных шаблонов данных в WPF.

  • Неявные шаблоны данных могут автоматически выбирать соответствующий шаблон из текущего словаря ресурсов для элемента, который использует привязку данных. Они делают это в зависимости от типа объекта данных, который отображается связыванием данных. Во-первых, вам нужно иметь некоторый элемент, который привязывается к объекту данных.

Первый метод построения ViewModel использует возможности неявных шаблонов данных в WPF.

Неявные шаблоны данных могут автоматически выбирать соответствующий шаблон из текущего словаря ресурсов для элемента, который использует привязку данных. Они делают это в зависимости от типа объекта данных, который отображается связыванием данных. Во-первых, вам нужно иметь некоторый элемент, который привязывается к объекту данных.

Давайте еще раз посмотрим на наш простой пример, в котором вы поймете, как вы можете сделать модель представления в первую очередь, используя шаблоны данных, особенно неявные шаблоны данных. Вот реализация нашего класса StudentViewModel.

using MVVMDemo.Model; 
using System.Collections.ObjectModel;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel {
	
      public StudentViewModel() { 
         LoadStudents(); 
      } 
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      } 
   } 
}

Вы можете видеть, что вышеупомянутая ViewModel не изменилась. Мы продолжим с тем же примером из предыдущей главы. Этот класс ViewModel просто предоставляет свойство коллекции Students и заполняет его при создании. Давайте перейдем к файлу StudentView.xaml, удалим существующую реализацию и определим шаблон данных в разделе Ресурсы.

<UserControl.Resources> 
   <DataTemplate x:Key = "studentsTemplate">
	
      <StackPanel Orientation = "Horizontal"> 
         <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
            Width = "100" Margin = "3 5 3 5"/> 
				
         <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
            Width = "100" Margin = "0 5 3 5"/> 
				
         <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
            Margin = "0 5 3 5"/> 
      </StackPanel> 
		
   </DataTemplate> 
</UserControl.Resources>

Теперь добавьте список и данные, связывающие этот список со свойством «Студенты», как показано в следующем коде.

<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>

В разделе «Ресурс» DataTemplate имеет ключ studentTemplate, а затем, чтобы фактически использовать этот шаблон, нам нужно использовать свойство ItemTemplate объекта ListBox. Итак, теперь вы можете видеть, что мы поручаем списку использовать этот конкретный шаблон для рендеринга этих учеников. Ниже приводится полная реализация файла StudentView.xaml.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate x:Key = "studentsTemplate"> 
		
         <StackPanel Orientation = "Horizontal"> 
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid> 
      <ListBox 
         ItemsSource = "{Binding Students}" 
         ItemTemplate = "{StaticResource studentsTemplate}"/> 
   </Grid>
	
</UserControl>

Когда приведенный выше код скомпилирован и выполнен, вы увидите следующее окно, которое содержит один ListBox. Каждый ListBoxItem содержит данные объекта класса Student, которые отображаются в полях TextBlock и Text.

Главное окно шаблонов данных WPF

Чтобы сделать этот шаблон неявным, нам нужно удалить свойство ItemTemplate из списка и добавить свойство DataType в определение нашего шаблона, как показано в следующем коде.

<UserControl.Resources> 
   <DataTemplate DataType = "{x:Type data:Student}">
	
      <StackPanel Orientation = "Horizontal"> 
         <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
            Width = "100" Margin = "3 5 3 5"/> 
				
         <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
            Width = "100" Margin = "0 5 3 5"/> 
				
         <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
            Margin = "0 5 3 5"/> 
				
      </StackPanel> 
		
   </DataTemplate> 
</UserControl.Resources>
 
<Grid> 
   <ListBox ItemsSource = "{Binding Students}"/> 
</Grid>

В DataTemplate очень важно расширение разметки x: Type, которое похоже на тип оператора в XAML. Итак, в основном нам нужно указать на тип данных Student, который находится в пространстве имен MVVMDemo.Model. Ниже приведен обновленный полный файл XAML.

<UserControl x:Class="MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid>
      <ListBox ItemsSource = "{Binding Students}"/> 
   </Grid> 
	
</UserControl>

Когда вы снова запустите это приложение, вы все равно получите тот же рендеринг шаблона «Студенты с данными», потому что он автоматически отображает тип визуализируемого объекта, найдя соответствующий DataTemplate.

Шаблоны данных

Мы рекомендуем вам выполнить приведенный выше пример пошаговым методом для лучшего понимания.

MVVM — View / ViewModel Communication

В этой главе мы узнаем, как добавить интерактивность в приложения MVVM и как правильно вызывать логику. Вы также увидите, что все это достигается путем поддержания слабой связи и хорошей структуризации, которая является сердцем шаблона MVVM. Чтобы понять все это, сначала давайте узнаем о командах.

View / ViewModel Связь с помощью команд

Шаблон команд хорошо документирован и часто использует шаблон проектирования в течение нескольких десятилетий. В этом паттерне есть два главных актера, призыватель и получатель.

View and ViewModel Communication

Чешуи

  • Invoker — это фрагмент кода, который может выполнять некоторую императивную логику.

  • Как правило, это элемент пользовательского интерфейса, с которым пользователь взаимодействует в контексте инфраструктуры пользовательского интерфейса.

  • Это может быть просто еще один кусок логического кода где-то еще в приложении.

Invoker — это фрагмент кода, который может выполнять некоторую императивную логику.

Как правило, это элемент пользовательского интерфейса, с которым пользователь взаимодействует в контексте инфраструктуры пользовательского интерфейса.

Это может быть просто еще один кусок логического кода где-то еще в приложении.

Получатель

  • Приемник — это логика, предназначенная для выполнения при срабатывании инициатора.

  • В контексте MVVM получатель — это обычно метод в вашей ViewModel, который необходимо вызвать.

Приемник — это логика, предназначенная для выполнения при срабатывании инициатора.

В контексте MVVM получатель — это обычно метод в вашей ViewModel, который необходимо вызвать.

Между этими двумя уровнями имеется слой препятствий, который подразумевает, что инициатор и получатель не должны явно знать друг о друге. Обычно это представляется как абстракция интерфейса, доступная для вызывающего, и конкретная реализация этого интерфейса способна вызвать получателя.

Давайте рассмотрим простой пример, в котором вы изучите команды и как их использовать для связи между View и ViewModel. В этой главе мы продолжим с тем же примером из предыдущей главы.

В файле StudentView.xaml у нас есть ListBox, который подключает данные ученика из ViewModel. Теперь давайте добавим кнопку для удаления студента из ListBox.

Важно то, что работать с командами для кнопки очень легко, потому что у них есть свойство команды для подключения к ICommand.

Таким образом, мы можем предоставить свойство нашей ViewModel, которое имеет ICommand и связывается с ним из свойства команды кнопки, как показано в следующем коде.

<Button Content = "Delete" 
   Command = "{Binding DeleteCommand}" 
   HorizontalAlignment = "Left" 
   VerticalAlignment = "Top" 
   Width = "75" />

Давайте добавим новый класс в ваш проект, который будет реализовывать интерфейс ICommand. Ниже приведена реализация интерфейса ICommand.

using System; 
using System.Windows.Input;

namespace MVVMDemo { 

   public class MyICommand : ICommand { 
      Action _TargetExecuteMethod; 
      Func<bool> _TargetCanExecuteMethod;
		
      public MyICommand(Action executeMethod) {
         _TargetExecuteMethod = executeMethod; 
      }
		
      public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){ 
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }
		
      public void RaiseCanExecuteChanged() { 
         CanExecuteChanged(this, EventArgs.Empty); 
      }
		
      bool ICommand.CanExecute(object parameter) { 
		
         if (_TargetCanExecuteMethod != null) { 
            return _TargetCanExecuteMethod(); 
         } 
			
         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(); 
         } 
      } 
   } 
}

Как вы можете видеть, это простая делегирующая реализация ICommand, где у нас есть два делегата, один для executeMethod и один для canExecuteMethod, который можно передать при создании.

В вышеприведенной реализации есть два перегруженных конструктора, один только для executeMethod и один для обоих executeMethod, и я могу canExecuteMethod.

Давайте добавим свойство типа MyICommand в класс модели StudentView. Теперь нам нужно создать экземпляр в StudentViewModel. Мы будем использовать перегруженный конструктор MyICommand, который принимает два параметра.

public MyICommand DeleteCommand { get; set;} 

public StudentViewModel() { 
   LoadStudents(); 
   DeleteCommand = new MyICommand(OnDelete, CanDelete); 
}

Теперь добавьте реализацию методов OnDelete и CanDelete.

private void OnDelete() { 
   Students.Remove(SelectedStudent); 
}

private bool CanDelete() { 
   return SelectedStudent != null; 
}

Нам также нужно добавить новый SelectedStudent, чтобы пользователь мог удалить выбранный элемент из ListBox.

private Student _selectedStudent;
 
public Student SelectedStudent { 
   get { 
      return _selectedStudent; 
   } 
	
   set { 
      _selectedStudent = value;
      DeleteCommand.RaiseCanExecuteChanged(); 
   } 
}

Ниже приводится полная реализация класса ViewModel.

using MVVMDemo.Model; 

using System.Collections.ObjectModel; 
using System.Windows.Input; 
using System;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public MyICommand DeleteCommand { get; set;} 
		
      public StudentViewModel() { 
         LoadStudents(); 
         DeleteCommand = new MyICommand(OnDelete, CanDelete); 
      }
		
      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }
		
      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();
			
         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" }); 
			
         Students = students; 
      }
		
      private Student _selectedStudent; 
		
      public Student SelectedStudent { 
         get {
            return _selectedStudent; 
         } 
			
         set { 
            _selectedStudent = value;
            DeleteCommand.RaiseCanExecuteChanged(); 
         } 
      }
		
      private void OnDelete() { 
         Students.Remove(SelectedStudent); 
      }
		
      private bool CanDelete() { 
         return SelectedStudent != null; 
      }
   } 
}

В StudentView.xaml нам нужно добавить свойство SelectedItem в ListBox, которое будет привязано к свойству SelectStudent.

<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>

Ниже приведен полный файл xaml.

<UserControl x:Class = "MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d"
   d:DesignHeight = "300" d:DesignWidth = "300">
	
   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
			
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
					
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>
	
   <Grid> 
      <StackPanel Orientation = "Horizontal"> 
         <ListBox ItemsSource = "{Binding Students}" 
            SelectedItem = "{Binding SelectedStudent}"/> 
				
         <Button Content = "Delete" 
            Command = "{Binding DeleteCommand}"
            HorizontalAlignment = "Left" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
      </StackPanel> 
   </Grid>
	
</UserControl>

Когда приведенный выше код скомпилирован и выполнен, вы увидите следующее окно.

View and ViewModel Communication MainWindow1

Вы можете видеть, что кнопка удаления отключена. Он будет включен при выборе любого элемента.

View и ViewModel Связь MainWindow2

Когда вы выбираете любой элемент и нажимаете удалить. Вы увидите, что выбранный список элементов удален, а кнопка удаления снова отключена.

View и ViewModel Связь MainWindow3

Мы рекомендуем выполнить вышеприведенный пример пошагово для лучшего понимания.

MVVM — Иерархии и навигация

При создании приложений MVVM вы обычно разбиваете сложные экраны информации на набор родительских и дочерних представлений, где дочерние представления содержатся в родительских представлениях на панелях или элементах управления контейнера и сами формируют иерархию использования.

  • После декомпозиции сложных представлений это не означает, что каждый фрагмент дочернего контента, который вы разделяете в свой собственный файл XAML, обязательно должен быть представлением MVVM.

  • Кусок контента просто обеспечивает структуру для визуализации чего-либо на экране и не поддерживает ввод или манипулирование пользователем для этого контента.

  • Возможно, он не нуждается в отдельной ViewModel, но это может быть просто фрагмент XAML, который визуализируется на основе свойств, предоставляемых родительской ViewModel.

  • Наконец, если у вас есть иерархия Views и ViewModel, родительский ViewModel может стать концентратором для связи, так что каждый дочерний ViewModel может оставаться отделенным от других дочерних ViewModels и от их родителя, насколько это возможно.

После декомпозиции сложных представлений это не означает, что каждый фрагмент дочернего контента, который вы разделяете в свой собственный файл XAML, обязательно должен быть представлением MVVM.

Кусок контента просто обеспечивает структуру для визуализации чего-либо на экране и не поддерживает ввод или манипулирование пользователем для этого контента.

Возможно, он не нуждается в отдельной ViewModel, но это может быть просто фрагмент XAML, который визуализируется на основе свойств, предоставляемых родительской ViewModel.

Наконец, если у вас есть иерархия Views и ViewModel, родительский ViewModel может стать концентратором для связи, так что каждый дочерний ViewModel может оставаться отделенным от других дочерних ViewModels и от их родителя, насколько это возможно.

MVVM Иерархии и навигация

Давайте рассмотрим пример, в котором мы определим простую иерархию между различными представлениями. Создайте новый проект приложения WPF MVVMHierarchiesDemo

Приложение WPF

Шаг 1 — Добавьте три папки (Model, ViewModel и Views) в ваш проект.

Приложение WPF Шаг 1

Шаг 2. Добавьте классы Customer и Order в папку Model, CustomerListView и OrderView в папку Views, а также CustomerListViewModel и OrderViewModel в папку ViewModel, как показано на следующем рисунке.

Приложение WPF Step2

Шаг 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. Когда приведенный выше код скомпилирован и выполнен, вы увидите следующий вывод.

Иерархии и навигация MainWindow1

Как вы можете видеть, мы добавили только две кнопки и CurrentViewModel в наше главное окно. Если вы нажмете любую кнопку, она перейдет к этому конкретному представлению. Давайте нажмем кнопку «Клиенты», и вы увидите, что отображается CustomerListView.

Иерархии и навигация MainWindow2

Мы рекомендуем выполнить вышеприведенный пример пошагово для лучшего понимания.

MVVM — валидации

В этой главе мы узнаем о проверках. Мы также рассмотрим простой способ проверки с использованием уже существующих привязок WPF, но с привязкой к компонентам MVVM.

Валидация в MVVM

  • Когда ваше приложение начинает принимать ввод данных от конечных пользователей, вы должны рассмотреть возможность проверки этого ввода.

  • Убедитесь, что он соответствует вашим общим требованиям.

  • WPF имеет несколько отличных сборок и функций в системе привязки для проверки ввода, и вы все равно можете использовать все эти функции при работе с MVVM.

  • Имейте в виду, что логика, которая поддерживает вашу проверку и определяет, какие правила существуют для каких свойств, должна быть частью модели или ViewModel, а не самого представления.

Когда ваше приложение начинает принимать ввод данных от конечных пользователей, вы должны рассмотреть возможность проверки этого ввода.

Убедитесь, что он соответствует вашим общим требованиям.

WPF имеет несколько отличных сборок и функций в системе привязки для проверки ввода, и вы все равно можете использовать все эти функции при работе с MVVM.

Имейте в виду, что логика, которая поддерживает вашу проверку и определяет, какие правила существуют для каких свойств, должна быть частью модели или ViewModel, а не самого представления.

Вы по-прежнему можете использовать все способы выражения проверки, которые поддерживаются привязкой данных WPF, включая:

  • Выбрасывание исключений для свойства установлено.
  • Реализация интерфейса IDataErrorInfo.
  • Реализация INotifyDataErrorInfo.
  • Используйте правила проверки WPF.

В общем, рекомендуется INotifyDataErrorInfo, который был представлен в WPF .net 4.5 и поддерживает запрос объекта на наличие ошибок, связанных со свойствами, а также устраняет пару недостатков во всех других параметрах. В частности, он позволяет асинхронную проверку. Это позволяет свойствам иметь более одной ошибки, связанной с ними.

Добавление проверки

Давайте рассмотрим пример, в котором мы добавим поддержку проверки в наше представление ввода, и в больших приложениях вам, вероятно, понадобится несколько мест в вашем приложении. Иногда в Views, иногда в ViewModels, а иногда в этих вспомогательных объектах есть обертки вокруг объектов модели.

Хорошей практикой является помещение поддержки валидации в общий базовый класс, который затем можно наследовать из различных сценариев.

Базовый класс будет поддерживать INotifyDataErrorInfo, чтобы эта проверка запускалась при изменении свойств.

Создать добавить новый класс с именем ValidatableBindableBase. Поскольку у нас уже есть базовый класс для обработки изменения свойства, давайте выведем из него базовый класс, а также реализуем интерфейс INotifyDataErrorInfo.

Ниже приведена реализация класса ValidatableBindableBase.

using System; 
using System.Collections.Generic; 
using System.ComponentModel; 

//using System.ComponentModel.DataAnnotations; 
using System.Linq; 
using System.Runtime.CompilerServices; 
using System.Text;
using System.Threading.Tasks; 
using System.Windows.Controls;

namespace MVVMHierarchiesDemo { 

   public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo { 
      private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();

      public event EventHandler<DataErrorsChangedEventArgs> 
         ErrorsChanged = delegate { };

      public System.Collections.IEnumerable GetErrors(string propertyName) {
		
         if (_errors.ContainsKey(propertyName)) 
            return _errors[propertyName]; 
         else 
            return null; 
      }
      
      public bool HasErrors { 
         get { return _errors.Count > 0; } 
      }
		
      protected override void SetProperty<T>(ref T member, T val, 
         [CallerMemberName] string propertyName = null) {
		
         base.SetProperty<T>(ref member, val, propertyName);
         ValidateProperty(propertyName, val);
      }
		
      private void ValidateProperty<T>(string propertyName, T value) {
         var results = new List<ValidationResult>();
			
         //ValidationContext context = new ValidationContext(this); 
         //context.MemberName = propertyName;
         //Validator.TryValidateProperty(value, context, results);

         if (results.Any()) {
            //_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList(); 
         } else { 
            _errors.Remove(propertyName); 
         }
			
         ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); 
      } 
   } 
}

Теперь добавьте AddEditCustomerView и AddEditCustomerViewModel в соответствующие папки. Ниже приведен код AddEditCustomerView.xaml.

<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView"
   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> 
      <Grid.RowDefinitions> 
         <RowDefinition Height = "Auto" /> 
         <RowDefinition Height = "Auto" />
      </Grid.RowDefinitions>
		
      <Grid x:Name = "grid1" 
         HorizontalAlignment = "Left" 
         DataContext = "{Binding Customer}" 
         Margin = "10,10,0,0" 
         VerticalAlignment = "Top">
			
         <Grid.ColumnDefinitions> 
            <ColumnDefinition Width = "Auto" /> 
            <ColumnDefinition Width = "Auto" /> 
         </Grid.ColumnDefinitions>
		
         <Grid.RowDefinitions> 
            <RowDefinition Height = "Auto" /> 
            <RowDefinition Height = "Auto" /> 
            <RowDefinition Height = "Auto" /> 
            <RowDefinition Height = "Auto" /> 
         </Grid.RowDefinitions>
		
         <Label Content = "First Name:" 
            Grid.Column = "0" 
            HorizontalAlignment = "Left" 
            Margin = "3" 
            Grid.Row = "0" 
            VerticalAlignment = "Center" />
			
         <TextBox x:Name = "firstNameTextBox" 
            Grid.Column = "1" 
            HorizontalAlignment = "Left" 
            Height = "23" 
            Margin = "3" 
            Grid.Row = "0" 
            Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}"
            VerticalAlignment = "Center" 
            Width = "120" />
			
         <Label Content = "Last Name:" 
            Grid.Column = "0" 
            HorizontalAlignment = "Left" 
            Margin = "3" 
            Grid.Row = "1" 
            VerticalAlignment = "Center" /> 
			
         <TextBox x:Name = "lastNameTextBox"
            Grid.Column = "1" 
            HorizontalAlignment = "Left" 
            Height = "23" 
            Margin = "3" 
            Grid.Row = "1" 
            Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}"
            VerticalAlignment = "Center" 
            Width = "120" />
			
         <Label Content = "Email:" 
            Grid.Column = "0" 
            HorizontalAlignment = "Left" 
            Margin = "3" 
            Grid.Row = "2" 
            VerticalAlignment = "Center" />
			
         <TextBox x:Name = "emailTextBox" 
            Grid.Column = "1" 
            HorizontalAlignment = "Left" 
            Height = "23" 
            Margin = "3" 
            Grid.Row = "2" 
            Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}"
            VerticalAlignment = "Center" 
            Width = "120" />
			
         <Label Content = "Phone:" 
            Grid.Column = "0" 
            HorizontalAlignment = "Left" 
            Margin = "3" 
            Grid.Row = "3" 
            VerticalAlignment = "Center" />
			
         <TextBox x:Name = "phoneTextBox" 
            Grid.Column = "1" 
            HorizontalAlignment = "Left" 
            Height = "23" 
            Margin = "3" 
            Grid.Row = "3" 
            Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}"
            VerticalAlignment = "Center" 
            Width = "120" />
			
      </Grid> 

      <Grid Grid.Row = "1"> 
         <Button Content = "Save" 
            Command = "{Binding SaveCommand}" 
            HorizontalAlignment = "Left" 
            Margin = "25,5,0,0" 
            VerticalAlignment = "Top" 
            Width = "75" />
		
         <Button Content = "Add" 
            Command = "{Binding SaveCommand}" 
            HorizontalAlignment = "Left" 
            Margin = "25,5,0,0" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
		
         <Button Content = "Cancel" 
            Command = "{Binding CancelCommand}" 
            HorizontalAlignment = "Left" 
            Margin = "150,5,0,0" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
      </Grid>
		
   </Grid> 
	
</UserControl>

Ниже приведена реализация AddEditCustomerViewModel.

using MVVMHierarchiesDemo.Model;

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo.ViewModel { 

   class AddEditCustomerViewModel : BindableBase { 
	
      public AddEditCustomerViewModel() {
         CancelCommand = new MyIcommand(OnCancel); 
         SaveCommand = new MyIcommand(OnSave, CanSave); 
      } 
		
      private bool _EditMode; 
		
      public bool EditMode { 
         get { return _EditMode; } 
         set { SetProperty(ref _EditMode, value);} 
      }
		
      private SimpleEditableCustomer _Customer;
		
      public SimpleEditableCustomer Customer { 
         get { return _Customer; } 
         set { SetProperty(ref _Customer, value);} 
      }

      private Customer _editingCustomer = null;
		
      public void SetCustomer(Customer cust) {
         _editingCustomer = cust; 
			
         if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged; 
         Customer = new SimpleEditableCustomer();
         Customer.ErrorsChanged += RaiseCanExecuteChanged;
         CopyCustomer(cust, Customer); 
      }
		
      private void RaiseCanExecuteChanged(object sender, EventArgs e) { 
         SaveCommand.RaiseCanExecuteChanged(); 
      }

      public MyIcommand CancelCommand { get; private set; }
      public MyIcommand SaveCommand { get; private set; }

      public event Action Done = delegate { };
		
      private void OnCancel() { 
         Done(); 
      }

      private async void OnSave() { 
         Done(); 
      }
		
      private bool CanSave() { 
         return !Customer.HasErrors; 
      }  
   } 
}

Ниже приведена реализация класса SimpleEditableCustomer.

using System;
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo.Model { 

   public class SimpleEditableCustomer : ValidatableBindableBase { 
      private Guid _id; 
		
      public Guid Id { 
         get { return _id; } 
         set { SetProperty(ref _id, value); } 
      }
		
      private string _firstName; 
      [Required]
		
      public string FirstName { 
         get { return _firstName; } 
         set { SetProperty(ref _firstName, value); } 
      }
		
      private string _lastName; 
      [Required] 
		
      public string LastName {  
         get { return _lastName; } 
         set { SetProperty(ref _lastName, value); } 
      }
		
      private string _email; 
      [EmailAddress] 
		
      public string Email {
         get { return _email; } 
         set { SetProperty(ref _email, value); } 
      }
		
      private string _phone; 
      [Phone] 
		
      public string Phone { 
         get { return _phone; } 
         set { SetProperty(ref _phone, value); } 
      } 
   } 
}

Когда приведенный выше код скомпилирован и выполнен, вы увидите следующее окно.

Проверки MVVM MainWindow1

Когда вы нажмете кнопку «Добавить клиента», вы увидите следующий вид. Когда пользователь оставляет любое поле пустым, оно подсвечивается, а кнопка сохранения становится недоступной.

MVVM Проверки MainWindow2

MVVM — Внедрение зависимостей

В этой главе мы кратко обсудим внедрение зависимостей. Мы уже рассмотрели привязку данных, отделяющую виды Views и ViewModel друг от друга, что позволяет им общаться, не зная явно, что происходит на другом конце взаимодействия.

Теперь нам нужно нечто подобное, чтобы отделить нашу ViewModel от клиентских сервисов.

В первые дни объектно-ориентированного программирования разработчики сталкивались с проблемой создания и извлечения экземпляров классов в приложениях. Различные решения были предложены для этой проблемы.

За последние несколько лет внедрение зависимостей и инверсия управления (IoC) приобрели популярность среди разработчиков и имеют приоритет над некоторыми более старыми решениями, такими как шаблон Singleton.

Инъекция зависимостей / IoC-контейнеры

IoC и внедрение зависимостей — это два тесно связанных между собой шаблона проектирования, а контейнер — это, по сути, кусок кода инфраструктуры, который выполняет оба эти шаблона за вас.

  • Шаблон IoC предназначен для делегирования ответственности за построение, а шаблон внедрения зависимостей — для предоставления зависимостей объекту, который уже был создан.

  • Оба они могут рассматриваться как двухэтапный подход к конструированию. Когда вы используете контейнер, контейнер принимает на себя следующие обязанности:

    • Он конструирует объект, когда его просят.
    • Контейнер определит, от чего зависит этот объект.
    • Построение этих зависимостей.
    • Инъекция их в строящийся объект.
    • Рекурсивно делаю процесс.

Шаблон IoC предназначен для делегирования ответственности за построение, а шаблон внедрения зависимостей — для предоставления зависимостей объекту, который уже был создан.

Оба они могут рассматриваться как двухэтапный подход к конструированию. Когда вы используете контейнер, контейнер принимает на себя следующие обязанности:

Давайте посмотрим, как мы можем использовать внедрение зависимостей, чтобы разорвать связь между ViewModels и клиентскими сервисами. Мы свяжем форму AddEditCustomerViewModel обработки сохранения с помощью внедрения зависимостей, связанного с этим.

Для начала нам нужно создать новый интерфейс в нашем проекте в папке Services. Если в вашем проекте нет папки services, сначала создайте ее и добавьте следующий интерфейс в папку Services.

using MVVMHierarchiesDemo.Model; 

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo.Services { 

   public interface ICustomersRepository { 
      Task<List<Customer>> GetCustomersAsync(); 
      Task<Customer> GetCustomerAsync(Guid id); 
      Task<Customer> AddCustomerAsync(Customer customer); 
      Task<Customer> UpdateCustomerAsync(Customer customer); 
      Task DeleteCustomerAsync(Guid customerId); 
   } 
}

Ниже приводится реализация ICustomersRepository.

using MVVMHierarchiesDemo.Model; 

using System; 
using System.Collections.Generic; 
using System.Linq; using System.Text; 
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo.Services { 

   public class CustomersRepository : ICustomersRepository {
      ZzaDbContext _context = new ZzaDbContext();

      public Task<List<Customer>> GetCustomersAsync() { 
         return _context.Customers.ToListAsync(); 
      }

      public Task<Customer> GetCustomerAsync(Guid id) { 
         return _context.Customers.FirstOrDefaultAsync(c => c.Id == id); 
      }
		
      public async Task<Customer> AddCustomerAsync(Customer customer){ 
         _context.Customers.Add(customer); 
         await _context.SaveChangesAsync(); 
         return customer;
      }

      public async Task<Customer> UpdateCustomerAsync(Customer customer) {
		
         if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) { 
            _context.Customers.Attach(customer); 
         } 
			
         _context.Entry(customer).State = EntityState.Modified;
         await _context.SaveChangesAsync(); 
         return customer;
			
      }

      public async Task DeleteCustomerAsync(Guid customerId) {
         var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId); 
			
         if (customer != null) {
            _context.Customers.Remove(customer); 
         }
			
         await _context.SaveChangesAsync(); 
      } 
   } 
}

Простой способ сделать обработку Save — это добавить новый экземпляр ICustomersRepository в AddEditCustomerViewModel и перегрузить конструкторы AddEditCustomerViewModel и CustomerListViewModel.

private ICustomersRepository _repo; 

public AddEditCustomerViewModel(ICustomersRepository repo) { 
   _repo = repo; 
   CancelCommand = new MyIcommand(OnCancel);
   SaveCommand = new MyIcommand(OnSave, CanSave); 
}

Обновите метод OnSave, как показано в следующем коде.

private async void OnSave() { 
   UpdateCustomer(Customer, _editingCustomer); 
	
   if (EditMode) 
      await _repo.UpdateCustomerAsync(_editingCustomer); 
   else 
      await _repo.AddCustomerAsync(_editingCustomer); 
   Done(); 
} 

private void UpdateCustomer(SimpleEditableCustomer source, Customer target) { 
   target.FirstName = source.FirstName; 
   target.LastName = source.LastName; 
   target.Phone = source.Phone; 
   target.Email = source.Email; 
}

Ниже приведен полный AddEditCustomerViewModel.

using MVVMHierarchiesDemo.Model; 
using MVVMHierarchiesDemo.Services; 

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text;
using System.Threading.Tasks;

namespace MVVMHierarchiesDemo.ViewModel { 

   class AddEditCustomerViewModel : BindableBase { 
      private ICustomersRepository _repo; 
		
      public AddEditCustomerViewModel(ICustomersRepository repo) { 
         _repo = repo;
         CancelCommand = new MyIcommand(OnCancel); 
         SaveCommand = new MyIcommand(OnSave, CanSave); 
      } 
		
      private bool _EditMode; 
		
      public bool EditMode { 
         get { return _EditMode; } 
         set { SetProperty(ref _EditMode, value); } 
      }

      private SimpleEditableCustomer _Customer; 
		
      public SimpleEditableCustomer Customer { 
         get { return _Customer; } 
         set { SetProperty(ref _Customer, value); } 
      }
		
      private Customer _editingCustomer = null;

      public void SetCustomer(Customer cust) { 
         _editingCustomer = cust; 
			
         if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged; 
         Customer = new SimpleEditableCustomer();
         Customer.ErrorsChanged += RaiseCanExecuteChanged;
         CopyCustomer(cust, Customer); 
      }

      private void RaiseCanExecuteChanged(object sender, EventArgs e) { 
         SaveCommand.RaiseCanExecuteChanged(); 
      }

      public MyIcommand CancelCommand { get; private set; } 
      public MyIcommand SaveCommand { get; private set; }

      public event Action Done = delegate { };
		
      private void OnCancel() { 
         Done(); 
      }

      private async void OnSave() { 
         UpdateCustomer(Customer, _editingCustomer); 
			
         if (EditMode) 
            await _repo.UpdateCustomerAsync(_editingCustomer); 
         else 
            await _repo.AddCustomerAsync(_editingCustomer); 
         Done(); 
      }

      private void UpdateCustomer(SimpleEditableCustomer source, Customer target) { 
         target.FirstName = source.FirstName; 
         target.LastName = source.LastName; 
         target.Phone = source.Phone; 
         target.Email = source.Email; 
      }

      private bool CanSave() { 
         return !Customer.HasErrors; 
      }
		
      private void CopyCustomer(Customer source, SimpleEditableCustomer target) { 
         target.Id = source.Id; 
			
         if (EditMode) { 
            target.FirstName = source.FirstName; 
            target.LastName = source.LastName; 
            target.Phone = source.Phone; 
            target.Email = source.Email; 
         }
      } 
   } 
}

Когда приведенный выше код скомпилирован и выполнен, вы увидите тот же вывод, но теперь ViewModels более свободно отсоединены.

Инъекция зависимостей MVVM MainWindow1

Когда вы нажмете кнопку «Добавить клиента», вы увидите следующий вид. Когда пользователь оставляет любое поле пустым, оно подсвечивается, а кнопка сохранения становится недоступной.

Инъекция зависимостей MVVM MainWindow2

MVVM — События

Событие — это программная конструкция, которая реагирует на изменение состояния, уведомляя любые конечные точки, которые зарегистрированы для уведомления. В первую очередь, события используются для информирования пользователя о вводе через мышь и клавиатуру, но их полезность не ограничивается этим. Всякий раз, когда обнаруживается изменение состояния, возможно, когда объект был загружен или инициализирован, может быть инициировано событие, чтобы предупредить любые заинтересованные третьи стороны.

  • В приложении WPF, использующем шаблон проектирования MVVM (Model-View-ViewModel), модель представления является компонентом, отвечающим за обработку логики и состояния представления приложения.

  • Файл с выделенным кодом представления не должен содержать код для обработки событий, возникающих из какого-либо элемента пользовательского интерфейса (UI), такого как Button или ComboBox, и не должен содержать какой-либо предметной логики.

  • В идеале, программный код представления содержит только конструктор, который вызывает метод InitializeComponent, и, возможно, некоторый дополнительный код для управления или взаимодействия со слоем представления, который трудно или неэффективно выразить в XAML, например, сложные анимации.

В приложении WPF, использующем шаблон проектирования MVVM (Model-View-ViewModel), модель представления является компонентом, отвечающим за обработку логики и состояния представления приложения.

Файл с выделенным кодом представления не должен содержать код для обработки событий, возникающих из какого-либо элемента пользовательского интерфейса (UI), такого как Button или ComboBox, и не должен содержать какой-либо предметной логики.

В идеале, программный код представления содержит только конструктор, который вызывает метод InitializeComponent, и, возможно, некоторый дополнительный код для управления или взаимодействия со слоем представления, который трудно или неэффективно выразить в XAML, например, сложные анимации.

Давайте рассмотрим простой пример событий нажатия кнопки в нашем приложении. Ниже приведен код XAML файла MainWindow.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>

Вы можете видеть, что свойство Click кнопки не используется в вышеупомянутом XAML-файле, но свойства Command и CommandParameter используются для загрузки различных представлений при нажатии кнопки. Теперь вам нужно определить реализацию команд в файле MainWindowViewModel.cs, но не в файле View. Ниже приводится полная реализация 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. Когда приведенный выше код скомпилирован и выполнен, вы увидите следующий вывод.

События MVVM MainWindow1

Как вы можете видеть, мы добавили только две кнопки и CurrentViewModel в наше главное окно. Теперь, если вы нажмете любую кнопку, он перейдет к этому конкретному представлению. Давайте нажмем кнопку «Клиенты», и вы увидите, что отображается CustomerListView.

События MVVM MainWindow2

Мы рекомендуем вам выполнить приведенный выше пример пошаговым методом для лучшего понимания.

MVVM — модульное тестирование

Идея модульного тестирования состоит в том, чтобы взять отдельные куски кода (модули) и написать методы тестирования, которые используют код ожидаемым образом, а затем проверить, чтобы получить ожидаемые результаты.

  • Будучи самим кодом, модульные тесты составляются так же, как и остальная часть проекта.

  • Они также выполняются программным обеспечением, запускающим тестирование, которое может ускорять каждый тест, эффективно поднимая или опуская большие пальцы, чтобы указать, прошел ли тест или нет, соответственно.

Будучи самим кодом, модульные тесты составляются так же, как и остальная часть проекта.

Они также выполняются программным обеспечением, запускающим тестирование, которое может ускорять каждый тест, эффективно поднимая или опуская большие пальцы, чтобы указать, прошел ли тест или нет, соответственно.

Давайте посмотрим на пример, созданный ранее. Ниже приводится реализация модели студента.

using System.ComponentModel;

namespace MVVMDemo.Model {
 
   public class StudentModel {}
	
   public class Student : INotifyPropertyChanged { 
      private string firstName; 
      private string lastName;

      public string FirstName { 
         get { return firstName; }
			
         set { 
            if (firstName != value) { 
               firstName = value; 
               RaisePropertyChanged("FirstName");
               RaisePropertyChanged("FullName"); 
            } 
         }
      }

      public string LastName { 
         get { return lastName; } 
			
         set { 
            if (lastName != value) { 
               lastName = value; 
               RaisePropertyChanged("LastName");
               RaisePropertyChanged("FullName");
            } 
         } 
      }

      public string FullName { 
         get { 
            return firstName + " " + lastName; 
         } 
      }

      public event PropertyChangedEventHandler PropertyChanged;

      private void RaisePropertyChanged(string property) { 
         if (PropertyChanged != null) { 
            PropertyChanged(this, new PropertyChangedEventArgs(property)); 
         } 
      } 
   } 
}

Ниже приводится реализация StudentView.

<UserControl x:Class="MVVMDemo.Views.StudentView" 
   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:MVVMDemo.Views" 
   xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel" 
   xmlns:data = "clr-namespace:MVVMDemo.Model" 
   xmlns:vml = "clr-namespace:MVVMDemo.VML" 
   vml:ViewModelLocator.AutoHookedUpViewModel = "True" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">

   <UserControl.Resources> 
      <DataTemplate DataType = "{x:Type data:Student}"> 
		
         <StackPanel Orientation = "Horizontal"> 
            <TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" 
               Width = "100" Margin = "3 5 3 5"/> 
					
            <TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" 
               Width = "100" Margin = "0 5 3 5"/> 
					
            <TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" 
               Margin = "0 5 3 5"/> 
         </StackPanel> 
			
      </DataTemplate> 
   </UserControl.Resources>

   <Grid> 
      <StackPanel Orientation = "Horizontal"> 
         <ListBox ItemsSource = "{Binding Students}"
            SelectedItem = "{Binding SelectedStudent}"/> 
				
         <Button Content = "Delete" 
            Command = "{Binding DeleteCommand}" 
            HorizontalAlignment = "Left" 
            VerticalAlignment = "Top" 
            Width = "75" /> 
      </StackPanel> 
   </Grid>

</UserControl>

Ниже приведена реализация StudentViewModel.

using MVVMDemo.Model;
 
using System.Collections.ObjectModel; 
using System.Windows.Input; 
using System;

namespace MVVMDemo.ViewModel { 

   public class StudentViewModel { 
	
      public MyICommand DeleteCommand { get; set;}
		
      public StudentViewModel() { 
         LoadStudents(); 
         DeleteCommand = new MyICommand(OnDelete, CanDelete); 
      }

      public ObservableCollection<Student> Students { 
         get; 
         set; 
      }

      public void LoadStudents() { 
         ObservableCollection<Student> students = new ObservableCollection<Student>();

         students.Add(new Student { FirstName = "Mark", LastName = "Allain" }); 
         students.Add(new Student { FirstName = "Allen", LastName = "Brown" }); 
         students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
			
         Students = students; 
      } 
		
      private Student _selectedStudent; 
		
      public Student SelectedStudent { 
         get { 
            return _selectedStudent; 
         } 
			
         set { 
            _selectedStudent = value;
            DeleteCommand.RaiseCanExecuteChanged(); 
         } 
      } 
		
      private void OnDelete() { 
         Students.Remove(SelectedStudent); 
      }

      private bool CanDelete() { 
         return SelectedStudent != null; 
      }
		
      public int GetStudentCount() { 
         return Students.Count; 
      } 
   } 
}

Ниже приведен файл MainWindow.xaml.

<Window x:Class = "MVVMDemo.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:MVVMDemo" 
   xmlns:views = "clr-namespace:MVVMDemo.Views" 
   mc:Ignorable = "d" 
   Title = "MainWindow" Height = "350" Width = "525">

   <Grid> 
      <views:StudentView x:Name = "StudentViewControl"/> 
   </Grid>
 
</Window>

Ниже приводится реализация MyICommand, которая реализует интерфейс ICommand.

using System; 
using System.Windows.Input;

namespace MVVMDemo { 

   public class MyICommand : ICommand { 
      Action _TargetExecuteMethod; 
      Func<bool> _TargetCanExecuteMethod;

      public MyICommand(Action executeMethod) { 
         _TargetExecuteMethod = executeMethod; 
      }

      public MyICommand(Action executeMethod, Func<bool> canExecuteMethod) { 
         _TargetExecuteMethod = executeMethod;
         _TargetCanExecuteMethod = canExecuteMethod; 
      }

      public void RaiseCanExecuteChanged() {
         CanExecuteChanged(this, EventArgs.Empty); 
      }
		
      bool ICommand.CanExecute(object parameter) { 
		
         if (_TargetCanExecuteMethod != null) { 
            return _TargetCanExecuteMethod();
         } 
			
         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(); 
         } 
      } 
   }
}

Когда приведенный выше код компилируется и выполняется, вы увидите следующий вывод в главном окне.

Модульное тестирование MVVM MainWindow

Чтобы написать модульный тест для приведенного выше примера, давайте добавим новый тестовый проект в решение.

Добавить новый проект

Добавьте ссылку на проект, щелкнув правой кнопкой мыши по ссылке.

Добавить новые ссылки

Выберите существующий проект и нажмите Ok.

Справочный менеджер MVVM Test

Давайте теперь добавим простой тест, который проверит количество учеников, как показано в следующем коде.

using System; 

using Microsoft.VisualStudio.TestTools.UnitTesting; 
using MVVMDemo.ViewModel;

namespace MVVMTest { 
   [TestClass] 
	
   public class UnitTest1 { 
      [TestMethod] 
		
      public void TestMethod1() { 
         StudentViewModel sViewModel = new StudentViewModel(); 
         int count = sViewModel.GetStudentCount();
         Assert.IsTrue(count == 3); 
      } 
   } 
}

Чтобы выполнить этот тест, выберите пункт «Тест» → «Выполнить» → «Все тесты».

Выполнить тест MVVM

В проводнике тестов видно, что тест пройден, поскольку в StudentViewModel добавлены три ученика. Измените условие подсчета с 3 на 4, как показано в следующем коде.

using System; 

using Microsoft.VisualStudio.TestTools.UnitTesting; 
using MVVMDemo.ViewModel;

namespace MVVMTest { 
   [TestClass] 
	
   public class UnitTest1 { 
      [TestMethod] public void TestMethod1() {
         StudentViewModel sViewModel = new StudentViewModel(); 
         int count = sViewModel.GetStudentCount();
         Assert.IsTrue(count == 4);
      } 
   } 
}

Когда план тестирования будет выполнен снова, вы увидите, что этот тест не пройден, потому что число студентов не равно 4.

MVVM Test Failed

Мы рекомендуем вам выполнить приведенный выше пример пошаговым методом для лучшего понимания.

МВВМ — Каркасы

В этой главе мы обсудим доступные наборы инструментов или инфраструктуры MVVM. Вы также можете использовать эти фреймворки, чтобы вам не приходилось писать кучу повторяющегося кода для самостоятельной реализации шаблона MVVM. Вот некоторые из самых популярных фреймворков —

  • призма
  • MVVM Light
  • Калиберн Микро

МВВМ Каркасы

призма

Prism предоставляет руководство в виде примеров и документации, которые помогут вам легко проектировать и создавать многофункциональные, гибкие и легко обслуживаемые настольные приложения Windows Presentation Foundation (WPF). Богатые интернет-приложения (RIA), созданные с помощью подключаемого модуля браузера Microsoft Silverlight и приложений Windows.

  • Prism использует шаблоны проектирования, которые воплощают важные принципы архитектурного проектирования, такие как разделение задач и слабая связь.

  • Prism помогает проектировать и создавать приложения с использованием слабосвязанных компонентов, которые могут развиваться независимо, но которые могут быть легко и без проблем интегрированы в общее приложение.

  • Эти типы приложений известны как составные приложения.

Prism использует шаблоны проектирования, которые воплощают важные принципы архитектурного проектирования, такие как разделение задач и слабая связь.

Prism помогает проектировать и создавать приложения с использованием слабосвязанных компонентов, которые могут развиваться независимо, но которые могут быть легко и без проблем интегрированы в общее приложение.

Эти типы приложений известны как составные приложения.

У призмы есть ряд особенностей. Ниже приведены некоторые важные особенности Prism.

MVVM Pattern

Призма имеет поддержку шаблона MVVM. Он имеет класс Bindablebase, аналогичный тому, который был реализован в предыдущих главах.

У него есть гибкий ViewModelLocator, в котором есть соглашения, но он позволяет вам переопределить эти соглашения и декларативно подключить ваши Views и ViewModel в слабосвязанной форме.

модульность

Это способность разбивать ваш код на полностью слабо связанные библиотеки классов по частям и объединять их во время выполнения в единое целое для конечного пользователя, в то время как код остается полностью отделенным.

UI Состав / Регионы

Это возможность подключать представления к контейнерам без представления, которое выполняет подключение, требуя явной ссылки на сам контейнер UI.

навигация

Prism имеет функции навигации, расположенные поверх областей, такие как прямая и обратная навигация, а также стек навигации, который позволяет моделям представления напрямую участвовать в процессе навигации.

команды

У Prism есть команды, поэтому у них есть команда делегата, которая очень похожа на MyICommand, который мы использовали в предыдущих главах, за исключением того, что у него есть некоторая дополнительная надежность, чтобы защитить вас от утечек памяти.

События Pub / Sub

Prism также поддерживает события Pub / Sub. Это слабо связанные события, в которых издатель и подписчик могут иметь разное время жизни и не должны иметь явных ссылок друг на друга, чтобы общаться через события.

MVVM Light

MVVM Light производится Laurent Bugnion и помогает вам отделить ваш View от вашей модели, что позволяет создавать приложения, которые чище, проще в обслуживании и расширении.

  • Он также создает тестируемые приложения и позволяет получить более тонкий слой пользовательского интерфейса (который сложнее проверить автоматически).

  • В этом инструментарии особое внимание уделяется открытию и редактированию пользовательского интерфейса в Blend, включая создание данных времени разработки, чтобы пользователи Blend могли «что-то видеть» при работе с элементами управления данными.

Он также создает тестируемые приложения и позволяет получить более тонкий слой пользовательского интерфейса (который сложнее проверить автоматически).

В этом инструментарии особое внимание уделяется открытию и редактированию пользовательского интерфейса в Blend, включая создание данных времени разработки, чтобы пользователи Blend могли «что-то видеть» при работе с элементами управления данными.

Калиберн Микро

Это еще одна небольшая платформа с открытым исходным кодом, которая помогает вам реализовать шаблон MVVM, а также поддерживает ряд готовых решений.

Caliburn Micro — это небольшая, но мощная платформа, предназначенная для создания приложений на всех платформах XAML.

Благодаря мощной поддержке MVVM и других проверенных шаблонов пользовательского интерфейса, Caliburn Micro позволит вам быстро построить решение, не жертвуя качеством кода или тестируемостью.