Ненавистный INotifyPropertyChanged

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

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

А когда таких свойств прописано с десяток и все это обернуто в регион (#region), то взгляд стекленеет, становится безжизненным и мозг отказывается верить в реальность происходящего. Конечно, самое большое зло от такого подхода – потеря времени на написание и на чтение.

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

К сожалению, при разработке пользовательского интерфейса на WPF такая реализация «в лоб» встречается до сих пор сплошь и рядом. Не далее чем неделю назад вычищал такой код из рабочего проекта. Я бы порадовался за трудолюбие и усидчивость разработчиков пишущих такой код, но я склонен думать, что программист должен быть ленивым. В хорошем смысле. Так что лучше потерять немного времени на настройку и потом быстро-быстро все сделать. Далее я опишу 4 более-менее различных подхода к решению данной проблемы.

Подготовка

Эксперименты будут проводиться на стандартном проекте для настольного WPF приложения. В приложении будет единственная форма с тремя элементами: надпись, строка для ввода текста и кнопка.

Код для формы

Все предельно просто для нужд эксперимента. Модель вида  (ViewModel)  тоже в целях эксперимента будет настолько простой, насколько это возможно. Так что будем тренироваться на таком коде:

Это будет отправной точкой для различных подходов. Теперь поставим перед собой задачу по модификации класса ViewModel, так чтобы по срабатыванию команды на обновление сработала связь элементов (binding) с  пользовательским интерфейсом.

Связывание модели вида и формы сделано в конструкторе формы, опять же для простоты.

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

Решения глубокой старины

Да, на мой взгляд, явная реализация интерфейса INotifyPropertyChanged может претендовать на такое название.

Для того чтобы данные обновлялись необходимо наследовать модель вида от INotifyPropertyChanged и реализовать его. Сложностей в этом подходе быть не должно. Самая простая реализация:

После чего надо модифицировать все свойства класса на явный вызов метода RaisePropertyChanged с передачей имени свойства. Пример:

Таким образом итоговый класс будет выглядеть следующим образом:

Здесь специально приведен полный код класса, для последующей оценки объема кода и его читабельности по сравнению с исходным.

Плюсы:

  • Независимость от сторонних библиотек. Как результат простота в разворачивании и автоматической сборке
  • Скорость компиляции

Минусы:

  • Большая трудоемкость написания и чтения
  • Разрастание кода
  • Хрупкость при исправлении имен свойств
  • Нарушение принципа DRY (Don’t Repeat Yourself)

Очень быстро такой код писать надоедает и выделяется базовый класс с реализацией интерфейса INotifyPropertyChanged. Этим исправляется нарушение принципа DRY, но объем кода не сильно уменьшается.

Итоговый код модели вида:

Плюсы и минусы все те же, за исключением DRY.

MVVM Light

Еще через некоторое время надоедает руками писать базовый класс для реализации INotifyPropertyChanged и поиск быстро выдает известную многим библиотеку MVVM Light.

Конечно же, данная библиотека не только реализует базовый класс для INPC на .Net 4. В поставке идут библиотеки для Silverlight, WP7. Дополнительные упрощающие конструкции для взаимодействия с интерфейсом.

Установка библиотеки производится методом xcopy и не имеет зависимостей. Т.е. подключаете ссылку на библиотеку в свой проект и начинаете работу.

Так же можно установить MVVM Light с помощью NuGet. Команда для установки:

PM> Install-Package MvvmLight

После этого можно приступить к работе. Честно сказать работа с MVVM Light не сильно отличается от работы с базовым классом. Необходимо наследовать модели от ViewModelBase и в свойствах прописывать RaisePropertyChanged(«имя свойства»).

Изменений… нет. Только если вы планируете пользоваться дополнительными классами из библиотеки.

Плюсы:

  • Меньше писать самому. Ровно на базовый класс.
  • Работает с Build-серверами без дополнительных телодвижений.

Минусы:

  • Большая трудоемкость написания и чтения
  • Разрастание кода
  • Хрупкость при исправлении имен свойств
  • Зависимость от сторонних библиотек

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

Хардкорный АOP (PostSharp)

Следующим шагом развития может считаться аспектно-ориентированный подход к решению проблемы. Я считаю, что единственно верный подход. Неважно с помощью чего вы это сделаете, с помощью PostSharp, Castle или Spring. Инфраструктурный код не должен мозолить глаза. К нему должен применяться подход – сделал и забыл.

PostSharp – условно бесплатный фреймворк. Есть как бесплатная community версия, так и расширенная professional. Взять PostSharp можно как с сайта производителя, так и с помощью NuGet. Для установки надо ввести в Package Manager Console

PM> install-package postsharp

После чего вас спросят об интеграции в Visual Studio и попросят перезапустить студию. Сразу после этого можно будет использовать PostSharp, так как NuGet включает сразу все ссылки на нужные библиотеки.

Итак, необходимо создать аспект, реализующий интерфейс INotifyPropertyChanged.

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

Самое главное что итог выглядит так:

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

Плюсы:

  • Сокращение рукописного кода
  • Разграничение функционального и инфраструктурного кода
  • Скорость разработки
  • Читабельность
  • Независимость от сложности реализации метода set.
  • Размер ViewModel
  • Полноценный AOP фреймворк
  • Стойкость к изменениям имен свойств

Минусы:

  • Зависимость от сторонних библиотек
  • Требуется установка для Build-сервера

Kind of Magic

Когда не требуется вся мощь АОР фреймворка можно использовать небольшую библиотеку, от которой проект не будет зависеть после компиляции. Имя ей Kind of Magic. Получить можно как с CodePlex, так и из VS Gallery.

Пару слов о том, как установить Kind of Magic. Вызываем Extension Manager с помощью Tools > Extension Manager…

 

И в появившемся окне переходим на закладку Online (в левой части окна) и, не теряя времени, вбиваем в строку поиска KindOfMagic. После этого система найдет искомый элемент. Жмем на кнопку Install, перезапускаем студию и продолжаем программирование.

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

Рядом с моделью вида создаем новый атрибут MagicAttribute. Именно с таким именем, это важно. Т.е.

После этого, создаем базовый класс, который будет отмечен атрибутом Magic.

Далее модель вида наследуем от созданного базового класса:

В целом получилось так же красиво, так как инфраструктурные вещи спрятаны и свойства остались не тронутыми.

Однако на этом не все, остался самый последний шаг для включения атрибута. Необходимо в меню Project выбрать пункт Enable Kind Of Magic. Только после этого все заработает как надо.

Общий принцип действия данного подхода в том, что сразу после компиляции находятся все классы и потомки реализующие интерфейс INotifyPropertyChanged, находится метод RaisePropertyChanged(string) и во всех классах к которым применен атрибут Magic переписываются свойства таким образом, чтобы они вызывали метод RaisePropertyChanged.

В самом конце получается код идентичный тому, что мы писали руками в самом начале.

При использовании данного подхода есть свои нюансы. Если метод set сложный, то бывает сложно определить в какой момент подставить и вызвать RaisePropertyChanged. В таком случае надо в базовом классе определить следующий метод.

И в сложном set подставлять руками управлять обновлением, вызывая метод Rise.

Для того, чтобы использовать Kind Of Magic c Build-сервером необходимо скопировать бинарные файлы дополнения в какую-либо папку на сервере и обновить файл Microsoft.CSharp.targets указав путь до бинарников. Т.е. если вы скопировали необходимые файлы в папку c:\Soft\KindOfMagic, то в Microsoft.CSharp.targets надо будет вставить строку

Плюсы:

  • Сокращение рукописного кода
  • Разграничение функционального и инфраструктурного кода
  • Скорость разработки
  • Читабельность
  • Размер ViewModel
  • Стойкость к изменениям имен свойств
  • Независимость от сторонних библиотек

Минусы:

  • Требуется установка для Build-сервера
  • При сложных Set требует ручной доводки

Определенным плюсом может выступать то, что Kind of Magic не выглядит как AOP, некоторые пугаются еще.

Заключение

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

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

Исходный код примеров можно скачать с Assembla или архивом.

 

Hard’n’heavy!