Compartilhar via


Criando um controle com uma aparência personalizável

O Windows Presentation Foundation (WPF) oferece a capacidade de criar um controle cuja aparência pode ser personalizada. Por exemplo, você pode alterar a aparência de um além do que as propriedades de configuração farão criando um CheckBox novo ControlTemplate. A ilustração a seguir mostra um que usa um padrão ControlTemplate e um que usa um CheckBoxCheckBoxControlTemplatearquivo .

A checkbox with the default control template. Uma caixa de seleção que usa o modelo de controle padrão

A checkbox with a custom control template. Uma caixa de seleção que usa um modelo de controle personalizado

Se você seguir o modelo de partes e estados ao criar um controle, a aparência do controle será personalizável. Ferramentas de designer como o Blend para Visual Studio oferecem suporte ao modelo de partes e estados, portanto, quando você seguir esse modelo, seu controle será personalizável nesses tipos de aplicativos. Este tópico aborda o modelo de partes e estados e como segui-lo ao criar seu próprio controle. Este tópico usa um exemplo de um controle personalizado, NumericUpDown, para ilustrar a filosofia desse modelo. O controle NumericUpDown exibe um valor numérico, que um usuário pode aumentar ou diminuir clicando nos botões do controle. A ilustração a seguir mostra o controle NumericUpDown que é abordado neste tópico.

NumericUpDown custom control. Um controle NumericUpDown personalizado

Este tópico contém as seguintes seções:

Pré-requisitos

Este tópico pressupõe que você saiba como criar um novo ControlTemplate para um controle existente, esteja familiarizado com quais são os elementos em um contrato de controle e compreenda os conceitos discutidos em Criar um modelo para um controle.

Observação

Para criar um controle que pode ter sua aparência personalizada, você deve criar um controle que herda da Control classe ou uma de suas subclasses diferente de UserControl. Um controle que herda de UserControl é um controle que pode ser criado rapidamente, mas ele não usa um ControlTemplate e você não pode personalizar sua aparência.

Modelo de partes e estados

O modelo de partes e estados especifica como definir a estrutura e o comportamento visuais de um controle. Para seguir o modelo de partes e estados, faça o seguinte:

  • Definir a estrutura visual e o comportamento visual no ControlTemplate de um controle.

  • Siga algumas melhores práticas quando a lógica do controle interagir com partes do modelo de controle.

  • Forneça um contrato de controle para especificar o que deve ser incluído no ControlTemplate.

Quando você define a estrutura visual e o comportamento visual no de um controle, os ControlTemplate autores do aplicativo podem alterar a estrutura visual e o comportamento visual do seu controle criando um novo ControlTemplate em vez de escrever código. Você deve fornecer um contrato de controle que informe aos ControlTemplateautores do aplicativo quais FrameworkElement objetos e estados devem ser definidos no . Você deve seguir algumas práticas recomendadas ao interagir com as partes no ControlTemplate para que seu controle manipule corretamente um ControlTemplatearquivo . Se você seguir esses três princípios, os autores de aplicativos poderão criar um ControlTemplate para seu controle com a mesma facilidade possível para os controles fornecidos com o WPF. A próxima seção explica cada uma dessas recomendações detalhadamente.

Definindo a estrutura e o comportamento visuais de um controle em um ControlTemplate

Ao criar seu controle personalizado usando o modelo de partes e estados, você define a estrutura visual e o comportamento visual do controle em sua lógica, em vez de em sua ControlTemplate lógica. A estrutura visual de um controle é a composição de FrameworkElement objetos que compõem o controle. O comportamento visual é a aparência do controle quando ele está em determinado estado. Para obter mais informações sobre como criar um que especifica a estrutura visual e o comportamento visual de um controle, consulte Criar um modelo para um ControlTemplate controle.

No exemplo do NumericUpDown controle, a estrutura visual inclui dois RepeatButton controles e um TextBlockarquivo . Se você adicionar esses controles ao código do controle NumericUpDown – no construtor, por exemplo –, as posições desses controles não serão alteráveis. Em vez de definir a estrutura visual e o comportamento visual do controle em seu código, você deve defini-lo no ControlTemplate. Em seguida, um desenvolvedor de aplicativos para personalizar a posição dos botões e TextBlock especificar qual comportamento ocorre quando Value é negativo porque o ControlTemplate pode ser substituído.

O exemplo a seguir mostra a estrutura visual do NumericUpDown controle, que inclui um para aumentar Value, um para diminuir Valuee um TextBlockRepeatButtonRepeatButton para exibir .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>

Um comportamento visual do controle NumericUpDown é que o valor estará em uma fonte vermelha se for negativo. Se você alterar o ForegroundTextBlock do código in quando o for negativo, o ValueNumericUpDown sempre mostrará um valor negativo vermelho. Você especifica o comportamento visual do controle no ControlTemplate adicionando VisualState objetos ao ControlTemplate. O exemplo a seguir mostra os objetos para os VisualStatePositive estados e Negative . Positive e Negative são mutuamente exclusivos (o controle está sempre em exatamente um dos dois), então o exemplo coloca os VisualState objetos em um único VisualStateGroup. Quando o controle entra no Negative estado, o ForegroundTextBlock do fica vermelho. Quando o controle está no Positive estado, o Foreground retorna ao seu valor original. A definição VisualState de objetos em um é discutida em Criar um modelo para um ControlTemplate controle.

Observação

Certifique-se de definir a propriedade anexada VisualStateManager.VisualStateGroups na raiz FrameworkElement do 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>

Usando partes do ControlTemplate no código

Um ControlTemplate autor pode omitir FrameworkElement ou objetar, propositalmente ou VisualState por engano, mas a lógica do seu controle pode precisar dessas partes para funcionar corretamente. O modelo de partes e estados especifica que seu controle deve ser resiliente a um ControlTemplate objeto ou FrameworkElementVisualState ausente. Seu controle não deve lançar uma exceção ou relatar um erro se um FrameworkElement, VisualStateou VisualStateGroup estiver ausente do ControlTemplate. Esta seção descreve as práticas recomendadas para interagir com FrameworkElement objetos e gerenciar estados.

Prever objetos FrameworkElement ausentes

Quando você define FrameworkElement objetos no ControlTemplate, a lógica do controle pode precisar interagir com alguns deles. Por exemplo, o controle assina o NumericUpDown evento dos Click botões para aumentar ou diminuir Value e define a Text propriedade do TextBlock para Value. Se um personalizado ControlTemplate omitir os TextBlock botões ou, é aceitável que o controle perca parte de sua funcionalidade, mas você deve ter certeza de que seu controle não causa um erro. Por exemplo, se a não contiver os botões a serem alteradosValue, o perderá essa funcionalidade, mas um aplicativo que usa o ControlTemplateNumericUpDown continuará a ControlTemplate ser executado.

As práticas a seguir garantirão que seu controle responda corretamente aos objetos ausentes FrameworkElement :

  1. Defina o x:Name atributo para cada FrameworkElement um que você precisa referenciar no código.

  2. Defina propriedades privadas para cada FrameworkElement uma com a qual você precisa interagir.

  3. Assine e cancele a assinatura de quaisquer eventos que seu controle manipule FrameworkElement no acessador definido da propriedade.

  4. Defina as FrameworkElement propriedades que você definiu na etapa 2 no OnApplyTemplate método. Este é o mais cedo que o no está disponível para o FrameworkElementControlTemplate controle. Use o x:Name do para obtê-lo do ControlTemplateFrameworkElement .

  5. Verifique se o não é null antes de FrameworkElement acessar seus membros. Se ele for null, não relate um erro.

Os exemplos a seguir mostram como o NumericUpDown controle interage com objetos de acordo com FrameworkElement as recomendações na lista anterior.

No exemplo que define a estrutura visual do controle no ControlTemplate, o RepeatButton que aumenta Value tem seu x:Name atributo definido como UpButton.NumericUpDown O exemplo a seguir declara uma propriedade chamada UpButtonElement que representa o que é declarado RepeatButtonControlTemplateno . O set acessador primeiro cancela a inscrição no evento do Click botão, se UpDownElement não nullfor , então ele define a propriedade e, em seguida, ele se inscreve no Click evento. Há também uma propriedade definida, mas não mostrada aqui, para a outra RepeatButton, chamada 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

O exemplo a seguir mostra o para o OnApplyTemplateNumericUpDown controle. O exemplo usa o GetTemplateChild método para obter os FrameworkElement objetos do ControlTemplate. Observe que o exemplo protege contra casos GetTemplateChild em que encontra um FrameworkElement com o nome especificado que não é do tipo esperado. Também é uma melhor prática ignorar elementos que têm o x:Name especificado, mas que são do tipo incorreto.

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

Seguindo as práticas mostradas nos exemplos anteriores, você garante que seu controle continuará a ser executado quando o ControlTemplate estiver faltando um FrameworkElementarquivo .

Usar o VisualStateManager para gerenciar estados

O VisualStateManager mantém o controle dos estados de um controle e executa a lógica necessária para a transição entre estados. Ao adicionar VisualState objetos ao ControlTemplate, você os adiciona a um VisualStateGroup e adiciona o à propriedade anexada VisualStateManager.VisualStateGroups para que o VisualStateGroupVisualStateManager tenha acesso a eles.

O exemplo a seguir repete o exemplo anterior que mostra os VisualState objetos que correspondem aos Positive estados e Negative do controle. O Storyboard nas voltas NegativeVisualState o Foreground do TextBlock vermelho. Quando o controle NumericUpDown está no estado Negative, o storyboard no estado Negative é iniciado. Em seguida, o no Negative estado pára quando o Storyboard controle retorna ao Positive estado. O PositiveVisualState não precisa conter um Storyboard porque quando o para o pára, o ForegroundStoryboardNegative retorna à sua cor original.

<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>

Observe que o recebe um nome, mas o TextBlockTextBlock não está no contrato de controle para NumericUpDown porque a lógica do controle nunca faz referência ao TextBlock. Os elementos que são referenciados no ControlTemplate têm nomes, mas não precisam fazer parte do contrato de controle porque um novo ControlTemplate para o controle pode não precisar fazer referência a esse elemento. Por exemplo, alguém que cria um novo ControlTemplate para NumericUpDown pode decidir não indicar que Value é negativo alterando o Foreground. Nesse caso, nem o código nem as ControlTemplate referências ao TextBlock nome.

A lógica do controle é responsável por alterar o estado do controle. O exemplo a seguir mostra que o controle chama o método para ir para o estado quando é 0 ou maior e o NumericUpDownGoToStateNegativePositive estado quando ValueValue é menor que 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

O GoToState método executa a lógica necessária para iniciar e parar os storyboards adequadamente. Quando um controle chama GoToState para alterar seu estado, o faz o VisualStateManager seguinte:

  • Se o VisualState controle vai ter um Storyboard, o storyboard começa. Então, se o controle está vindo tem um Storyboard, o VisualState storyboard termina.

  • Se o controle já estiver no estado especificado, GoToState não executará nenhuma ação e retornará true.

  • Se o estado especificado não existir no , GoToState não executará nenhuma controlControlTemplate ação e retornará false.

Melhores práticas para trabalhar com o VisualStateManager

É recomendável que você faça o seguinte para manter os estados do controle:

  • Usar propriedades para controlar seu estado.

  • Criar um método auxiliar para fazer a transição entre estados.

O controlar NumericUpDown usa sua propriedade Value para controlar se ele está no estado Positive ou Negative. O NumericUpDown controle também define os Focused estados e UnFocused , que rastreia a IsFocused propriedade. Se você usar estados que naturalmente não correspondem a uma propriedade do controle, poderá definir uma propriedade particular para controlar o estado.

Um único método que atualiza todos os estados centraliza as chamadas para o VisualStateManager e mantém seu código gerenciável. O exemplo a seguir mostra o método auxiliar do controle NumericUpDown, UpdateStates. Quando Value é maior ou igual a 0, o Control está no Positive estado. Quando Value é menor que 0, o controle está no estado Negative. Quando IsFocused está, o controle está trueno estado, caso contrário, está no FocusedUnfocused estado. O controle pode chamar UpdateStates sempre que precisar alterar seu estado, independentemente de qual estado é alterado.

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

Se você passar um nome de estado para GoToState quando o controle já estiver nesse estado, não fará nada, portanto, GoToState não será necessário verificar o estado atual do controle. Por exemplo, se o Value for alterado de um número negativo para outro número negativo, o storyboard para o estado Negative não será interrompido e o usuário não verá uma alteração no controle.

O VisualStateManager usa VisualStateGroup objetos para determinar qual estado sair quando você chama GoToState. O controle está sempre em um estado para cada VisualStateGroup um que é definido em seu ControlTemplate e só sai de um estado quando ele vai para outro estado do mesmo VisualStateGroup. Por exemplo, o ControlTemplate do NumericUpDown controle define os objetos e em um VisualStateGroup e NegativeVisualState os FocusedPositive objetos em UnfocusedVisualState outro. (Você pode ver o e UnfocusedVisualState definido na seção Exemplo completo neste tópico Quando o controle passa do estado para o PositiveNegative estado, ou vice-versa, o Focused controle permanece no Focused estado ou Unfocused .

Há três locais típicos em que o estado de um controle pode ser alterado:

  • Quando o ControlTemplate é aplicado ao Control.

  • Quando uma propriedade é alterada.

  • Quando ocorre um evento.

Os exemplos a seguir demonstram a atualização do estado do controle NumericUpDown nesses casos.

Você deve atualizar o estado do controle no método para que o controle apareça no OnApplyTemplate estado correto quando o ControlTemplate é aplicado. O exemplo a OnApplyTemplate seguir chama UpdateStates para garantir que o controle esteja nos estados apropriados. Por exemplo, suponha que você crie um NumericUpDown controle e, em seguida, defina-o como verde e Value como -Foreground5. Se você não chamar UpdateStates quando o é aplicado ao NumericUpDown controle, o controle não está no Negative estado e o ControlTemplate valor é verde em vez de vermelho. Você deve chamar UpdateStates para colocar o controle no estado Negative.

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

Geralmente, é necessário atualizar os estados de um controle quando uma propriedade é alterada. O exemplo a seguir mostra todo o método ValueChangedCallback. Como ValueChangedCallback é chamado quando o Value é alterado, o método UpdateStates chamará caso Value seja alterado de positivo para negativo ou vice-versa. É aceitável chamar UpdateStates quando o Value é alterado, mas permanece positivo ou negativo; nesse caso, o controle não mudará de estado.

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

Talvez você também precise atualizar os estados quando ocorrer um evento. O exemplo a seguir mostra que as NumericUpDown chamadas UpdateStates no Control para manipular o GotFocus evento.

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

O VisualStateManager ajuda você a gerenciar os estados do controle. Usando o VisualStateManager, você garante que seu controle faça a transição correta entre estados. Se você seguir as recomendações descritas nesta seção para trabalhar com o , o VisualStateManagercódigo do seu controle permanecerá legível e de manutenção.

Fornecendo o contrato de controle

Você fornece um contrato de controle para que os autores saibam o que ControlTemplate colocar no modelo. Um contrato de controle contém três elementos:

  • Os elementos visuais usados pela lógica do controle.

  • Os estados do controle e o grupo ao qual cada estado pertence.

  • As propriedades públicas que afetam o controle visualmente.

Alguém que cria um novo ControlTemplate precisa saber quais FrameworkElement objetos a lógica do controle usa, que tipo é cada objeto e qual é seu nome. Um ControlTemplate autor também precisa saber o nome de cada estado possível em que o controle pode estar e em qual VisualStateGroup estado o está.

Voltando ao NumericUpDown exemplo, o controle espera que o ControlTemplate tenha os seguintes FrameworkElement objetos:

O controle pode estar nos seguintes estados:

Para especificar quais FrameworkElement objetos o controle espera, use o , que especifica o TemplatePartAttributenome e o tipo dos elementos esperados. Para especificar os estados possíveis de um controle, use o , que especifica o TemplateVisualStateAttributenome do estado e a qual VisualStateGroup ele pertence. Coloque o TemplatePartAttribute e TemplateVisualStateAttribute na definição de classe do controle.

Qualquer propriedade pública que afeta a aparência do controle também é uma parte do contrato de controle.

O exemplo a seguir especifica o objeto e os estados para o FrameworkElementNumericUpDown controle.

[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

Exemplo completo

O exemplo a seguir é o inteiro ControlTemplate para o NumericUpDown controle.

<!--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>

O exemplo a seguir mostra a lógica do 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

Confira também