Создание элемента управления с настраиваемым внешним видом

Windows Presentation Foundation (WPF) позволяет создавать элементы управления, внешний вид которых можно настраивать. Например, можно изменить внешний вид элемента CheckBox сверх того, что возможно с помощью свойств, создав новый объект ControlTemplate. На следующем рисунке показан элемент CheckBox, использующий объект ControlTemplate по умолчанию, и элемент CheckBox, использущий пользовательский объект ControlTemplate.

A checkbox with the default control template. CheckBox, использующий шаблон элемента управления по умолчанию

A checkbox with a custom control template. CheckBox, использующий пользовательский шаблон элемента управления

Если при создании элемента управления применяется модель частей и состояний, его внешний вид будет настраиваемым. Такие инструменты конструктора, как Blend для Visual Studio, поддерживают модель частей и состояний, поэтому при ее использовании элемент управления может настраиваться в таких приложениях. В этом разделе рассматривается модель частей и состояний, а также приводятся инструкции по ее реализации при создании собственного элемента управления. В данном разделе для иллюстрации этой модели используется пример пользовательского элемента управления, NumericUpDown. Элемент управления NumericUpDown отображает числовое значение, которое можно увеличивать или уменьшать с помощью кнопок элемента управления. На следующем рисунке показан элемент управления NumericUpDown, рассматриваемый в этом разделе.

NumericUpDown custom control. Настраиваемый элемент управления NumericUpDown

Этот раздел состоит из следующих подразделов.

Необходимые компоненты

В этом разделе предполагается, что вы умеете создать новый объект ControlTemplate для существующего элемента управления, знакомы с элементами контракта элемента управления и владеете основными понятиями, описанными в разделе Создание шаблона элемента управления.

Примечание.

Чтобы создать элемент управления, внешний вид которого можно настраивать, необходимо создать элемент управления, который наследуется от класса Control или одного из его подклассов, отличных от UserControl. Элемент управления, который наследуется от UserControl, создается быстро, однако в нем не используется объект ControlTemplate, поэтому его внешний вид настраивать нельзя.

Модель частей и состояний

Модель частей и состояний указывает, как определить визуальную структуру и визуальное поведение элемента управления. Чтобы использовать модель частей и состояний, выполните следующие действия.

  • Определите визуальную структуру и визуальное поведение в объекте ControlTemplate элемента управления.

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

  • Предоставьте контракт элемента управления, чтобы указать, что необходимо включить в объект ControlTemplate.

Если визуальная структура и визуальное поведение определены в объекте ControlTemplate элемента управления, разработчики приложения могут менять визуальную структуру и визуальное поведение элемента управления, создавая новый объект ControlTemplate вместо написания кода. Необходимо предоставить контракт элемента управления, в котором разработчикам приложений сообщается, какие объекты и состояния FrameworkElementдолжны определяться в объекте ControlTemplate. При взаимодействии с частями в объекте ControlTemplate следует соблюдать некоторые рекомендации, чтобы элемент управления правильно работал с неполным объектом ControlTemplate. Если придерживаться этих трех принципов, разработчики приложений смогут создавать объект ControlTemplate для элемента управления так же просто, как и для элементов управления, которые поставляются в WPF. В следующем разделе все эти рекомендации описываются подробно.

Определение визуальной структуры и визуального поведения элемента управления в ControlTemplate

При создании пользовательского элемента управления с помощью модели частей и состояний визуальная структура и визуальное поведение элемента управления определяются в объекте ControlTemplate, а не в логике элемента. Визуальная структура элемента управления — это коллекция объектов FrameworkElement, которые составляют элемент управления. Визуальное поведение — это способ отображения элемента управления в определенном состоянии. Дополнительные сведения о создании объекта ControlTemplate, который определяет визуальную структуру и визуальное поведение элемента управления, см. в разделе Создание шаблона элемента управления.

В примере с элементом управления NumericUpDown визуальная структура включает два элемента управления RepeatButton и элемент TextBlock. Если добавить эти элементы управления в коде элемента NumericUpDown, например в конструкторе, их позиции нельзя будет менять. Вместо определения визуальной структуры и визуального поведения элемента управления в коде их следует определить в объекте ControlTemplate. Тогда разработчики приложений смогут настраивать положение кнопок и элемента TextBlock и задавать поведение при отрицательном значении Value, так как объект ControlTemplate можно будет заменить.

В следующем примере показана визуальная структура элемента управления NumericUpDown, в которую входит элемент RepeatButton для увеличения значения Value, элемент RepeatButton для уменьшения значения Value и элемент TextBlock для отображения значения Value.

<ControlTemplate TargetType="src:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
              Margin="7,2,2,2" Grid.RowSpan="2" 
              Background="#E0FFFFFF"
              VerticalAlignment="Center" 
              HorizontalAlignment="Stretch">

        <!--Bind the TextBlock to the Value property-->
        <TextBlock Name="TextBlock"
                   Width="60" TextAlignment="Right" Padding="5"
                   Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                     AncestorType={x:Type src:NumericUpDown}}, 
                     Path=Value}"/>
      </Border>

      <RepeatButton Content="Up" Margin="2,5,5,0"
        Name="UpButton"
        Grid.Column="1" Grid.Row="0"/>
      <RepeatButton Content="Down" Margin="2,0,5,5"
        Name="DownButton"
        Grid.Column="1" Grid.Row="1"/>

      <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
        Stroke="Black" StrokeThickness="1"  
        Visibility="Collapsed"/>
    </Grid>

  </Grid>
</ControlTemplate>

Визуальное поведение элемента управления NumericUpDown заключается в том, что, когда его значение отрицательное, оно отображается красным шрифтом. Если изменить свойство Foreground элемента TextBlock в коде при отрицательном значении Value, в элементе NumericUpDown всегда будет отображаться красное отрицательное значение. Визуальное поведение элемента управления задается в объекте ControlTemplate путем добавления в ControlTemplate объектов VisualState. В следующем примере показаны объекты VisualState для состояний Positive и Negative. Positive и Negative являются взаимоисключающими (элемент управления всегда находится только в одном из двух состояний), поэтому в примере объекты VisualState помещаются в одну группу VisualStateGroup. Когда элемент управления переходит в состояние Negative, фон Foreground элемента TextBlock становится красным. Когда элемент управления находится в состоянии Positive, фон Foreground возвращается к исходному значению. Определение объектов VisualState в элементе управления ControlTemplate рассматривается подробнее в разделе Создание шаблона элемента управления.

Примечание.

Необходимо обязательно установить присоединенное свойство VisualStateManager.VisualStateGroups в корневом элементе FrameworkElement в объекте ControlTemplate.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Использование частей элемента ControlTemplate в коде

Создатель элемента ControlTemplate может намеренно или по ошибке пропустить объекты FrameworkElement или VisualState, однако они могут требоваться для нормальной работы логики элемента управления. Модель частей и состояний требует, чтобы отсутствие объектов FrameworkElement или VisualState в элементе ControlTemplate не влияло на работу элемента управления. Элемент управления не должен создавать исключение или сообщать об ошибке, если в элементе ControlTemplate отсутствует объект FrameworkElement, VisualState или VisualStateGroup. В этом разделе описываются рекомендации по взаимодействию с объектами FrameworkElement и управлению состояниями.

Прогнозирование отсутствия объектов FrameworkElement

Логика элемента управления может предусматривать взаимодействие с некоторыми объектами FrameworkElement, определенными в элементе ControlTemplate. Например, элемент управления NumericUpDown подписывается на событие кнопок Click, чтобы увеличивать или уменьшать значение Value, и для свойства Text элемента TextBlock задает значение Value. Если в пользовательском элементе управления ControlTemplate отсутствует элемент TextBlock или кнопки, в нем могут не работать связанные функции, однако это не должно вызывать ошибку. Например, если в элементе ControlTemplate нет кнопок для изменения значения Value, элемент NumericUpDown теряет эту функцию, однако приложение, использующее ControlTemplate, продолжит выполняться.

Выполнение следующих рекомендаций гарантирует, что элемент управления будет правильно реагировать на отсутствие объектов FrameworkElement.

  1. Задайте атрибут x:Name для каждого объекта FrameworkElement, на который необходимо ссылаться в коде.

  2. Определите частные свойства для каждого объекта FrameworkElement, с которым необходимо взаимодействовать.

  3. Подпишитесь и отмените подписку на все события, обрабатываемые элементом управления, в методе доступа set для свойства FrameworkElement.

  4. Задайте свойства FrameworkElement, определенные на шаге 2, в методе OnApplyTemplate. Это самый ранний этап, когда объект FrameworkElement в ControlTemplate становится доступным для элемента управления. Используйте атрибут x:Name в объекте FrameworkElement, чтобы получить его из элемента ControlTemplate.

  5. Убедитесь, что объект FrameworkElement не имеет значения null, прежде чем получать доступ к его членам. Если он имеет значение null, не сообщайте об ошибке.

В следующих примерах показано, как элемент управления NumericUpDown взаимодействует с объектами FrameworkElement в соответствии с рекомендациями в предыдущем списке.

В примере, определяющем визуальную структуру элемента управления NumericUpDown в элементе ControlTemplate, кнопка RepeatButton, которая увеличивает значение Value, имеет атрибут x:Name со значением UpButton. В следующем примере объявляется свойство UpButtonElement, которое представляет элемент RepeatButton, объявленный в элементе ControlTemplate. Метод доступа set сначала отменяет подписку на событие кнопки Click, если элемент UpDownElement не имеет значения null, затем устанавливает свойство и подписывается на событие Click. Здесь также есть свойство для другого элемента RepeatButton под названием DownButtonElement. Оно определено, но не показано.

private RepeatButton upButtonElement;

private RepeatButton UpButtonElement
{
    get
    {
        return upButtonElement;
    }

    set
    {
        if (upButtonElement != null)
        {
            upButtonElement.Click -=
                new RoutedEventHandler(upButtonElement_Click);
        }
        upButtonElement = value;

        if (upButtonElement != null)
        {
            upButtonElement.Click +=
                new RoutedEventHandler(upButtonElement_Click);
        }
    }
}
Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal value As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = value

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property

В следующем примере приведен код метода OnApplyTemplate для элемента управления NumericUpDown. В примере используется метод GetTemplateChild для получения объектов FrameworkElement из элемента ControlTemplate. Обратите внимание, что в примере реализована защита от случаев, когда метод GetTemplateChild находит объект FrameworkElement с указанным именем, но непредвиденным типом. Кроме того, рекомендуется игнорировать элементы с указанными атрибутами x:Name, но некорректным типом.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Следуя рекомендациям, приведенным в предыдущих примерах, можно гарантировать, что элемент управления продолжит работать, когда в элементе ControlTemplate отсутствует объект FrameworkElement.

Использование VisualStateManager для управления состояниями

Элемент VisualStateManager отслеживает состояния элемента управления и выполняет логику, необходимую для перехода между состояниями. При добавлении объектов VisualState в элемент ControlTemplate они добавляются в элемент VisualStateGroup, а элемент VisualStateGroup добавляется в присоединенное свойство элементов VisualStateManager.VisualStateGroups, чтобы у элемента VisualStateManager был доступ к ним.

В следующем примере повторяется предыдущий пример, в котором показаны объекты VisualState, соответствующие состояниям Positive и Negative элемента управления. Объект Storyboard с состоянием VisualState равным Negative делает фон Foreground элемента TextBlock красным. Когда элемент управления NumericUpDown находится в состоянии Negative, начинается раскадровка в состоянии Negative. Отображение элемента Storyboard в состоянии Negative останавливается, когда элемент управления возвращается в состояние Positive. Состояние Positive со значением VisualState не обязательно должно содержать элемент Storyboard, так как при остановке отображения Storyboard в состоянии Negative фон Foreground возвращается к исходному цвету.

<ControlTemplate TargetType="local:NumericUpDown">
  <Grid  Margin="3" 
         Background="{TemplateBinding Background}">

    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup Name="ValueStates">

        <!--Make the Value property red when it is negative.-->
        <VisualState Name="Negative">
          <Storyboard>
            <ColorAnimation To="Red"
              Storyboard.TargetName="TextBlock" 
              Storyboard.TargetProperty="(Foreground).(Color)"/>
          </Storyboard>

        </VisualState>

        <!--Return the TextBlock's Foreground to its 
            original color.-->
        <VisualState Name="Positive"/>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate>

Обратите внимание, что элементу TextBlock присваивается имя, но TextBlock не находится в контракте элемента управления NumericUpDown, так как логика элемента управления никогда не ссылается на TextBlock. Элементы, на которые ссылаются в элементе ControlTemplate, обладают именами, но не обязательно должны быть частью контракта элемента управления, так как новому элементу ControlTemplate может не требоваться ссылаться на эти элементы. Например, создатель нового элемента ControlTemplate для NumericUpDown может не указывать, что значение Value является отрицательным, меняя фон Foreground. В этом случае ни код, ни элемент ControlTemplate не ссылаются на элемент TextBlock по имени.

Изменение состояния элемента управления обеспечивается логикой элемента управления. В следующем примере показано, что элемент управления NumericUpDown вызывает метод GoToState для перехода в состояние Positive, если значение Value больше или равно 0, и в состояние Negative, если значение Value меньше 0.

if (Value >= 0)
{
    VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
    VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If

Метод GoToState выполняет логику, необходимую для надлежащего запуска и остановки раскадровки. Если элемент управления вызывает метод GoToState для изменения своего состояния, элемент VisualStateManager выполняет следующие действия:

  • Если состояние VisualState, в которое переходит элемент управления, имеет элемент Storyboard, раскадровка начинается. Если состояние VisualState, из которого переходит элемент управления, имеет элемент Storyboard, раскадровка заканчивается.

  • Если элемент управления уже находится в указанном состоянии, метод GoToState не выполняет никаких действий и возвращает значение true.

  • Если указанное состояние не существует в элементе ControlTemplate объекта control, метод GoToState не выполняет никаких действий и возвращает значение false.

Рекомендации по работе с VisualStateManager

Для поддержания состояния элемента управления рекомендуется выполнять следующие действия.

  • Используйте свойства для отслеживания его состояния.

  • Создайте вспомогательный метод для перехода между состояниями.

Элемент управления NumericUpDown использует свое свойство Value для отслеживания того, находится ли он в состоянии Positive или Negative. В элементе управления NumericUpDown также определяются состояния Focused и UnFocused, которые отслеживают значение свойства IsFocused. При использовании состояний, которые не соответствуют свойству элемента управления, можно определить частное свойство для отслеживания состояния.

Использование одного метода, который обновляет все состояния, централизует вызовы VisualStateManager и обеспечивает управляемость кода. В следующем примере показан вспомогательный метод элемента управления NumericUpDown, UpdateStates. Если значение Value больше или равно 0, элемент Control находится в состоянии Positive. Если значение Value меньше 0, элемент управления находится в состоянии Negative. Когда свойство IsFocused имеет значение true, элемент управления находится в состоянии Focused, противном случае он находится в состоянии Unfocused. Элемент управления может вызывать метод UpdateStates каждый раз, когда ему нужно изменить свое состояние, независимо от того, какое состояние меняется.

private void UpdateStates(bool useTransitions)
{
    if (Value >= 0)
    {
        VisualStateManager.GoToState(this, "Positive", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Negative", useTransitions);
    }

    if (IsFocused)
    {
        VisualStateManager.GoToState(this, "Focused", useTransitions);
    }
    else
    {
        VisualStateManager.GoToState(this, "Unfocused", useTransitions);
    }
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)

    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If IsFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

    End If
End Sub

Если методу GoToState передается название состояния, в котором находится элемент управления, GoToState ничего не делает, поэтому проверять текущее состояние элемента управления не нужно. Например, если значение Value меняется с одного отрицательного числа на другое, раскадровка для состояния Negative не прерывается, и пользователь не увидит изменения в элементе управления.

Элемент VisualStateManager использует объекты VisualStateGroup для определения состояния, из которого выполняется выход, при вызове метода GoToState. Элемент управления всегда находится в одном состоянии из группы VisualStateGroup, определенной в его элементе ControlTemplate, и выходит из состояния только при переходе в другое состояние из той же группы VisualStateGroup. Например, элемент ControlTemplate в элементе NumericUpDown определяет объекты VisualState (Positive и Negative) в одной группе VisualStateGroup и объекты VisualState (Focused и Unfocused) — в другой. (Состояния VisualState (Focused и Unfocused) определены в разделе Полный пример в этом разделе.) Когда элемент управления переходит из состояния Positive в состояние Negative или наоборот, он остается либо в состоянии Focused, либо в состоянии Unfocused.

Есть три типовых ситуации, когда меняется состояние элемента управления:

  • Когда к элементу Control применяется объект ControlTemplate.

  • Когда меняется свойство.

  • Когда возникает событие.

В следующих примерах показано обновление состояния элемента управления NumericUpDown в этих случаях.

Необходимо обновить состояние элемента управления в методе OnApplyTemplate, чтобы элемент управления отображался в правильном состоянии при применении объекта ControlTemplate. В следующем примере в методе OnApplyTemplate вызывается UpdateStates, чтобы убедиться, что элемент управления находится в правильных состояниях. Допустим, что вы создаете элемент управления NumericUpDown, а затем для его свойства Foreground задаете значение зеленого цвета, а в качестве значения Value указываете –5. Если не вызвать UpdateStates, когда к элементу NumericUpDown применяется объект ControlTemplate, элемент управления не перейдет в состояние Negative, а его значение будет зеленым, а не красным. Чтобы перевести элемент управления в состояние Negative, необходимо вызвать UpdateStates.

public override void OnApplyTemplate()
{
    UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
    DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
    //TextElement = GetTemplateChild("TextBlock") as TextBlock;

    UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()

    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

    UpdateStates(False)
End Sub

Часто требуется обновлять состояния элемента управления при изменении свойства. В следующем примере показан метод ValueChangedCallback целиком. Так как ValueChangedCallback вызывается при изменении Value, метод вызывает UpdateStates, если значение Value изменилось с положительного на отрицательное или наоборот. Допустимо вызывать UpdateStates, когда значение Value меняется, но остается положительными или отрицательными, так как в этом случае элемент управления не изменит свое состояние.

private static void ValueChangedCallback(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
{
    NumericUpDown ctl = (NumericUpDown)obj;
    int newValue = (int)args.NewValue;

    // Call UpdateStates because the Value might have caused the
    // control to change ValueStates.
    ctl.UpdateStates(true);

    // Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(
        new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
            newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                        ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = CInt(args.NewValue)

    ' Call UpdateStates because the Value might have caused the
    ' control to change ValueStates.
    ctl.UpdateStates(True)

    ' Call OnValueChanged to raise the ValueChanged event.
    ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub

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

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    UpdateStates(True)
End Sub

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

Предоставление контракта элемента управления

Контракт элемента управления предоставляется для того, чтобы разработчики ControlTemplate знали, что следует поместить в шаблон. Контракт элемента управления имеет три элемента:

  • визуальный элемент, используемый логикой элемента управления;

  • состояния элемента управления и группа, к которой принадлежит каждое состояние;

  • общие свойства, визуально воздействующие на элемент управления.

Создатель нового элемента ControlTemplate должен знать, какие объекты FrameworkElement используются в логике элемента управления, какой тип у каждого объекта и какие у них имена. Создателю ControlTemplate также нужно знать имя каждого возможного состояния, в котором может находиться элемент управления, и имя состояния, в котором VisualStateGroup находится в данный момент.

Возвращаясь к примеру NumericUpDown, элемент управления ожидает, что у элемента ControlTemplate имеются следующие объекты FrameworkElement:

Элемент управления может находиться в следующих состояниях:

Чтобы указать объекты, которые ожидает элемент управления FrameworkElement, используйте элемент TemplatePartAttribute, в котором задаются имя и тип ожидаемых элементов. Чтобы указать возможные состояния элемента управления, используйте элемент TemplateVisualStateAttribute, в котором задается имя состояния и группа VisualStateGroup, к которой состояние принадлежит. Включите TemplatePartAttribute и TemplateVisualStateAttribute в определение класса для элемента управления.

Любое общее свойство, влияющее на внешний вид элемента управления, также является частью контракта элемента управления.

В следующем примере указываются объект FrameworkElement и состояния для элемента управления NumericUpDown.

[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
    Inherits Control
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty

    Public Property TextAlignment() As TextAlignment

    Public Property TextDecorations() As TextDecorationCollection

    Public Property TextWrapping() As TextWrapping
End Class

Полный пример

В следующем примере показан весь элемент ControlTemplate для элемента управления NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:VSMCustomControl">


  <Style TargetType="{x:Type local:NumericUpDown}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="local:NumericUpDown">
          <Grid  Margin="3" 
                Background="{TemplateBinding Background}">


            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup Name="ValueStates">

                <!--Make the Value property red when it is negative.-->
                <VisualState Name="Negative">
                  <Storyboard>
                    <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(Color)"/>
                  </Storyboard>

                </VisualState>

                <!--Return the control to its initial state by
                    return the TextBlock's Foreground to its 
                    original color.-->
                <VisualState Name="Positive"/>
              </VisualStateGroup>

              <VisualStateGroup Name="FocusStates">

                <!--Add a focus rectangle to highlight the entire control
                    when it has focus.-->
                <VisualState Name="Focused">
                  <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual" 
                                                   Storyboard.TargetProperty="Visibility" Duration="0">
                      <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                          <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                      </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                  </Storyboard>
                </VisualState>

                <!--Return the control to its initial state by
                    hiding the focus rectangle.-->
                <VisualState Name="Unfocused"/>
              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid>
              <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
              </Grid.ColumnDefinitions>

              <Border BorderThickness="1" BorderBrush="Gray" 
                Margin="7,2,2,2" Grid.RowSpan="2" 
                Background="#E0FFFFFF"
                VerticalAlignment="Center" 
                HorizontalAlignment="Stretch">
                <!--Bind the TextBlock to the Value property-->
                <TextBlock Name="TextBlock"
                  Width="60" TextAlignment="Right" Padding="5"
                  Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                                 AncestorType={x:Type local:NumericUpDown}}, 
                                 Path=Value}"/>
              </Border>

              <RepeatButton Content="Up" Margin="2,5,5,0"
                Name="UpButton"
                Grid.Column="1" Grid.Row="0"/>
              <RepeatButton Content="Down" Margin="2,0,5,5"
                Name="DownButton"
                Grid.Column="1" Grid.Row="1"/>

              <Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2" 
                Stroke="Black" StrokeThickness="1"  
                Visibility="Collapsed"/>
            </Grid>

          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

В следующем примере показана логика элемента управления NumericUpDown.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace VSMCustomControl
{
    [TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
    [TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
    [TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
    public class NumericUpDown : Control
    {
        public NumericUpDown()
        {
            DefaultStyleKey = typeof(NumericUpDown);
            this.IsTabStop = true;
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value", typeof(int), typeof(NumericUpDown),
                new PropertyMetadata(
                    new PropertyChangedCallback(ValueChangedCallback)));

        public int Value
        {
            get
            {
                return (int)GetValue(ValueProperty);
            }

            set
            {
                SetValue(ValueProperty, value);
            }
        }

        private static void ValueChangedCallback(DependencyObject obj,
            DependencyPropertyChangedEventArgs args)
        {
            NumericUpDown ctl = (NumericUpDown)obj;
            int newValue = (int)args.NewValue;

            // Call UpdateStates because the Value might have caused the
            // control to change ValueStates.
            ctl.UpdateStates(true);

            // Call OnValueChanged to raise the ValueChanged event.
            ctl.OnValueChanged(
                new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
                    newValue));
        }

        public static readonly RoutedEvent ValueChangedEvent =
            EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                          typeof(ValueChangedEventHandler), typeof(NumericUpDown));

        public event ValueChangedEventHandler ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        protected virtual void OnValueChanged(ValueChangedEventArgs e)
        {
            // Raise the ValueChanged event so applications can be alerted
            // when Value changes.
            RaiseEvent(e);
        }

        private void UpdateStates(bool useTransitions)
        {
            if (Value >= 0)
            {
                VisualStateManager.GoToState(this, "Positive", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Negative", useTransitions);
            }

            if (IsFocused)
            {
                VisualStateManager.GoToState(this, "Focused", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Unfocused", useTransitions);
            }
        }

        public override void OnApplyTemplate()
        {
            UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
            DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
            //TextElement = GetTemplateChild("TextBlock") as TextBlock;

            UpdateStates(false);
        }

        private RepeatButton downButtonElement;

        private RepeatButton DownButtonElement
        {
            get
            {
                return downButtonElement;
            }

            set
            {
                if (downButtonElement != null)
                {
                    downButtonElement.Click -=
                        new RoutedEventHandler(downButtonElement_Click);
                }
                downButtonElement = value;

                if (downButtonElement != null)
                {
                    downButtonElement.Click +=
                        new RoutedEventHandler(downButtonElement_Click);
                }
            }
        }

        void downButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value--;
        }

        private RepeatButton upButtonElement;

        private RepeatButton UpButtonElement
        {
            get
            {
                return upButtonElement;
            }

            set
            {
                if (upButtonElement != null)
                {
                    upButtonElement.Click -=
                        new RoutedEventHandler(upButtonElement_Click);
                }
                upButtonElement = value;

                if (upButtonElement != null)
                {
                    upButtonElement.Click +=
                        new RoutedEventHandler(upButtonElement_Click);
                }
            }
        }

        void upButtonElement_Click(object sender, RoutedEventArgs e)
        {
            Value++;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            Focus();
        }


        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            UpdateStates(true);
        }

        protected override void OnLostFocus(RoutedEventArgs e)
        {
            base.OnLostFocus(e);
            UpdateStates(true);
        }
    }

    public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);

    public class ValueChangedEventArgs : RoutedEventArgs
    {
        private int _value;

        public ValueChangedEventArgs(RoutedEvent id, int num)
        {
            _value = num;
            RoutedEvent = id;
        }

        public int Value
        {
            get { return _value; }
        }
    }
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media

<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        Me.IsTabStop = True
    End Sub

    Public Shared ReadOnly ValueProperty As DependencyProperty =
        DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
                          New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))

    Public Property Value() As Integer

        Get
            Return CInt(GetValue(ValueProperty))
        End Get

        Set(ByVal value As Integer)

            SetValue(ValueProperty, value)
        End Set
    End Property

    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
                                            ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = CInt(args.NewValue)

        ' Call UpdateStates because the Value might have caused the
        ' control to change ValueStates.
        ctl.UpdateStates(True)

        ' Call OnValueChanged to raise the ValueChanged event.
        ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
    End Sub

    Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
                                         GetType(ValueChangedEventHandler), GetType(NumericUpDown))

    Public Custom Event ValueChanged As ValueChangedEventHandler

        AddHandler(ByVal value As ValueChangedEventHandler)
            Me.AddHandler(ValueChangedEvent, value)
        End AddHandler

        RemoveHandler(ByVal value As ValueChangedEventHandler)
            Me.RemoveHandler(ValueChangedEvent, value)
        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Me.RaiseEvent(e)
        End RaiseEvent

    End Event


    Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
        ' Raise the ValueChanged event so applications can be alerted
        ' when Value changes.
        MyBase.RaiseEvent(e)
    End Sub


#Region "NUDCode"
    Private Sub UpdateStates(ByVal useTransitions As Boolean)

        If Value >= 0 Then
            VisualStateManager.GoToState(Me, "Positive", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Negative", useTransitions)
        End If

        If IsFocused Then
            VisualStateManager.GoToState(Me, "Focused", useTransitions)
        Else
            VisualStateManager.GoToState(Me, "Unfocused", useTransitions)

        End If
    End Sub

    Public Overloads Overrides Sub OnApplyTemplate()

        UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
        DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)

        UpdateStates(False)
    End Sub

    Private m_downButtonElement As RepeatButton

    Private Property DownButtonElement() As RepeatButton
        Get
            Return m_downButtonElement
        End Get

        Set(ByVal value As RepeatButton)

            If m_downButtonElement IsNot Nothing Then
                RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
            m_downButtonElement = value

            If m_downButtonElement IsNot Nothing Then
                AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
            End If
        End Set
    End Property

    Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value -= 1
    End Sub

    Private m_upButtonElement As RepeatButton

    Private Property UpButtonElement() As RepeatButton
        Get
            Return m_upButtonElement
        End Get

        Set(ByVal value As RepeatButton)
            If m_upButtonElement IsNot Nothing Then
                RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
            m_upButtonElement = value

            If m_upButtonElement IsNot Nothing Then
                AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
            End If
        End Set
    End Property

    Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Value += 1
    End Sub

    Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
        MyBase.OnMouseLeftButtonDown(e)
        Focus()
    End Sub


    Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
        MyBase.OnGotFocus(e)
        UpdateStates(True)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
        MyBase.OnLostFocus(e)
        UpdateStates(True)
    End Sub
#End Region
End Class


Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
                                             ByVal e As ValueChangedEventArgs)

Public Class ValueChangedEventArgs
    Inherits RoutedEventArgs

    Public Sub New(ByVal id As RoutedEvent,
                   ByVal num As Integer)

        Value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
End Class

См. также