Привет.
Около недели назад прочитал статью «Как получить удобный доступ к 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"
Регистрируем наш конвертер для состояния пользователя:
<converters:UserStateToStringConverter x:Key="UserStateToStringConverter"/>
Создадим темплейт для элемента списка пользователей.
<DataTemplate x:Key="AudioIconTemplate">
...тут у нас аватар
Header="{Binding UserState, Converter={StaticResource UserStateToStringConverter}}"
...а тут что-нибудь еще, не принципиально что именно.
</DataTemplate>
Вот, готово. Теперь мы видим, в каком состоянии находится тот или иной пользователь в нашем списке. Точно так же как и строку, конвертер может вернуть свойство типа Visibility или иное другое, вплоть до DataTemplate (хоть для темплейтов есть еще одна вкусняшка). Идем дальше, к самим XAML ресурсам.
Состояния контрола — Control States.
При помощи состояний Вы можете сделать с контролом всё, что душе угодно, при помощи всего нескольких строк кода! Допустим, что, пока пользователь активен, у элемента в списке светлый фон, а когда неактивен — становится темнее. Чтоб добиться такого, элементом списка должен быть контрол — тут DataTemplate заменится на ControlTemplate — и в ControlTemplate должны быть описаны все его (контрола) состояния.
К примеру:
<ControlTemplate TargetType="controls:UserControl">
<Grid x:Name="Container" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="UserStates">
<VisualState x:Name="OfflineState">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="UserStateTextBlock">
<EasingColorKeyFrame KeyTime="0" Value="{Binding DoneBadColor, RelativeSource={RelativeSource TemplatedParent}}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="OnlineState">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)" Storyboard.TargetName="UserStateTextBlock">
<EasingColorKeyFrame KeyTime="0" Value="{Binding DoneAverageColor, RelativeSource={RelativeSource TemplatedParent}}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateManager.VisualStateGroups>
......тут сам темплейт, к которому применяются состояния.
Итого, разметка контрола готова. Теперь посмотрим в зависимости от чего и как мы будем менять наши состояния. Мы должны сделать для нашего контрола свойство, к которому привяжем состояние пользователя, полученное с сервера в нашей 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 x:Key="UserOfflineStateTemplate">
<TextBlock Text="Offline"
Foreground="{StaticResource StatusOnlineBrush}"/>
</DataTemplate>
<DataTemplate x:Key="UserOnlineStateTemplate">
<TextBlock Text="Online"
Foreground="{StaticResource StatusOfflineBrush}"/>
</DataTemplate>
Где StatusOfflineBrush и StatusOnlineBrush — это абстрактные кисти, которые мы при надобности инициализовали би выше в XAML разметке.
И немного поменяем DataTemplate для пользователя из шага номер 1:
<DataTemplate x:Key="UserTemplate">
...тут все еще аватарка
<controls:StatProgressTemplateControl Content="{Binding UserState}"
OfflineTemplate="{StaticResource NotStartedIconTemplate}"
OnlineTemplate="{StaticResource UserOnlineStateTemplate}"/>
...а тут остальная часть темплейта.
</DataTemplate>
Ну что ж, жду гром и молнию конструктивную критику на свою голову.
Всем заранее спасибо.
Автор: Overrided