WCF RIA Services. Внедряем паттерн Model-View-ViewModel (MVVM). Часть 4

от автора

Вступление

Паттерн Model-View-ViewModel (MVVM) используется для создания слабо связанных приложений на Silverlight и WPF. В данном курсе мы не будем рассматривать основы данного подхода, а просто узнаем как внедрить его в наш проект, который мы создавали на протяжении 3 уроков. Если не внедряться в дебри, то MVVM – это одна из альтернатив, которая является логическим развитием таких паттернов программирования как MVC и MVP и полностью поддерживает привязку данных, команды, и все возможности, которые предоставляют Silverlight и WPF. Модель вида (ViewModel) отвечает за предоставление виду (View) всех необходимых ресурсов. То есть предоставляются все необходимые свойства для того, что б вид мог легко осуществлять привязку данных, создание команд, а в модели вида тем временем присутствует и работает вся необходимая приложению логика. Структурно, устанавливаемый «DataContext» равен экземпляру модели вида, с которым происходит связывание данных (binding).

Основным преимуществом такого подхода является практически полная независимость вида от модели вида, что грубо можно выразить, как независимую разработку каждой части программистом и дизайнером. Так же, приятным следствием такого разделения является достаточная легкость создания модульных текстов (unit test), так как логика абсолютно не связана с UI (интерфейсом пользователя).

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

Шаг 1: Создание модели вида


С самого начало мы организовывали всю логику в сопутствующем файле для страницы (имя_страницы.xaml.cs). Тем самым мы породили сильную связанность логики и представления. А это в большинстве случаев очень плохой тон. Пришло время разграничить эти понятия. Вынесем весь код в модель вида. Создаем новый класс в клиентском проекте с именем «TasksViewModel». Далее, было бы не плохо проанализировать представление и определить, какие свойства нужно будет создать в модели вида. На картинке ниже мы видим форму, которую нужно будет создать. Для начала добавим к двум верхним TextBox кнопку выбора даты, используя DatePickers, удалим из DataGrid неиспользуемый столбцы и слегка подправим оставшиеся для получения более привлекательно и читабельного вида.

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

public class TasksViewModel  {      public DateTime LowerSearchDate { get; set; }      public DateTime UpperSearchDate { get; set; }      public ICommand SearchByDateCommand { get; set; }      public ICommand AddTaskCommand { get; set; }      public ICommand SaveChangesCommand { get; set; }      public IEnumerable<Task> Tasks { get; set; }  } 

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

public class TasksViewModel : INotifyPropertyChanged  {      public event PropertyChangedEventHandler PropertyChanged = delegate { };      DateTime _LowerSearchDate;      public DateTime LowerSearchDate      {          get          {              return _LowerSearchDate;          }          set          {              if (value != _LowerSearchDate)              {                  _LowerSearchDate = value;                  PropertyChanged(this, new PropertyChangedEventArgs("LowerSearchDate"));              }          }      }       DateTime _UpperSearchDate;      public DateTime UpperSearchDate      {          get          {              return _UpperSearchDate;          }          set          {              if (value != _UpperSearchDate)              {                  _UpperSearchDate = value;                  PropertyChanged(this, new PropertyChangedEventArgs("UpperSearchDate "));              }          }      } 

Для возможности использовать свойства ICommand нужно добавить соответствующую реализацию. Будем использовать простейшую реализацию, еще называемую RelayCommand. Но в реальных проектах советую использовать DelegateCommand, который предлагает Prism.

Для создания RelayCommand необходимо добавить новый класс в клиентский проект с именем «RelayCommand»:

namespace TaskManager {     public class RelayCommand<T> : ICommand     {         Action<T> _TargetExecuteMethod;         Func<T, bool> _TargetCanExecuteMethod;          public RelayCommand(Action<T> executeMethod)         {             _TargetExecuteMethod = executeMethod;         }          public RelayCommand(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;         }          public event EventHandler CanExecuteChanged = delegate { };          void ICommand.Execute(object parameter)         {             if (_TargetExecuteMethod != null)             {                 _TargetExecuteMethod((T)parameter);             }         }         #endregion     } } 

Так как модель вида предоставляет все необходимые данные для вида, а DomainContext – отвечает за своевременное обновление данных при их изменении, то напрашивается очевидное решение, что при использовании паттерна MVVM в RIA Services, нужно просто использовать DomainContext внутри модели вида.

TasksDomainContext _Context = new TasksDomainContext();     public TasksViewModel()  {      SearchByDateCommand = new RelayCommand<object>(OnSearchByDate);      AddTaskCommand = new RelayCommand<object>(OnAddTask);      SaveChangesCommand = new RelayCommand<object>(OnSaveChanges);      Tasks = _Context.Tasks;      if (!DesignerProperties.IsInDesignTool)      {          _Context.Load(_Context.GetTasksQuery());      }  } 

В коде выше, мы просто создаем TasksDomainContext внутри модели вида, а в конструкторе инициализируем команды, что б связать их с нужными методами. Свойство Tasks модели вида содержит в себе ссылку на коллекцию сущностей Tasks, которая в свою очередь предоставляется контекстом домена, и которое вызывает события INotifyCollectionChanged для обеспечения актуальной информации в представлении в случае изменения коллекции, которое возникает при вызове метода «load» или «SubmitChanges» и обновляет сущности в фоновом режиме. Обратите внимание на свойство «DesignerProperties.IsInDesignTool» в конструкторе, на которое идет проверка в блоке «If», которое предотвращает вызов метода «Load» из дизайнера, так как это повлечет ошибку.

Следующим шагом будет перенос методов из MainPage.xaml.cs в модель вида. То есть перенос логики. MainPage.xaml.cs очищаем полностью, внутри остается только это:

namespace TasksManager {     public partial class MainPage : UserControl     {         public MainPage()         {             InitializeComponent();         } 

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

private void OnSearchByDate(object param)  {      _Context.Tasks.Clear();      EntityQuery<Task> query = _Context.GetTasksQuery();      LoadOperation<Task> loadOp = _Context.Load(query.Where(t => t.StartDate >= LowerSearchDate && t.StartDate <= UpperSearchDate));  }     private void OnAddTask(object param)  {      // Данный подход не является корректным      // Более подробно про структуру MVVM можно узнать, ознакомившись с Prism 4      AddTaskView popup = new AddTaskView();      popup.DataContext = new Task();      popup.Closed += delegate      {          if (popup.DialogResult == true)          {              Task newTask = popup.DataContext as Task;              if (newTask != null) _Context.Tasks.Add(newTask);          }      };      popup.Show();  }     private void OnSaveChanges(object param)  {      _Context.SubmitChanges();  } 

Теперь добавим всплывающее окно. На клиентском проекте «Добавить» — «Создать элемент» — «Страница Silverlight» с именем «AddTaskView» и следующим содержимым:

<controls:ChildWindow x:Class="TaskManager.AddTaskView"            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"             xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"            Width="361" Height="287"             Title="Add Task" mc:Ignorable="d" xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices" xmlns:my="clr-namespace:TaskManager.Web" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">     <Grid x:Name="LayoutRoot" Margin="2">         <Grid.RowDefinitions>             <RowDefinition />             <RowDefinition Height="Auto" />         </Grid.RowDefinitions>          <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />         <Button x:Name="SaveButton" Content="Save" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1"                 Click="OKButton_Click" />         <Grid DataContext="{Binding}" HorizontalAlignment="Left" Margin="12,12,0,0" Name="grid1" VerticalAlignment="Top" Width="315">             <Grid.ColumnDefinitions>                 <ColumnDefinition Width="Auto" />                 <ColumnDefinition Width="237" />                 <ColumnDefinition Width="4*" />             </Grid.ColumnDefinitions>             <Grid.RowDefinitions>                 <RowDefinition Height="Auto" />                 <RowDefinition Height="Auto" />                 <RowDefinition Height="Auto" />                 <RowDefinition Height="100" />             </Grid.RowDefinitions>             <sdk:Label Content="Description:" Grid.Column="0" Grid.Row="3" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />             <TextBox Grid.Column="1" Grid.Row="3" Height="91" HorizontalAlignment="Left" Margin="3,3,0,6" Name="descriptionTextBox" Text="{Binding Path=Description, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, TargetNullValue=''}" VerticalAlignment="Center" Width="221" />             <sdk:Label Content="End Date:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />             <controls:DatePicker Grid.Column="1" Grid.Row="2" Height="23" HorizontalAlignment="Left" Margin="3,3,0,3" Name="endDateDatePicker" SelectedDate="{Binding Path=EndDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, TargetNullValue=''}" VerticalAlignment="Center" Width="120" />             <sdk:Label Content="Start Date:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />             <controls:DatePicker Grid.Column="1" Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="3,3,0,3" Name="startDateDatePicker" SelectedDate="{Binding Path=StartDate, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, TargetNullValue=''}" VerticalAlignment="Center" Width="120" />             <sdk:Label Content="Task Name:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />             <TextBox Grid.Column="1" Grid.Row="0" Height="23" HorizontalAlignment="Left" Margin="3,3,0,3" Name="taskNameTextBox" Text="{Binding Path=TaskName, Mode=TwoWay, NotifyOnValidationError=true, ValidatesOnExceptions=true, TargetNullValue=''}" VerticalAlignment="Center" Width="221" />         </Grid>     </Grid> </controls:ChildWindow> 

Все. Модель вида на данном этапе полностью готова и функциональна. Перейдем к доработке нашего UI.

Шаг 2: Связываем Вид и Модель вида

Начиная с первой части мы используем DomainDataSource, который был автоматически сгенерирован при «drag and drop». При использовании MVVM необходимо будет избавиться от DomainDataSource, так как его использование в XAML нарушает концепцию разделения MVVM.

Добавляем DataContext. Указываем имя модели вида. И Добавляем связывание на основе созданных свойств:

<UserControl x:Class="TaskManager.MainPage" ...>      <UserControl.DataContext>          <local:TasksViewModel/>      </UserControl.DataContext>          <Grid x:Name="LayoutRoot" Background="White">          <sdk:DataGrid ItemsSource="{Binding Tasks}" .../>          <Button Command="{Binding SearchByDateCommand}" .../>          <Button Command="{Binding AddTaskCommand}" ... />          <Button Command="{Binding SaveChangesCommand}" ... />          <sdk:DatePicker SelectedDate="{Binding LowerSearchDate}" ... />          <sdk:DatePicker SelectedDate="{Binding UpperSearchDate}" ... />      </Grid>  </UserControl> 

Видео для этого урока

Исходники

На Github

ссылка на оригинал статьи http://habrahabr.ru/post/215321/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *