Skapa en kontroll som har ett anpassningsbart utseende

Med Windows Presentation Foundation (WPF) kan du skapa en kontroll vars utseende kan anpassas. Du kan till exempel ändra utseendet på en CheckBox utöver vad inställningsegenskaper kommer att göra genom att skapa en ny ControlTemplate. Följande bild visar en CheckBox som använder en standard ControlTemplate och en CheckBox som använder en anpassad ControlTemplate.

En kryssruta med standardkontrollmallen. En kryssruta som använder standardkontrollmallen

En kryssruta med en anpassad kontrollmall. En kryssruta som använder en anpassad kontrollmall

Om du följer modellen för delar och tillstånd när du skapar en kontroll blir kontrollens utseende anpassningsbart. Designerverktyg som Blend för Visual Studio stöder modellen för delar och tillstånd, så när du följer den här modellen blir kontrollen anpassningsbar i dessa typer av program. I det här avsnittet beskrivs modellen för delar och tillstånd och hur du följer den när du skapar en egen kontroll. Det här avsnittet använder ett exempel på en anpassad kontroll, NumericUpDown, för att illustrera modellens filosofi. Kontrollen NumericUpDown visar ett numeriskt värde som en användare kan öka eller minska genom att klicka på kontrollens knappar. Följande bild visar kontrollen NumericUpDown som beskrivs i det här avsnittet.

Anpassad kontroll för NumericUpDown. En anpassad NumericUpDown-kontroll

Det här avsnittet innehåller följande avsnitt:

Förutsättningar

Det här avsnittet förutsätter att du vet hur du skapar en ny ControlTemplate för en befintlig kontroll, är bekant med vad elementen i ett kontrollkontrakt är och förstår de begrepp som beskrivs i Skapa en mall för en kontroll.

Anmärkning

Om du vill skapa en kontroll som kan anpassa dess utseende måste du skapa en kontroll som ärver från Control klassen eller någon av dess underklasser förutom UserControl. En kontroll som ärver från UserControl är en kontroll som snabbt kan skapas, men den använder inte en ControlTemplate och du kan inte anpassa dess utseende.

Modell för delar och tillstånd

Modellen för delar och tillstånd anger hur du definierar den visuella strukturen och det visuella beteendet för en kontroll. Om du vill följa modellen för delar och tillstånd bör du göra följande:

  • Definiera den visuella strukturen och det visuella beteendet i ControlTemplate en kontroll.

  • Följ vissa metodtips när kontrollens logik interagerar med delar av kontrollmallen.

  • Ange ett kontrollkontrakt för att ange vad som ska ingå i ControlTemplate.

När du definierar den visuella strukturen och det visuella beteendet i ControlTemplate en kontroll kan programförfattare ändra kontrollens visuella struktur och visuella beteende genom att skapa en ny ControlTemplate i stället för att skriva kod. Du måste ange ett kontrollkontrakt som talar om för programförfattare vilka objekt och tillstånd som FrameworkElement ska definieras i ControlTemplate. Du bör följa några bästa praxis när du interagerar med delarna i ControlTemplate så att din kontroll korrekt hanterar en ofullständig ControlTemplate. Om du följer dessa tre principer kan programförfattarna skapa en ControlTemplate för din kontroll lika enkelt som de kan för de kontroller som levereras med WPF. I följande avsnitt beskrivs var och en av dessa rekommendationer i detalj.

Definiera den visuella strukturen och det visuella beteendet för en kontroll i ett ControlTemplate

När du skapar din anpassade kontroll med hjälp av modellen för delar och tillstånd definierar du kontrollens visuella struktur och visuella beteende i stället ControlTemplate för i dess logik. Den visuella strukturen för en kontroll är sammansatt av FrameworkElement objekt som utgör kontrollen. Det visuella beteendet är hur kontrollen visas när den är i ett visst tillstånd. Mer information om hur du skapar en ControlTemplate som anger kontrollens visuella struktur och visuella beteende finns i Skapa en mall för en kontroll.

I exemplet med NumericUpDown kontrollen innehåller den visuella strukturen två RepeatButton kontroller och en TextBlock. Om du lägger till dessa kontroller i koden NumericUpDown för kontrollen – i konstruktorn, till exempel – skulle positionerna för dessa kontroller vara oföränderliga. I stället för att definiera kontrollens visuella struktur och visuella beteende i koden bör du definiera den i ControlTemplate. Sedan kan en programutvecklare anpassa knapparnas position och TextBlock ange vilket beteende som inträffar när Value det är negativt eftersom det ControlTemplate kan ersättas.

I följande exempel visas kontrollens NumericUpDown visuella struktur, som innehåller en RepeatButton för att öka Value, en RepeatButton för att minska Valueoch en TextBlock för att visa 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>

Ett visuellt beteende för NumericUpDown kontrollen är att värdet är i ett rött teckensnitt om det är negativt. Om du ändrar ForegroundTextBlock i koden när Value är negativt, kommer NumericUpDown alltid att visa ett rött negativt värde. Du anger kontrollens visuella beteende genom att lägga till ControlTemplate objekt i VisualState med hjälp av ControlTemplate. I följande exempel visas objekten VisualState för tillstånden Positive och Negative . Positive och Negative är ömsesidigt uteslutande (kontrollen finns alltid i exakt en av de två), så exemplet placerar objekten VisualState i en enda VisualStateGroup. När kontrollen går in i Negative-tillståndet, blir ForegroundTextBlock röd. När kontrollen är i tillståndet Positive återgår den Foreground till sitt ursprungliga värde. Definiera VisualState objekt i en ControlTemplate beskrivs ytterligare i Skapa en mall för en kontroll.

Anmärkning

Se till att ange den VisualStateManager.VisualStateGroups anslutna egenskapen i roten FrameworkElement för 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>

Använda delar av ControlTemplate i Kod

En ControlTemplate författare kan utelämna FrameworkElement eller VisualState objekt, antingen avsiktligt eller av misstag, men kontrollens logik kan behöva dessa delar för att fungera korrekt. Modellen för delar och tillstånd anger att kontrollen ska vara motståndskraftig mot en brist på ControlTemplate eller FrameworkElement objekt. Kontrollen bör inte utlösa ett undantag eller rapportera ett fel om en FrameworkElement, VisualStateeller VisualStateGroup saknas i ControlTemplate. I det här avsnittet beskrivs de rekommenderade metoderna för att interagera med FrameworkElement objekt och hantera tillstånd.

Förutse saknade FrameworkElement-objekt

När du definierar FrameworkElement objekt i ControlTemplatekan kontrollens logik behöva interagera med några av dem. Till exempel prenumererar kontrollen NumericUpDown på knapparnas Click händelse för att öka eller minska Value och sätter egenskapen Text för TextBlock till Value. Om en anpassad ControlTemplate utelämnar TextBlock eller knappar, är det acceptabelt att kontrollen förlorar några av sina funktioner, men du bör vara säker på att kontrollen inte orsakar något fel. Om en ControlTemplate till exempel inte innehåller knapparna för att ändra Valueförlorar den NumericUpDown funktionen, men ett program som använder ControlTemplate kommer att fortsätta att köras.

Följande metoder säkerställer att kontrollen svarar korrekt på objekt som saknas FrameworkElement :

  1. Ange attributet x:Name för varje FrameworkElement som du behöver referera till i kod.

  2. Definiera privata egenskaper för var och en FrameworkElement som du behöver interagera med.

  3. Prenumerera och avsluta prenumeration på händelser som din kontroll hanterar i egenskapens FrameworkElement set-metod.

  4. Ange de FrameworkElement egenskaper som du definierade i steg 2 i OnApplyTemplate metoden. Detta är det tidigaste som FrameworkElement i ControlTemplate är tillgängligt för kontrollen. Använd x:Name för FrameworkElement för att hämta den från ControlTemplate.

  5. Kontrollera att FrameworkElement är inte null innan du kommer åt dess medlemmar. Om det är nullska du inte rapportera något fel.

Följande exempel visar hur NumericUpDown kontrollen interagerar med FrameworkElement objekt i enlighet med rekommendationerna i föregående lista.

I det exempel som definierar den visuella strukturen för kontrollen NumericUpDown i ControlTemplate, har RepeatButton som ökar Value sitt x:Name-attribut inställt på UpButton. I följande exempel deklareras en egenskap med namnet UpButtonElement som representerar RepeatButton den som deklareras i ControlTemplate. Accessorn set avbryter först prenumerationen på Click-händelsen om UpDownElement inte null, sedan anger den egenskapen och prenumererar sedan på Click-händelsen. Det finns också en definierad egenskap, men visas inte här, för den andra RepeatButton, med namnet 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

I följande exempel visas OnApplyTemplate för NumericUpDown kontrollen. I exemplet används GetTemplateChild-metoden för att få FrameworkElement-objekten från ControlTemplate. Observera att exemplet skyddar mot fall där GetTemplateChild hittar en FrameworkElement med det angivna namnet som inte är av den förväntade typen. Det är också en bra idé att ignorera element som har den angivna x:Name men är av fel typ.

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

Genom att följa de metoder som visas i föregående exempel ser du till att din kontroll fortsätter att köras när ControlTemplate saknar en FrameworkElement.

Använda VisualStateManager för att hantera tillstånd

VisualStateManager håller reda på tillstånden för en styrning och utför den logik som krävs för att övergå mellan tillstånd. När du lägger till VisualState objekt i ControlTemplate, lägger du till dem i en VisualStateGroup och tilldelar VisualStateGroup till den bifogade egenskapen VisualStateManager.VisualStateGroups så att VisualStateManager har åtkomst till dem.

I följande exempel upprepas det föregående exemplet som visar de VisualState objekt som motsvarar tillstånden Positive och Negative för kontrollen. Den Storyboard i NegativeVisualState gör ForegroundTextBlock röd. NumericUpDown När kontrollen är i tillståndet Negative börjar storyboarden i tillståndetNegative. Sedan upphör Storyboard i Negative-tillståndet när kontrollen återgår till Positive-tillståndet. Positive VisualState behöver inte innehålla Storyboard eftersom, när Storyboard för Negative stoppar, Foreground återgår till sin ursprungliga färg.

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

Observera att TextBlock har fått ett namn, men TextBlock inte finns i kontrollkontraktet för NumericUpDown eftersom kontrollens logik aldrig refererar till TextBlock. Elementen som refereras i ControlTemplate har namn, men behöver inte ingå i kontrollkontraktet eftersom en ny ControlTemplate för kontrollen kanske inte behöver referera till elementet. Till exempel kan någon som skapar en ny ControlTemplate för NumericUpDown välja att inte indikera att Value är negativ genom att ändra Foreground. I så fall refererar varken koden eller ControlTemplate referenserna till TextBlock vid namn.

Kontrollens logik ansvarar för att ändra kontrollens tillstånd. Följande exempel visar att NumericUpDown kontrollen anropar GoToState metoden för att gå in i Positive tillståndet när Value är 0 eller högre och tillståndet Negative när Value är mindre än 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

Metoden GoToState utför den logik som krävs för att starta och stoppa storyboards på rätt sätt. När en kontroll anropar GoToState för att ändra dess tillstånd gör VisualStateManager följande:

  • Om VisualState-kontrollen har en Storyboard, börjar storyboarden. Om kontrollen VisualState som den kommer från har en Storyboard, så slutar storyboarden.

  • Om kontrollen redan är i det angivna GoToState tillståndet vidtar du ingen åtgärd och returnerar true.

  • Om det angivna tillståndet inte finns i ControlTemplate i control, vidtar GoToState ingen åtgärd och returnerar false.

Metodtips för att arbeta med VisualStateManager

Vi rekommenderar att du gör följande för att upprätthålla kontrollens status:

  • Använd egenskaper för att spåra dess tillstånd.

  • Skapa en hjälpmetod för övergång mellan tillstånd.

Kontrollen NumericUpDown använder sin Value-egenskap för att avgöra om den är i tillståndet Positive eller Negative. Kontrollen NumericUpDown definierar också tillstånden Focused och UnFocused , som spårar IsFocused egenskapen. Om du använder tillstånd som inte naturligt motsvarar en egenskap för kontrollen kan du definiera en privat egenskap för att spåra tillståndet.

En enda metod som uppdaterar alla tillstånd centraliserar anrop till VisualStateManager och håller koden hanterbar. I följande exempel visas NumericUpDown kontrollens hjälpmetod, UpdateStates. När Value är större än eller lika med 0, Control är i tillståndet Positive . När Value är mindre än 0 är kontrollen i tillståndet Negative . När IsFocused är trueär kontrollen i tillståndet Focused , annars är den i tillståndet Unfocused . Kontrollen kan anropa UpdateStates när den behöver ändra sitt tillstånd, oavsett vilket tillstånd som ändras.

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

Om du skickar ett tillståndsnamn till GoToState när kontrollen redan är i det tillståndet, GoToState gör ingenting, så du behöver inte söka efter kontrollens aktuella tillstånd. Om Value till exempel ändras från ett negativt tal till ett annat negativt tal avbryts inte storyboarden för Negative tillståndet och användaren ser ingen ändring i kontrollen.

VisualStateManager Använder VisualStateGroup objekt för att avgöra vilket tillstånd som ska avslutas när du anropar GoToState. Kontrollen befinner sig alltid i ett tillstånd för varje VisualStateGroup som definieras i dess ControlTemplate och lämnar bara ett tillstånd när den går över till ett annat tillstånd från samma VisualStateGroup. Kontrollen ControlTemplate definierar till exempel objekten NumericUpDown och PositiveNegative i ett VisualState och objekten VisualStateGroup och FocusedUnfocused i ett annat. (Du kan se Focused och UnfocusedVisualState definieras i avsnittet Fullständigt exempel, i detta ämne. När kontrollen går från Positive-tillståndet till Negative-tillståndet, eller vice versa, förblir kontrollen i antingen Focused-tillståndet eller Unfocused-tillståndet.)

Det finns tre vanliga platser där tillståndet för en kontroll kan ändras:

I följande exempel visas hur du uppdaterar kontrollens NumericUpDown tillstånd i dessa fall.

Du bör uppdatera tillståndet för kontrollen i OnApplyTemplate metoden så att kontrollen visas i rätt tillstånd när den ControlTemplate tillämpas. Följande exempel anropar UpdateStates i OnApplyTemplate för att säkerställa att kontrollen är i rätt tillstånd. Anta till exempel att du skapar en NumericUpDown kontroll och sedan anger den Foreground till grön och Value till -5. Om du inte anropar UpdateStates när ControlTemplate tillämpas på NumericUpDown kontrollen är kontrollen inte i Negative tillståndet och värdet är grönt i stället för rött. Du måste anropa UpdateStates för att placera kontrollen i tillståndet 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

Du behöver ofta uppdatera statusar för en styrning när en egenskap ändras. I följande exempel visas hela ValueChangedCallback metoden. Eftersom ValueChangedCallback anropas när Value ändras, anropar metoden UpdateStates om Value ändras från positiv till negativ eller vice versa. Det är acceptabelt att anropa UpdateStates när Value ändras men förblir positiv eller negativ eftersom kontrollen i så fall inte kommer att ändra tillstånd.

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

Du kan också behöva uppdatera tillstånd när en händelse inträffar. Följande exempel visar att NumericUpDown anropar UpdateStatesControl för att hantera händelse av typen 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

Hjälper VisualStateManager dig att hantera kontrollens tillstånd. Genom att använda VisualStateManagerser du till att kontrollen övergår korrekt mellan tillstånden. Om du följer rekommendationerna som beskrivs i det här avsnittet för att arbeta med VisualStateManagerkommer kontrollens kod att förbli läsbar och underhållsbar.

Tillhandahålla kontrollkontraktet

Du anger ett kontrollkontrakt så att ControlTemplate författarna vet vad de ska lägga till i mallen. Ett kontrollkontrakt har tre element:

  • De visuella element som kontrollens logik använder.

  • Tillstånden för kontrollen och den grupp som varje tillstånd tillhör.

  • De offentliga egenskaper som visuellt påverkar kontrollen.

Någon som skapar en ny ControlTemplate behöver veta vilka FrameworkElement objekt kontrollens logik använder, vilken typ varje objekt är och vad dess namn är. En ControlTemplate författare måste också känna till namnet på varje möjligt tillstånd som kontrollen kan finnas i och vilket VisualStateGroup tillstånd som är i.

När du återgår till exemplet NumericUpDown, förväntar sig kontrollen ControlTemplate att ha följande objekt: FrameworkElement

Kontrollen kan vara i följande tillstånd:

Om du vill ange vilka FrameworkElement objekt som kontrollen förväntar sig använder TemplatePartAttributedu , som anger namnet och typen av förväntade element. För att ange de möjliga tillstånden för en kontroll använder du TemplateVisualStateAttribute, vilket anger tillståndets namn och vilken VisualStateGroup det tillhör. Placera TemplatePartAttribute och TemplateVisualStateAttribute på klassdefinitionen för kontrollen.

All offentlig egendom som påverkar kontrollens utseende är också en del av kontrollavtalet.

I följande exempel anges FrameworkElement objektet och tillstånden NumericUpDown för kontrollen.

[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

Fullständigt exempel

Följande exempel är hela ControlTemplate för kontrollen 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>

I följande exempel visas logiken för 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

Se även