В
последнее время начинаю замечать, что все больше людей интересуется паттерном MVVM при переходе на WPF/Silverlight. Хотя, пожалуй, с Silverlight стараются
связываться все меньше и меньше разработчиков, так как эта технология попросту
умирает, и Microsoft официально заявила
о том, что Silverlight 5 – это последняя версия, которая будет поддерживаться до
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 через ресурсы. Делается это так:
Рассмотрим несколько способов связывания 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