Een controle-element maken met een aanpasbaar uiterlijk

Windows Presentation Foundation (WPF) biedt u de mogelijkheid om een controle te maken waarvan het uiterlijk kan worden aangepast. U kunt bijvoorbeeld het uiterlijk van een CheckBox wijzigen buiten de instellingseigenschappen door een nieuwe ControlTemplatete maken. In de volgende afbeelding ziet u een CheckBox die gebruikmaakt van een standaard-ControlTemplate en een CheckBox die gebruikmaakt van een aangepaste ControlTemplate.

een selectievakje met de standaardsjabloon voor besturingselementen. Een selectievakje dat gebruikmaakt van de standaardsjabloon voor besturingselementen

een selectievakje met een aangepaste besturingselementsjabloon. Een selectievakje dat gebruikmaakt van een aangepaste besturingselementsjabloon

Als u het onderdelen- en statusmodel volgt wanneer u een besturingselement maakt, kan het uiterlijk van uw besturingselement worden aangepast. Designer-hulpprogramma's zoals Blend voor Visual Studio ondersteunen het model onderdelen en statussen, dus wanneer u dit model volgt, kunt u het besturingselement aanpassen in deze typen toepassingen. In dit onderwerp worden de onderdelen en statusmodellen besproken en wordt uitgelegd hoe u dit kunt volgen wanneer u uw eigen controle maakt. In dit onderwerp wordt een voorbeeld gebruikt van een aangepast besturingselement, NumericUpDown, om de filosofie van dit model te illustreren. Het besturingselement NumericUpDown geeft een numerieke waarde weer, die een gebruiker kan vergroten of verkleinen door op de knoppen van het besturingselement te klikken. In de volgende afbeelding ziet u het besturingselement NumericUpDown dat in dit onderwerp wordt besproken.

aangepast NumericUpDown-besturingselement. een aangepast NumericUpDown-besturingselement

Dit onderwerp bevat de volgende secties:

Vereiste voorwaarden

In dit onderwerp wordt ervan uitgegaan dat u weet hoe u een nieuwe ControlTemplate maakt voor een bestaand besturingselement, bekend bent met wat de elementen in een besturingscontract zijn en dat u de concepten begrijpt die worden besproken in Een sjabloon maken voor een besturingselement.

Opmerking

Als u een besturingselement wilt maken dat het uiterlijk ervan kan aanpassen, moet u een besturingselement maken dat wordt overgenomen van de Control klasse of een van de subklassen die niet UserControlzijn. Een besturingselement dat overneemt van UserControl is een besturingselement dat snel kan worden gemaakt, maar geen ControlTemplate gebruikt en u kunt het uiterlijk ervan niet aanpassen.

Onderdelen- en statusmodel

Het onderdelen- en statusmodel geeft aan hoe de visuele structuur en het visuele gedrag van een besturingselement moeten worden gedefinieerd. Als u het model onderdelen en statussen wilt volgen, moet u het volgende doen:

  • Definieer de visuele structuur en het visuele gedrag in de ControlTemplate van een besturingselement.

  • Volg bepaalde aanbevolen procedures wanneer de logica van uw besturingselement communiceert met onderdelen van de besturingssjabloon.

  • Geef een beheercontract op om te bepalen wat moet worden opgenomen in de ControlTemplate.

Wanneer u de visuele structuur en het visuele gedrag in het ControlTemplate van een besturingselement definieert, kunnen auteurs van toepassingen de visuele structuur en het visuele gedrag van uw besturingselement wijzigen door een nieuwe ControlTemplate te maken in plaats van code te schrijven. U moet een controlecontract opgeven waarmee auteurs van toepassingen kunnen zien welke FrameworkElement objecten en statussen moeten worden gedefinieerd in de ControlTemplate. U moet enkele aanbevolen praktijken volgen wanneer u interageert met de onderdelen in de ControlTemplate, zodat uw systeem een onvolledige ControlTemplategoed afhandelt. Als u deze drie principes volgt, kunnen auteurs van toepassingen net zo eenvoudig een ControlTemplate voor uw besturingselement maken als voor de besturingselementen die bij WPF worden geleverd. In de volgende sectie wordt elk van deze aanbevelingen uitgebreid beschreven.

De visuele structuur en het visuele gedrag van een besturingselement definiëren in een ControlTemplate

Wanneer u uw aangepaste besturingselement maakt met behulp van het onderdelen- en statusmodel, definieert u de visuele structuur en het visuele gedrag van het besturingselement in de ControlTemplate in plaats van in de logica. De visuele structuur van een besturingselement is het samengestelde van FrameworkElement objecten waaruit het besturingselement bestaat. Het visuele gedrag is de manier waarop het besturingselement wordt weergegeven wanneer het zich in een bepaalde status bevindt. Voor meer informatie over het maken van een ControlTemplate die de visuele structuur en het visuele gedrag van een besturingselement aangeeft, zie Een sjabloon maken voor een besturingselement.

In het voorbeeld van besturingselement NumericUpDown omvat de visuele structuur twee RepeatButton-besturingselementen en een TextBlock. Als u deze besturingselementen toevoegt in de code van het NumericUpDown besturingselement- in de constructor, bijvoorbeeld, zijn de posities van deze besturingselementen onveranderbaar. In plaats van de visuele structuur en het visuele gedrag van het besturingselement in de code te definiëren, moet u het definiëren in de ControlTemplate. Vervolgens kan een toepassingsontwikkelaar de positie van de knoppen en TextBlock aanpassen en opgeven welk gedrag zich voordoet wanneer Value negatief is omdat de ControlTemplate kan worden vervangen.

In het volgende voorbeeld ziet u de visuele structuur van het besturingselement NumericUpDown, dat een RepeatButton bevat om Valuete verhogen, een RepeatButton om Valuete verlagen en een TextBlock om Valueweer te geven.

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

Een visueel gedrag van het besturingselement NumericUpDown is dat de waarde zich in een rood lettertype bevindt als deze negatief is. Als u de Foreground van de TextBlock in code wijzigt wanneer de Value negatief is, wordt in de NumericUpDown altijd een rode negatieve waarde weergegeven. U geeft het visuele gedrag van het besturingselement in de ControlTemplate op door VisualState objecten toe te voegen aan de ControlTemplate. In het volgende voorbeeld ziet u de VisualState objecten voor de statussen Positive en Negative. Positive en Negative sluiten elkaar wederzijds uit (het besturingselement bevindt zich altijd in precies één van de twee), dus het voorbeeld plaatst de VisualState objecten in één VisualStateGroup. Wanneer het besturingselement de status van de Negative bereikt, wordt de Foreground van de TextBlock rood. Wanneer het besturingselement de status Positive heeft, keert de Foreground terug naar de oorspronkelijke waarde. Het definiëren van VisualState objecten in een ControlTemplate wordt verder besproken in Een sjabloon maken voor een besturingselement.

Opmerking

Zorg ervoor dat u de gekoppelde eigenschap VisualStateManager.VisualStateGroups instelt op de root FrameworkElement van de 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>

Onderdelen van de ControlTemplate gebruiken in Code

Een ControlTemplate auteur kan FrameworkElement of VisualState objecten weglaten, opzettelijk of per ongeluk, maar de logica van uw besturingselement heeft deze onderdelen mogelijk nodig om goed te functioneren. Het onderdelen- en statusmodel geeft aan dat uw besturingselement resistent moet zijn tegen een ControlTemplate dat FrameworkElement- of VisualState-objecten mist. Uw controle-element mag niet een uitzondering geven of een fout melden als een FrameworkElement, VisualStateof VisualStateGroup ontbreekt in de ControlTemplate. In deze sectie worden de aanbevolen procedures beschreven voor interactie met FrameworkElement objecten en het beheren van statussen.

Verwachte ontbrekende FrameworkElement-objecten

Wanneer u FrameworkElement objecten in de ControlTemplatedefinieert, moet de logica van uw besturingselement mogelijk met een aantal objecten werken. Het besturingselement NumericUpDown abonneert zich bijvoorbeeld op de Click gebeurtenis van de knoppen om Value te verhogen of te verlagen en stelt de eigenschap Text van de TextBlock in op Value. Als een aangepaste ControlTemplate de TextBlock of knoppen weglaat, is het acceptabel dat het besturingselement een deel van de functionaliteit verliest, maar u moet er zeker van zijn dat uw besturingselement geen fout veroorzaakt. Als een ControlTemplate bijvoorbeeld niet de knoppen bevat om Valuete wijzigen, verliest de NumericUpDown die functionaliteit, maar een toepassing die gebruikmaakt van de ControlTemplate blijft worden uitgevoerd.

De volgende procedures zorgen ervoor dat uw besturingselement correct reageert op ontbrekende FrameworkElement objecten:

  1. Stel het kenmerk x:Name in voor elke FrameworkElement waarnaar u in code moet verwijzen.

  2. Definieer privé-eigenschappen voor elke FrameworkElement waarmee u moet communiceren.

  3. Abonneer u op en meld u af voor alle gebeurtenissen die uw besturingselement verwerkt in de set-accessor van de FrameworkElement-eigenschap.

  4. Stel de FrameworkElement eigenschappen in die u in stap 2 hebt gedefinieerd in de methode OnApplyTemplate. Dit is het vroegste moment waarop de FrameworkElement in de ControlTemplate beschikbaar is voor de besturing. Gebruik de x:Name van de FrameworkElement om de ControlTemplateop te halen.

  5. Controleer of de FrameworkElement niet null is voordat u toegang tot de leden ervan krijgt. Als het nullis, meldt u geen fout.

In de volgende voorbeelden ziet u hoe het besturingselement NumericUpDown communiceert met FrameworkElement objecten in overeenstemming met de aanbevelingen in de voorgaande lijst.

In het voorbeeld dat de visuele structuur van het besturingselement NumericUpDown in de ControlTemplatedefinieert, heeft de RepeatButton die Value verhoogt, zijn kenmerk x:Name ingesteld op UpButton. In het volgende voorbeeld wordt een eigenschap met de naam UpButtonElement gedeclareerd die de RepeatButton vertegenwoordigt die in de ControlTemplateis gedeclareerd. De set-accessor meldt zich eerst af voor het Click-evenement van de knop als UpDownElement niet nullis, stelt dan de eigenschap in, en abonneert zich vervolgens op het Click-evenement. Er is ook een eigenschap gedefinieerd, maar hier niet weergegeven, voor de andere RepeatButton, genaamd 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

Het volgende voorbeeld toont de OnApplyTemplate voor het besturingselement NumericUpDown. In het voorbeeld wordt de methode GetTemplateChild gebruikt om de FrameworkElement objecten op te halen uit de ControlTemplate. U ziet dat in het voorbeeld wordt beschermd tegen gevallen waarin GetTemplateChild een FrameworkElement vindt met de opgegeven naam die niet van het verwachte type is. Het is ook een best practice om elementen met de opgegeven x:Name te negeren, maar die van het verkeerde type zijn.

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

Door de praktijken te volgen die in de vorige voorbeelden worden getoond, zorgt u ervoor dat uw besturingselement blijft draaien in het geval dat de ControlTemplate een FrameworkElementmist.

VisualStateManager gebruiken om statussen te beheren

De VisualStateManager houdt de statussen van een besturingselement bij en voert de logica uit die nodig is om tussen statussen te schakelen. Wanneer u VisualState objecten aan de ControlTemplatetoevoegt, plaatst u ze in een VisualStateGroup en verbindt u de VisualStateGroup met de gekoppelde eigenschap VisualStateManager.VisualStateGroups, zodat de VisualStateManager er toegang toe heeft.

In het volgende voorbeeld wordt het vorige voorbeeld herhaald waarin de VisualState objecten worden weergegeven die overeenkomen met de Positive en Negative statussen van het besturingselement. De Storyboard in de NegativeVisualState verandert de Foreground van de TextBlock rood. Wanneer het NumericUpDown besturingselement de status Negative heeft, begint het storyboard met de status Negative. Vervolgens stopt de Storyboard in de status Negative wanneer het besturingselement terugkeert naar de status Positive. De PositiveVisualState hoeft geen Storyboard te bevatten, omdat wanneer de Storyboard voor de Negative stopt, de Foreground terugkeert naar de oorspronkelijke kleur.

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

Houd er rekening mee dat de TextBlock een naam krijgt, maar de TextBlock zich niet in het beheercontract bevindt voor NumericUpDown omdat de logica van het besturingselement nooit verwijst naar de TextBlock. Elementen waarnaar wordt verwezen in de ControlTemplate hebben namen, maar hoeven geen deel uit te maken van het besturingscontract, omdat een nieuwe ControlTemplate voor het besturingselement mogelijk niet hoeft te verwijzen naar dat element. Iemand die bijvoorbeeld een nieuwe ControlTemplate voor NumericUpDown maakt, kan besluiten niet aan te geven dat Value negatief is door de Foregroundte wijzigen. In dat geval verwijzen noch de code noch de ControlTemplate bij naam naar de TextBlock.

De logica van het besturingselement is verantwoordelijk voor het wijzigen van de status van het besturingselement. In het volgende voorbeeld ziet u dat het NumericUpDown besturingselement de GoToState methode aanroept om naar de status Positive te gaan wanneer Value 0 of hoger is en dat de status Negative wanneer Value kleiner is dan 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

De methode GoToState voert de logica uit die nodig is om de storyboards op de juiste manier te starten en te stoppen. Wanneer een besturingselement GoToState aanroept om de status te wijzigen, doet de VisualStateManager het volgende:

  • Als het besturingselement naar VisualState gaat en Storyboardheeft, begint het storyboard. Als de VisualState waaruit het besturingselement afkomstig is een Storyboardheeft, eindigt het storyboard.

  • Als het besturingselement zich al in de opgegeven status bevindt, handelt GoToState niet en retourneert het true.

  • Als de opgegeven status niet bestaat in de ControlTemplate van control, voert GoToState geen actie uit en wordt falsegeretourneerd.

Aanbevolen procedures voor het werken met VisualStateManager

U wordt aangeraden de volgende handelingen uit te voeren om de statussen van uw controle te behouden:

  • Gebruik eigenschappen om de status bij te houden.

  • Maak een helpermethode voor de overgang tussen statussen.

Het besturingselement NumericUpDown gebruikt de eigenschap Value om bij te houden of het de status Positive of Negative heeft. Het besturingselement NumericUpDown definieert ook de statussen Focused en UnFocused, waarmee de eigenschap IsFocused wordt bijgehouden. Als u statussen gebruikt die niet van nature overeenkomen met een eigenschap van het besturingselement, kunt u een privé-eigenschap definiëren om de status bij te houden.

Eén methode die alle statussen bijwerkt, centraliseert aanroepen naar de VisualStateManager en houdt uw code beheersbaar. In het volgende voorbeeld wordt de helperfunctie van het NumericUpDown besturingselement, UpdateStates, getoond. Wanneer Value groter is dan of gelijk is aan 0, heeft de Control de status Positive. Wanneer Value kleiner is dan 0, heeft het besturingselement de status Negative. Wanneer IsFocused is true, heeft het besturingselement de status Focused; anders bevindt het zich in de status Unfocused. Het besturingselement kan UpdateStates aanroepen wanneer dit zijn toestand moet wijzigen, ongeacht welke toestand verandert.

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

Als u een statusnaam doorgeeft aan GoToState wanneer het besturingselement al in die toestand is, doet GoToState niets, en hoeft u niet de huidige toestand van het besturingselement te controleren. Als Value bijvoorbeeld van het ene negatieve getal naar het andere gaat, wordt het storyboard voor de status Negative niet onderbroken en ziet de gebruiker geen wijziging in het bedieningselement.

De VisualStateManager gebruikt VisualStateGroup objecten om te bepalen welke toestand moet worden verlaten wanneer u GoToStateaanroept. Het besturingselement bevindt zich altijd in één status voor elke VisualStateGroup die is gedefinieerd in de ControlTemplate en verlaat alleen een status wanneer het naar een andere status gaat binnen dezelfde VisualStateGroup. De ControlTemplate van het besturingselement NumericUpDown definieert bijvoorbeeld de Positive en NegativeVisualState objecten in één VisualStateGroup en de Focused en UnfocusedVisualState objecten in een andere. (U ziet de Focused en UnfocusedVisualState gedefinieerd in de sectie Volledig voorbeeld in dit onderwerp, Wanneer de controle van de Positive status naar de Negative status gaat, of omgekeerd, blijft de controle in de status Focused of Unfocused.)

Er zijn drie typische plaatsen waar de status van een besturingselement kan veranderen:

  • Wanneer de ControlTemplate wordt toegepast op de Control.

  • Wanneer een eigenschap wordt gewijzigd.

  • Wanneer er een gebeurtenis optreedt.

In de volgende voorbeelden ziet u hoe u in deze gevallen de status van het NumericUpDown besturingselement bijwerkt.

U moet de status van het besturingselement in de OnApplyTemplate methode bijwerken, zodat het besturingselement de juiste status heeft wanneer de ControlTemplate wordt toegepast. In het volgende voorbeeld wordt UpdateStates aan OnApplyTemplate aanroepen om ervoor te zorgen dat het besturingselement de juiste statussen heeft. Stel dat u een NumericUpDown besturingselement maakt en de bijbehorende Foreground vervolgens instelt op groen en Value op -5. Als u UpdateStates niet aanroept wanneer de ControlTemplate wordt toegepast op het besturingselement NumericUpDown, bevindt het besturingselement zich niet in de status Negative en is de waarde groen in plaats van rood. U moet UpdateStates aanroepen om het besturingselement in de status Negative te plaatsen.

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

U moet vaak de statussen van een besturingselement bijwerken wanneer een eigenschap wordt gewijzigd. In het volgende voorbeeld ziet u de hele ValueChangedCallback methode. Omdat ValueChangedCallback wordt aangeroepen wanneer Value verandert, roept de methode UpdateStates aan voor het geval Value gewijzigd van positief in negatief of omgekeerd. Het is acceptabel om UpdateStates aan te roepen wanneer Value verandert maar positief of negatief blijft, omdat de besturing in dat geval geen statussen verandert.

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

Mogelijk moet u ook statussen bijwerken wanneer een gebeurtenis plaatsvindt. Het volgende voorbeeld laat zien dat de NumericUpDownUpdateStates aanroept op de Control om de GotFocus gebeurtenis af te handelen.

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

De VisualStateManager helpt u bij het beheren van de toestanden van het bedieningselement. Door de VisualStateManagerte gebruiken, zorgt u ervoor dat de overgang tussen statussen correct verloopt. Als u de aanbevelingen volgt die in deze sectie worden beschreven voor het werken met de VisualStateManager, blijft de code van uw besturingselement leesbaar en onderhoudbaar.

Het verschaffen van het controlecontract

U geeft een beheercontract op, zodat ControlTemplate auteurs weten wat ze in de sjabloon moeten plaatsen. Een beheercontract heeft drie elementen:

  • De visuele elementen die door de logica van het besturingselement worden gebruikt.

  • De statussen van het besturingselement en de groep waartoe elke status behoort.

  • De openbare eigenschappen die visueel van invloed zijn op het besturingselement.

Iemand die een nieuwe ControlTemplate maakt, moet weten welke FrameworkElement-objecten de logica van het besturingselement gebruikt, wat het type van elk object is en wat de naam ervan is. Een ControlTemplate auteur moet ook de naam kennen van elke mogelijke toestand waarin het besturingselement zich kan bevinden, en welke toestand VisualStateGroup is.

Als u terugkeert naar het NumericUpDown voorbeeld, verwacht het besturingselement dat de ControlTemplate de volgende FrameworkElement objecten heeft:

Het besturingselement kan de volgende statussen hebben:

Als u wilt opgeven welke FrameworkElement objecten het besturingselement verwacht, gebruikt u de TemplatePartAttribute, waarmee de naam en het type van de verwachte elementen worden opgegeven. Als u de mogelijke statussen van een besturingselement wilt opgeven, gebruikt u de TemplateVisualStateAttribute, waarmee de naam van de status wordt opgegeven en tot welke VisualStateGroup het behoort. Plaats de TemplatePartAttribute en TemplateVisualStateAttribute op de klassedefinitie van het besturingselement.

Alle openbare eigenschappen die van invloed zijn op het uiterlijk van uw besturingselement, maken ook deel uit van het beheercontract.

In het volgende voorbeeld wordt het FrameworkElement-object en de statussen voor het besturingselement NumericUpDown opgegeven.

[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

Volledig voorbeeld

Het volgende voorbeeld is het hele ControlTemplate voor het besturingselement 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>

In het volgende voorbeeld ziet u de logica voor de 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

Zie ook