Правильное оперирование 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:MyApp.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:MyApp.Controls;assembly=MyApp.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>


Ну что ж, жду гром и молнию конструктивную критику на свою голову.

Всем заранее спасибо.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 26

    0
    ИМХО, «свой путь», это все-таки некорректно. Это стандартный путь, как оно все задумано. А предыдущий автор видимо еще не проникся идеологией XAML'а, поэтому пытается изобретать велосипеды
      –1
      Совершенно согласен относительно фразы «свой путь». Но, когда писал статью, то подумал что фраза "правильный путь" будет слишком вызывающей и претенциозной.
      +1
      Ресурсы для Вас это почему — то обязательно кисти или конвертеры? В ресурсах можно хранить любые application scope вещи, которые хочется удобно использовать через StaticResource. Даже в шаблоне проекта под Windows Phone 8+ LocalizedResources объявлены в Application.Resources для удобного использования через {Binding}. Можно сколь угодно долго спорить об архитектурных аспектах, о том, что кто — то использовал бы в данном случае DI и т.д.

      Определенно Вы неправильно меня поняли, моя статья не призывает заменить {Binding} и не призывает использовать все без исключения ресурсы из кода. В статье описан подход, который упростит именно те случаи, когда это необходимо, я дал инструмент, каждому инструменту свое место применения. Когда Вам нужно просверлить отверстие, вы берете дрель, когда Вам нужно наколоть дрова, вы берете топор. Вы же говорите, что топором неправильно сверлить отверстия, я согласен — неправильно.

      Ваш пример с вытягиванием переименованного ресурса не очень подходит, т.к. в этом случае и оригинальный {Binding} также перестанет работать.
        –1
        Ваш пример с вытягиванием переименованного ресурса не очень подходит, т.к. в этом случае и оригинальный {Binding} также перестанет работать.

        В этом моменте отчасти согласен. Пусть пример неудачен, тем не менее — простите за сравнение — Ваш инструмент это смесь бульдога с носорогом.
        В ресурсах можно хранить любые application scope вещи, которые хочется удобно использовать через StaticResource

        Можно не всегда означает «нужно». Я попытался обьяснить, почему так — но Вы отвергли то, что я пытался Вам донести, фразой
        Можно сколь угодно долго спорить об архитектурных аспектах...
        — потому что именно эти «архитектурные аспекты» — основа. Фундамент. Краеугольный камень, назыайте как хотите — при плохой архитектуре проект превратится в Бог знает что.
          0
          «Правильной» архитектуры не бывает, бывают задачи и инструменты для их решения, архитектура это тоже инструмент. Вы вырываете мои слова из контекста, я не говорю, что это «нужно», я привел пример того как используют ресурсы создатели данной технологии.
            –1
            «Правильной» архитектуры не бывает
            Да, зато бывает хорошее архитектурное решение или плохое. И — IMHO — в хорошей архитектуре, которая может быть расширена в будушем с помощью добавления новых модулей и т.д. и которая будет достаточно гибкой — место для Вашего подхода вряд ли найдется.
              +3
              Строковые ресурсы (StringResources.Online) собираются из XML файла аналогичным способом в класс имена свойств которого идентичны ключам из XML, и вы их используете в коде. Слой для хранения настроек Application Settings также использует генерацию класса с обертками над строковыми идентификаторами, чтобы обеспечить вам строготипизированное хранилище без магических строк. В конце-концов, Visual studio генерирует вам поле в классе window\page на каждый именованный контрол. Это мешало модульности? Расширяемости? Это придумал не я, это придумали в Microsoft.
              Вы упорно доказываете мне, что топором дырку сверлить неудобно.
                +1
                Пусть так, дырку топором не сверлят. Но Вы не дали себе труда обьяснить, как и в каких ситуациях использовать Ваш инструмент, считая это очевидным. Примерно так: «Это генератор чёрных дыр. Включается в розетку вот тут.»
                Вся моя статья родилась из двух вопросов, которые я задал себе, не найдя ответов в Вашем материале. А именно «Зачем ?» и «Как применять?». Грубо говоря, я подхожу к разработке с такой точки зрения — если в гайдлайнах и best-practices о чём-то не упоминается, то стоит задуматься, а нужно ли это «что-то» вообще?
                  +1
                  Еще вопрос, вытекающий из первых. А нет ли более привычного или традиционного решения.
                    –2
                    Да, в целом так и было)
        0
        На ум приходят две ситуации, когда приходится из xaml тянуть ресурсы/контролы в code behind:
        1. При создании кастомных контролов приходится разносить ControlTemplate и класс контрола на отдельные файлы если мы хотим, чтобы от нашего контрола можно было наследоваться. Для обращения к внутренним элементам контрола из кода приходится также обращаться к xaml.
        2. При использовании WPF локализации, увы, строки приходится хранить в xaml-ресурсах. Доступ к строкам в code behind как раз осуществляется через ресурсы приложения.
          +2
          А чем стандартное хранилище в RESX не подходит для локализации? Постоянно пользуемся — нет проблем и нужды шарить строки в XAML.
            0
            Локализация с помощью RESX файлов действительно удобнее. Однако бывают ситуации, когда выбор уже был сделан в пользу локализации WPF. Приходится работать с тем что есть. В этом случае кстати выручает Text Template, описанный в предыдущей статье.
              0
              Даже когда писали проекты под WinPhone7, всегда хранили локализацию в RESX. Не поделитесь причинами, которые соблазнили Вас строки в XAML хранить?
                +1
                Повторюсь, выбор был сделан до моего подключения к проекту. Если бы я писал проект с нуля — использовал бы resx.
                В общем, если использовать для локализации средства WPF (с утилитой LocBaml), так или иначе приходиться тянуть строки из словарей ресурсов. Да, локализация WPF не самый лучший инструмент по сравнению с resx.
                  0
                  Перевожу: было принято политическое-антипродуктивное решение, религиозная неприязнь или имели место прочие артефакты эффективного менеджмента. А может зависимость от другого проекта, на котором имелр место одно из описанного в начале этого комментария.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Да, действительно. Про них я не упомянул — подвела избирательность памяти(
              0
              А разве триггеры доступны в SL?
                0
                Для SL был Expression SDK, в котором был аналог для DataTrigger.
                  0
                  Из тех же сборок доступна пачка триггеров для WPF также.
                  Только с ними нужно быть осторожно.
                  Недавно вот наткнулся на багу в Interactivity.KeyTrigger подписывается на клавиатурные события каждый раз как срабатывает событие Loaded у AssociatedObject. Отписку при этом не делает. Получается сколько раз табы в tabControl попереключаешь, столько раз у тебя KeyTrigger внутри контента табы дернет команду или другой Trigger Action.

                  Где-то на stackOverflow фикс запостан.
            0
            Но рассмотрим ситуацию, когда список включает в себя несколько тысяч юзеров, а в руках у вас low-end девайс под управлением WinPhone8/8.1. Тут, очевидно, начнутся проблемы с производительностью.

            Неудачный пример. Списки поддерживают виртуализацию GUI.
              0
              Возможно, пример неудачен. Но я оговорился, что тупить скроллинг будет как минимум на low-end девайсах, Виртуализация там не спасет — особенно в случае с таким теплейтом для юзера, который указал я — с аватаркой и кучей текста.
              0
              Если возникла внезапная необходимость тянуть ресурсы в code-behind — это, господа и дамы, костыль, который является признаком либо плохой архитектуры, либо плохо написанных контролов/ресурсов, либо и того, и другого вместе.

              Ну почему это? Расскажите как реализовать StyleSelector или DataTemplateSelector без ссылок на ресурсы из кода?

              Для конвертеров куда удобней пользоваться синглтонами в комбинации с {x:Static}. Да, знаю что не работает под сервелатом.
                +1
                По поводу вашего UserStateToStringConverter, если уж и писать такие конверторы, то хотелось бы имена ресурсов задавать в xaml, например, задавать dictionary соответствий UserState и имен ресурсов. В этом случае не придется плодить конверторы на все подобные случаи.
                По поводу UserStateTemplateControl, здесь это вообще не нужно. Гораздо проще это сделать в xaml безо всякого code-behind: заюзать ContentPresenter и по вашему UserState переключать шаблоны тригерами.
                  0
                  dictionary соответствий UserState и имен ресурсов

                  Смотря как Вы храните локализацию. Если в XAML — то ок, согласен. А если в resx? Во втором случае, по-моему, конвертер удобней. К тому же такой конвертер был приведен как пример, я никого не призываю «плодить конвертеры».
                  А что касается Content Presenter`а, то триггеры тоже не всегда самые удобные вещи. К примеру имя ресурса в XAML было кем-то переименовано, и апликуха падает. Найти просто в обеих случаях, вот только с триггером поддерживать сложней. Я не утверждаю что триггеры — плохой подход и т.д., я просто пытался найти самый оптимальный путь для оперирования XAML ресурсами.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое