[Из песочницы] Правильное оперирование XAML-ресурсами
Привет, Хабр.Около недели назад прочитал статью «Как получить удобный доступ к XAML-ресурсам из Code-Behind» и был неслабо удивлен. Заранее прошу прощения у EBCEu4, автора вышеупомянутой статьи, потому что собираюсь немного раскритиковать изложенный им подход.
Хочу заметить, что статья содержит только рекомендации по правильному использованию ресурсов и не претендует на полноту изложения. Моя статья будет состоять из трёх пунктов. В первом я приведу пример ситуации, когда вышеописанный подход оправдан, во втором — попробовать обьяснить, почему же неправильно тянуть ресурсы из XAML разметки в code-behind, в третьей — попробую дать пример кода, который помогает избежать подобных действий.
Пункт 1. АдвокатДавайте немного повнимательней посмотрим на указанную мной статью, и разберемся в чем же дело.Указанный материал закончился призывом качать скрипт и использовать его в своих проектах, но автор не дал себе труда привести пример ситуации, когда такой подход оправдан (как совершенно справедливо заметили в коментариях). В пользу подобного подхода могу привести только один пример. Допустим на минутку, что вы создаете приложение, одна из страниц которого включает в себя список юзеров. Вы сделали красивый темплейт для отображения юзера, к примеру, так: фотография пользователя (с необходимым размером и скейлингом), имя/никнейм, скайп/ тел.номер, и, конечно же, статус — оффлайн или онлайн.
На компьютере проблем не увидим — благо ресурсов хватает. Но рассмотрим ситуацию, когда список включает в себя несколько тысяч юзеров, а в руках у вас low-end девайс под управлением WinPhone8/8.1. Тут, очевидно, начнутся проблемы с производительностью. ListView будет тупить при скроллинге, возникнут артефакты, не спасёт и виртуализация. И если в Universal App вы можете попробовать оптимизировать производительность при помощи ContainerContentChanging, то в Silverlight-приложении так не получится (там попросту нету такой штуки).
Вот в таких ситуациях подобный подход оправдан: можно отказаться от биндингов, портящих всю малину, и напрямую «скармливать» цвета и иные ресурсы контролам / айтемам в листе и т.д. Да и то, заглядывая наперед, при использовании MVVM и/или Dependency Injections игра может не стоить свеч, а значит, получаем bad-practice в своем проекте, что может привести к осложнением валидации конечного продукта в магазине.
Пункт 2. Прокурор А теперь — к стенке ближе к делу.Во-первых, удивил меня сам подход. Зачем, ради всего святого кроме случая, приведенного в первом пункте, тянуть ресурсы из «родной» для них среды (XAML разметки) в code-behind и получать дополнительный шанс в них же и запутаться? Я лично считаю такую практику просто преступной и извращенной. И собираюсь немного «потыкать пальцем» в слабые на мой взгляд места подобного подхода.
Итак:
Если возникла внезапная необходимость тянуть ресурсы в code-behind — это, господа и дамы, костыль, который является признаком либо плохой архитектуры, либо плохо написанных контролов/ресурсов, либо и того, и другого вместе.
Представьте себе ситуацию (хотя б на минутку), что ваш проект выстрелил вам в ногу и вы получаете доход. Но вот проходит год и ребята из Microsoft на ежегодном мероприятии обещают введение новой экосистемы или хотя бы изменение существующей. Что получаем? Правильно, высокую вероятность крэша в результате попытки вытянуть переименованный, к примеру, системный цвет или свойство margin (да хоть что угодно, в принципе). Соответственно, придется лезть опять в позабытый код и брать в руки напильник переделывать кучу всего. Не слишком хорошая перспектива, не так ли?
Проблема с разработкой. Если вы усердно трудитесь над чем-то напоминающим Enterprise, то, скорее всего, вы используете паттерн MVVM и Dependency Injections. В таком случае все еще веселее, потому что при MVVM вам придется сначала вытянуть ресурс в свою VM-ку, а уж потом забайндить его к контролу на вьюшке: профита в перформансе никакого, а костыль — вот он, родимый! С DI каша заваривается еще круче. Допустим, Вы, ничтоже сумняшеся, сделали из парсера XAML свой сервис, и дёргаете его на энном количестве VM. Тут свинью вам подложит сам парсер, ибо xpath — не самая быстрая вещь, к сожалению. И если у Вас обращений к подобному сервису много, то лучше вы б его не писали вообще. От себя скажу, что, попытайся я использовать подобный подход на текущем проекте (за исключением ситуации из примера), получил бы по рукам.
Проблема с тестировкой. В упомянутом случае Enterprise, 100% будут написаны тесты или, что еще лучше, вы будете использовать TDD во время разработки. Как прикажете покрыть такой парсер тестами?
Пункт 3. А что же делать? Ну что же, кто критикует — обязан предоставить альтернативное решение. Предлагаю Вам свой путь, который позволяет в полной мере контролировать представление Вашего продукта без лишних костылей и велосипедов. Да, кода будет больше, но головной боли меньше. Встречайте: Конвертеры (Converters) К примеру, в зависимости полученного от сервера статуса юзера (оффлайн/онлайн) вам нужно изменить соответствующий текст в элементе списка.Код:
public class UserStateToStringConverter: IValueConverter { public object Convert (object value, Type targetType, object parameter, CultureInfo culture) { if (value!= null && value is UserState) { switch ((UserState)value) { case UserState.Online: return StringResources.Online; case UserState.Offline: return StringResources.Offline; } }
return null; }
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) { throw new System.NotImplementedException (); } } ConvertBack не заимплементирована просто за ненадобностью.UserState — наш enum на 2 значения (Online/Offline), а StringResources.Online/Offline — соответствующие локализованные ресурсы (строки).
Применение такого конвертера:
Регистрируем пространство имен с конвертерами:
xmlns: converters=«clr-namespace: Mindmarker.App.Converters»
Регистрируем наш конвертер для состояния пользователя:
Состояния контрола — Control States.При помощи состояний Вы можете сделать с контролом всё, что душе угодно, при помощи всего нескольких строк кода! Допустим, что, пока пользователь активен, у элемента в списке светлый фон, а когда неактивен — становится темнее. Чтоб добиться такого, элементом списка должен быть контрол — тут DataTemplate заменится на ControlTemplate — и в ControlTemplate должны быть описаны все его (контрола) состояния.
К примеру:
…тут сам темплейт, к которому применяются состояния. Итого, разметка контрола готова. Теперь посмотрим в зависимости от чего и как мы будем менять наши состояния. Мы должны сделать для нашего контрола свойство, к которому привяжем состояние пользователя, полученное с сервера в нашей VM-ке, и от которого оттолкнемся в дальнейшем:
public static readonly DependencyProperty UserStateProperty = DependencyProperty.Register («IndentAngle», typeof (UserState), typeof (UserControl), new PropertyMetadata (OnUserStatePropertyChanged)); Как видим, мы хотим чтобы изменение свойства UserStateProperty вызывало метод OnUserStatePropertyChanged. Он может выглядеть так:
public static void OnProgressControlPropertyChanged (DependencyObject d, DependencyPropertyChangedEventArgs e) { UserControl sender = d as UserControl;
if (sender!= null) { sender.ChangeVisualState ((UserState)e.NewValue); } } ChangeVisualState метод будет выглядеть следующим образом:
private void ChangeVisualState (UserState newState) { switch (newState) { case UserState.Offline: VisualStateManager.GoToState (this, «OfflineState», false); break; case UserState.Online: VisualStateManager.GoToState (this, «OnlineState», false); break; } } Теперь наш контрол будет спокойно менять свой цвет в зависимости от полученного значения.
Сладкое — на десерт.
Кроме таких банальных вещей, как конвертеры и состояния, есть еще такая штука, как ContentControl. И уж он позволяет сделать очень многое. Хотя, в принципе, подход нагло стянут у конвертера, но значительно расширен с помощью природы самого ContentControl-а. Взгляните:
Базовый класс для DataTemplateSelector-а (ведь конкретных реализаций может быть много, не так ли?):
public abstract class DataTemplateSelector: ContentControl { protected abstract DataTemplate GetTemplate (object item, DependencyObject container);
protected override void OnContentChanged (object oldValue, object newValue) { base.OnContentChanged (oldValue, newValue);
ContentTemplate = GetTemplate (newValue, this); } } И конкретный пример для нашего юзера:
public class UserStateTemplateControl: DataTemplateSelector { public DataTemplate UserOnlineTemplate { get; set; } public DataTemplate UserOfflineTemplate { get; set; }
protected override DataTemplate GetTemplate (object item, DependencyObject container) { UserState state = UserState.Offline;
if (item!= null && item is UserState) { state = (UserState)item; }
switch (state) { case CategoryProgressStatus.Offline: return UserOnlineTemplate; case CategoryProgressStatus.Online: return UserOfflineTemplate; }
return null; } } Та-а-ак, класс для своих нужд готов. Тепер поиграемся с XAML`ом.
Объявим пространство имен с нашим контролом — селектором.
xmlns: controls=«clr-namespace: Mindmarker.Controls; assembly=Mindmarker.Controls»
Напишем наши дополнительные темплейты, которые будут отвечать разным состояниям контрола:
И немного поменяем DataTemplate для пользователя из шага номер 1:
Всем заранее спасибо.