Wednesday, March 19, 2014

Advanced MVVM

В последнее время начинаю замечать, что все больше людей интересуется паттерном MVVM при переходе на WPF/Silverlight. Хотя, пожалуй, с Silverlight стараются связываться все меньше и меньше разработчиков, так как эта технология попросту умирает, и Microsoft официально заявила о том, что Silverlight– это последняя версия, которая будет поддерживаться до 2021 года, и новых больше не будет (пост в официальном блоге). Поэтому актуальной на данный момент пока остается WPF для написания прикладных приложений. А учитывая тот факт, что XAML и MVVM используются не только для прикладных приложений, написанных на WPF, но и для приложений, которые работают на планшетах (Windows Store App-приложения) и на мобильных телефонах (написанные на Windows Phone 7/8). Так что знание паттерна MVVM и умение работать с декларативным языком разметки XAML точно вам пригодится, если вы планируете развивать себя в этом направлении. Поэтому в этой статье мы рассмотрим некоторые нюансы программирования с использованием паттерна MVVM, которые, за моими наблюдениями, вызывают когнитивный диссонанс у начинающих разработчиков при начале более глубокого изучения этот паттерна. Давайте посмотрим, как выглядит классическая реализация паттерна MVVM.
Но реалии таковы, что такая структура на практике обычно переливается в следующую:
И даже в такую:
Это пример связи одного представления с моделью. Часто такая структура присущая композитным окнам, реализации древовидной структуры для отображения и т.д. Пример подобной структуры мне приходилось видеть в окне, которое для отображения комбинировало данные с разных моделей и моделей представлений. В последнее время вы, возможно, слышали о паттерне MVPVM (Model-View-Presenter-ViewModel) – этот паттерн отличается от паттерна MVVM тем, что у нас появился призентер, на который возлагается логика поведения приложения. По сути, это попытка свести воедино паттерн MVC и MVVM. Вот как выглядит этот паттерн:
Картинка взята со статьи Проектировочный шаблон Model-View-Presenter-ViewModel для WPF. В этой статье рассказывается о том, что автор запутался в "мистических" возможностях библиотеки Prism и решил досконально изучить паттерн MVP. Такой подход тоже имеет место для существования, но как по мне, автор мог долгое время работать с паттерном MVP, в связи с чем наблюдались трудности с переключением на паттерн MVVM. Такой подход дает свои преимущества, но наша архитектура начинает разрастаться просто в гигантских масштабах, что не есть хорошо. Хотя если вы умеете нормально разделять архитектуру на слои, проблем у вас не должно возникнуть.
Основная проблема, с которой вы можете столкнуться, состоит в том, что вы не понимаете, как во всем этом не запутаться. Понять, как работает паттерн MVVM, несложно, но как связать все воедино, чтобы самому не утонуть в куче кода, – это действительно проблема, которую пытаться решить архитекторы программного обеспечения и разработчики с сильной базой по построению архитектуры программного обеспечения. Грамотное построение архитектуры решает приведенные выше проблемы, но это на уровень выше, чем просто использование паттерна MVVM.
Вернемся к такой модели, в которой не используется уровень Model. Ниже приведен пример с habrahabr.ru, который идеально подходит для демонстрации.
public class TasksViewModel : INotifyPropertyChanged
{
       public event PropertyChangedEventHandler PropertyChanged = delegate { };
       private DateTime _LowerSearchDate;

       public DateTime LowerSearchDate
       {
             get { return _LowerSearchDate; }
             set
             {
                    if (value != _LowerSearchDate)
                    {
                           _LowerSearchDate = value;
                           PropertyChanged(this, new PropertyChangedEventArgs("LowerSearchDate"));
                    }
             }
       }

       private DateTime _UpperSearchDate;

       public DateTime UpperSearchDate
       {
             get { return _UpperSearchDate; }
             set
             {
                    if (value != _UpperSearchDate)
                    {
                           _UpperSearchDate = value;
                           PropertyChanged(this, new PropertyChangedEventArgs("UpperSearchDate "));
                    }
             }
       }
}
Приведенный выше пример, в котором используется только модель представления (ViewModel), показывает, что без модели данных в некоторых случаях можно неплохо обходиться. Примером, когда нам может потребоваться использование уровня модели (Model), – это получение данных с БД, работа с сервисами и т.д. Но я выбрал это пример с habrahabr не только чтобы показать, как работает паттерн MVVM, по сути, без использования модели как таковой, но и для того чтобы продемонстрировать, как его не стоит использовать.
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();
}
Хоть автор статьи и предупредил, что данный подход является некорректным, но он не рассказал пользователям о причинах, а порекомендовал посмотреть Prism. Я не согласен с таким описанием вещей. Это звучит как: "Ты не знаешь, почему это не работает? Посмотри в MSDN (Google, Yandex список можно продолжать до бесконечности, в зависимости от типа разработки, сути это не меняет)". Некорректность данного подхода – в том, что наша модель представления, которой в данном случае выступает TaskViewModel, знает что-то о другом представлении. При правильном подходе к разработке программного обеспечения на основе паттерна MVVM такого быть не должно. Как же сделать так, чтобы в модели представления не было такой явной логики? Для этого необходимо вынести работу с диалоговыми окнами в отдельный интерфейс, также для связывания лучше использовать один из IoC-контейнеров, так мы избавимся от жёсткой привязки. Ниже приведен пример интерфейса.
/// <summary>
/// Interface responsible for abstracting ViewModels from Views.
/// </summary>
public interface IDialogService
{
       /// <summary>
       /// Gets the registered views.
       /// </summary>
       ReadOnlyCollection<FrameworkElement> Views { get; }

       /// <summary>
       /// Registers a View.
       /// </summary>
       /// <param name="view">The registered View.</param>
       void Register(FrameworkElement view);

       /// <summary>
       /// Unregisters a View.
       /// </summary>
       /// <param name="view">The unregistered View.</param>
       void Unregister(FrameworkElement view);

       /// <summary>
       /// Shows a dialog.
       /// </summary>
       /// <remarks>
       /// The dialog used to represent the ViewModel is retrieved from the registered mappings.
       /// </remarks>
       /// <param name="ownerViewModel">
       /// A ViewModel that represents the owner window of the dialog.
       /// </param>
       /// <param name="viewModel">The ViewModel of the new dialog.</param>
       /// <returns>
       /// A nullable value of type bool that signifies how a window was closed by the user.
       /// </returns>
       bool? ShowDialog(object ownerViewModel, object viewModel);

       /// <summary>
       /// Shows a dialog.
       /// </summary>
       /// <param name="ownerViewModel">
       /// A ViewModel that represents the owner window of the dialog.
       /// </param>
       /// <param name="viewModel">The ViewModel of the new dialog.</param>
       /// <typeparam name="T">The type of the dialog to show.</typeparam>
       /// <returns>
       /// A nullable value of type bool that signifies how a window was closed by the user.
       /// </returns>
       bool? ShowDialog<T>(object ownerViewModel, object viewModel) where T : Window;

       /// <summary>
       /// Shows a message box.
       /// </summary>
       /// <param name="ownerViewModel">
       /// A ViewModel that represents the owner window of the message box.
       /// </param>
       /// <param name="messageBoxText">A string that specifies the text to display.</param>
       /// <param name="caption">A string that specifies the title bar caption to display.</param>
       /// <param name="button">
       /// A MessageBoxButton value that specifies which button or buttons to display.
       /// </param>
       /// <param name="icon">A MessageBoxImage value that specifies the icon to display.</param>
       /// <returns>
       /// A MessageBoxResult value that specifies which message box button is clicked by the user.
       /// </returns>
       MessageBoxResult ShowMessageBox(
             object ownerViewModel,
             string messageBoxText,
             string caption,
             MessageBoxButton button,
             MessageBoxImage icon);

       /// <summary>
       /// Shows the OpenFileDialog.
       /// </summary>
       /// <param name="ownerViewModel">
       /// A ViewModel that represents the owner window of the dialog.
       /// </param>
       /// <param name="openFileDialog">The interface of a open file dialog.</param>
       /// <returns>DialogResult.OK if successful; otherwise DialogResult.Cancel.</returns>
       DialogResult ShowOpenFileDialog(object ownerViewModel, IOpenFileDialog openFileDialog);

       /// <summary>
       /// Shows the FolderBrowserDialog.
       /// </summary>
       /// <param name="ownerViewModel">
       /// A ViewModel that represents the owner window of the dialog.
       /// </param>
       /// <param name="folderBrowserDialog">The interface of a folder browser dialog.</param>
       /// <returns>The DialogResult.OK if successful; otherwise DialogResult.Cancel.</returns>
       DialogResult ShowFolderBrowserDialog(object ownerViewModel,
                                  IFolderBrowserDialog folderBrowserDialog);
}
Пример интерфейса взят с codeproject.com. Там на эту тему написана отдельная статья. Здесь же показан способ исправления этой проблемы. Для призма диалоговое окно можно посмотреть в статье  Implement a confirmation dialog in WPF using MVVM and Prism, либо если вы используете готовые шаблоны для работы с MVVM, как, например MVVM Light Toolkit, то там можно сделать так. Я веду к тому, что написать правильное поведение с диалоговыми окнами в MVVM требует опыта работы с C#, для начинающих понимание событий, отложенного выполнения функции, использование делегатов дается сложно. А умение правильно построить приложение на WPF с использованием паттерна, который мы рассматриваем в данной статье, намного сложнее. 
Рассмотрим несколько способов связывания ViewModel + View. Один из них связан с использованием IoC контейнера с привязкой через интерфейсы. А второй связан с привязкой  ViewModel c View через ресурсы. Делается это так:
<DataTemplate DataType="{x:Type local:MyViewModel}">
    <local:MyView />
</DataTemplate>
Отображать нужное нам представление можно через ContentPresenter. Для того чтобы понять, как работает такое связывание, лучше посмотреть статью по созданию простого визарда на WPF (Create Wizard in WPF).
Продолжим погружение в паттерн MVVM. Сейчас постараюсь рассказать об одной частой ошибке, которую совершают начинающие разработчики, которые только начали изучать данный паттерн. Эта ошибка заключается в путанице с понятием модели (Model) и модели представления (ViewModel). Если вы внимательно посмотрите на самый первый рисунок, то увидите, что обработка действий пользователя осуществляется через команды (ввод текста, нажатие клавиш и т.д.). Так вот, если вы даже создадите класс и назовёте его просто MyModel и будете считать, что этот класс и есть моделью данных, но при этом ваш класс выполняет хоть какие-то действия, кроме указания того, что данные изменились (notification), то вы, по сути, используете ViewModel а не View. Запомнить это очень просто: если есть обработка действий пользователя – это не модель данных.
И последнее, на чем хотелось бы остановится перед завершением статьи, – это немного рассказать о том, как используется данный паттерн. Основная роль, как сказано в начале статьи, уделяется наследованию. Для того чтобы модели данных (Model) уведомить модель представления (ViewModel), что данные изменились, необходимо использовать интерфейс INotifyPropertyChanged для простых данных, а для коллекций нужно использовать INotifyCollectionChanged или использовать готовую реализацию ObservableCollection<T>, которая реализует этот интерфейс. В итоге мы получаем довольно монстрообразное представление. Для примера приведен базовый код без функций, чтобы показать, как происходит расширение функционала.
public class NotifyObject : INotifyPropertyChanged
{
       public event PropertyChangedEventHandler PropertyChanged;

       [NotifyPropertyChangedInvocator]
       protected virtual void OnPropertyChanged(string propertyName)
       {
             PropertyChangedEventHandler handler = PropertyChanged;
             if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
       }
}

//ModelBase
public class ModelBase : NotifyObject {}

public class ViewModelBase : NotifyObject {}


public class TreeViewModelBase : ViewModelBase { }
      

public class ListViewModelBase : ViewModelBase {}

public class MessageViewModelBase : ViewModelBase {}

//Модель представления для дерева
public class RequestTreeViewModel : TreeViewModelBase {}

//Модель представления для ListView
public class RequestListViewModel : TreeViewModelBase { }   
И в таком духе продолжается расширение. Возможно, в других проектах модель построена по-другому. Но в проектах, с которыми приходилось работать мне, архитектура была построена таким образом. Со временем классы с приставками Base становятся все больше и больше. Поэтому нужно внимательно следить за тем, чтобы этот размер не перешел критическую границу. Сильное дробление на подмодели иногда спасает процесс, но иногда и значительно усложняет его. Если архитектура построена правильно, то проблем у вас с этим возникнуть не должно. Проблема заключается в тестировании данного кода. Но это уже отдельная большая тема.

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

No comments:

Post a Comment