Правильное оперирование XAML-ресурсами

в 15:48, , рубрики: .net, #windowsPhone, silverlight, разработка под windows phone, метки: , ,

Привет.

Около недели назад прочитал статью «Как получить удобный доступ к 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js