Поддержка визуальных состояний

81

Элемент управления ColorPicker демонстрирует хороший пример проектирования элемента управления. Поскольку его поведение и внешний вид тщательно разделены, другие дизайнеры могут создавать новые шаблоны, которые радикально изменят его внешность.

Одной из причин простоты ColorPicker является отсутствие в нем концепции состояний. Другими словами, он не меняет своего внешнего вида в зависимости от наличия фокуса, наведенного курсора мыши, доступности и т.п. Элемент FlipPanel из следующего примера в этом отношении несколько отличается.

Основная идея, положенная в основу FlipPanel, состоит в том, что он предоставляет две поверхности для размещения содержимого, из которых в каждый конкретный момент времени видимой является только одна. Чтобы увидеть другую, вам нужно "перевернуть" панель. Эффект "переворачивания" может быть настроен с помощью шаблона элемента управления, но эффект по умолчанию использует простое затухание, которое обеспечивает переход между передней и задней поверхностями:

Переворачивание FlipPanel

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

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

Ясно, что шаблон элемента управления должен указывать два отдельных раздела: области содержимого передней и задней стороны FlipPanel. Однако есть одна дополнительная деталь, а именно — элементу FlipPanel нужен какой-нибудь способ переключения между двумя состояниями: перевернутым и не перевернутым. Это можно было бы сделать, добавив триггеры к шаблону. Один триггер скрывал бы переднюю панель и показывал вторую по щелчку на кнопке, в то время как другой возвращал бы ее обратно. При этом можно использовать любую приемлемую анимацию.

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

Начало проектирования класса FlipPanel

Если выделить суть, то FlipPanel окажется неожиданно простым элементом. Он состоит из двух областей содержимого, которые пользователь может наполнить единственным элементом (скорее всего, контейнером компоновки, содержащим набор элементов). Формально это значит, что элемент FlipPanel — не настоящая панель, потому что она не использует логики компоновки для организации группы дочерних элементов.

Однако это вряд ли вызовет проблему, потому что структура FlipPanel ясна и интуитивно понятна. FlipPanel также включает в себя кнопку, позволяющую переключаться между двумя областями содержимого.

Хотя можно создать специальный элемент управления, наследуя его от класса вроде ContentControl или Panel, класс FlipPanel наследуется непосредственно от базового класса Control. Если функциональность специализированного класса элемента управления не нужна, то это — лучшая отправная точка. Наследование от более простого класса FrameworkElement даст в результате нечто, лишенное стандартной инфраструктуры элемента и шаблона:

public class FlipPanel : Control
{...}

Первое, что понадобится сделать — это создать свойства для FlipPanel. Как почти все свойства в элементе WPF, это должны быть свойства зависимости. Ниже показано, как FlipPanel определяет свойство FrontContent, которое содержит элемент, отображаемый на передней поверхности:

public static readonly DependencyProperty FrontContentProperty = 
     DependencyProperty.Register("FrontContent", typeof(object), typeof(FlipPanel), null);

public object FrontContent
{
      get
      {
             return GetValue(FrontContentProperty);
      }
      set
      {
            SetValue(FrontContentProperty, value);
      }
}

Свойство BackContent почти идентично:

public static readonly DependencyProperty BackContentProperty =
            DependencyProperty.Register("BackContent", typeof(object), typeof(FlipPanel), null);

public object BackContent
{
    get
    {
        return GetValue(BackContentProperty);
    }
    set
    {
         SetValue(BackContentProperty, value);
    }
}

Остается добавить только одно существенное свойство: IsFlipped. Это свойство типа bool отслеживает текущее состояние FlipPanel (повернута панель или нет) и позволяет потребителю элемента управления переворачивать его программно:

public static readonly DependencyProperty IsFlippedProperty =
            DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipPanel), null);

        public bool IsFlipped
        {
            get
            {
                return (bool)GetValue(IsFlippedProperty);
            }
            set
            {
                SetValue(IsFlippedProperty, value);      

                ChangeVisualState(true);
            }
        }

Средство установки свойства IsFlipped вызывает специальный метод по имени ChangeVisualState(). Этот метод обеспечивает обновление изображения для соответствия текущему состоянию (повернута панель лицом или тылом). Код, который решает эту задачу, рассматривается чуть позже.

FlipPanel наследует почти все необходимое от класса Control. Исключением является лишь свойство CornerRadius. Хотя класс Control включает свойства BorderBrush и BorderThickness, которые можно применять для рисования контура вокруг FlipPanel, ему недостает свойства CornerRadius для скругления квадратных углов, как это делает элемент Border. Реализовать такой же эффект в FlipPanel просто — необходимо добавить свойство зависимости CornerRadius и воспользоваться им для конфигурирования элемента Border в шаблоне FilpPanel по умолчанию:

public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(FlipPanel), null);

        public CornerRadius CornerRadius
        {
            get
            {
                return (CornerRadius)GetValue(CornerRadiusProperty);
            }
            set
            {
                SetValue(CornerRadiusProperty, value);
            }
        }

Также нужно добавить стиль, применяющий шаблон по умолчанию к FlipPanel. Этот стиль помещается в словарь ресурсов generic.xaml, как и в ColorPicker. Остается одна последняя деталь. Чтобы заставить элемент управления выбрать стиль по умолчанию из файла generic.xaml, нужно вызвать метод DefaultStyleKeyProperty.OverrideMetadata() в статическом конструкторе FlipPanel:

static FlipPanel()
{
   DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipPanel), 
        new FrameworkPropertyMetadata(typeof(FlipPanel)));
}

Выбор частей и состояний

Имея базовую структуру, можно идентифицировать части и состояния, которые будут использованы в шаблоне элемента управления. Ясно, что для FlipPanel требуются два состояния:

Вдобавок понадобятся две части:

FlipButton

Это кнопка, при щелчке на которой видимость переключается от передней панели к задней (или наоборот). FlipPanel обеспечивает это, обрабатывая события данной кнопки.

FlipButtonAlternate

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

Можно было бы также добавить содержимое для фронтальной и обратной области. Однако элементу FlipPanel не нужно манипулировать этими областями непосредственно — до тех пор, пока шаблон включает анимацию, скрывающую или отображающую их в надлежащее время. (Другой вариант — определить эти части так, чтобы явно изменять их видимость в коде. В таком случае панель сможет переключаться между передней и задней областями содержимого, даже если никакая анимация не определена, скрывая одну часть и показывая другую. Для простоты в FlipPanel такой подход использоваться не будет.)

Чтобы анонсировать факт, что FlipPanel использует эти части и состояния, необходимо применить атрибут TemplatePart к классу элемента управления, как показано ниже:

[TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton)),
    TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton)),
    TemplateVisualState(Name = "Normal", GroupName = "ViewStates"),
    TemplateVisualState(Name = "Flipped", GroupName = "ViewStates")]

Части FlipButton и FlipButtonAlternate ограничены — каждая из них может быть только экземпляром ToggleButton либо экземпляром класса, унаследованного от ToggleButton.

Для обеспечения более гибкой поддержки шаблона применяйте наименее специализированный тип элемента. Например, лучше использовать FrameworkElement, чем ContentControl, если только не требуется какое-нибудь свойство или поведение, предоставляемое ContentControl.

Пройди тесты
Лучший чат для C# программистов