Condividi tramite


Creazione di un controllo con un aspetto personalizzabile

Windows Presentation Foundation (WPF) consente di creare un controllo il cui aspetto può essere personalizzato. Ad esempio, è possibile modificare l'aspetto di un CheckBox oltre quello che consentono le impostazioni delle proprietà creando un nuovo ControlTemplate oggetto. Nella figura seguente viene illustrato un CheckBox oggetto che usa un valore predefinito ControlTemplate e un CheckBox oggetto che usa un oggetto personalizzato ControlTemplate.

Casella di controllo con il modello di controllo predefinito. Controllo CheckBox che usa il modello di controllo predefinito

Casella di controllo con un modello di controllo personalizzato. CheckBox che usa un modello di controllo personalizzato

Se si seguono le parti e il modello di stati quando si crea un controllo, l'aspetto del controllo sarà personalizzabile. Gli strumenti di progettazione, ad esempio Blend per Visual Studio, supportano le parti e il modello degli stati, quindi quando si segue questo modello il controllo sarà personalizzabile in questi tipi di applicazioni. In questo argomento vengono illustrati i modelli di parti e stati e come seguirlo quando si crea un controllo personalizzato. In questo argomento viene usato un esempio di controllo personalizzato, NumericUpDown, per illustrare la filosofia di questo modello. Il NumericUpDown controllo visualizza un valore numerico, che un utente può aumentare o diminuire facendo clic sui pulsanti del controllo. Nella figura seguente viene illustrato il NumericUpDown controllo descritto in questo argomento.

Controllo personalizzato NumericUpDown. Controllo NumericUpDown personalizzato

Questo argomento contiene le sezioni seguenti:

Prerequisiti

In questo argomento si presuppone che si sappia come creare un nuovo ControlTemplate per un controllo esistente, conoscere gli elementi di un contratto di controllo e comprendere i concetti descritti in Creare un modello per un controllo.

Annotazioni

Per creare un controllo che può avere l'aspetto personalizzato, è necessario creare un controllo che eredita dalla classe o una delle relative sottoclassi diverse da ControlUserControl. Un controllo che eredita da UserControl può essere creato rapidamente, ma non usa un ControlTemplate e non è possibile personalizzarne l'aspetto.

Modello di parti e stati

Il modello parti e stati specifica come definire la struttura visiva e il comportamento visivo di un controllo. Per seguire il modello di parti e stati, eseguire le operazioni seguenti:

  • Definire la struttura visiva e il comportamento visivo di un controllo in ControlTemplate.

  • Seguire alcune procedure consigliate quando la logica del controllo interagisce con parti del modello di controllo.

  • Specificare un contratto di controllo per specificare gli elementi da includere in ControlTemplate.

Quando si definiscono la struttura visiva e il comportamento visivo in ControlTemplate di un controllo, gli autori dell'applicazione possono modificare la struttura visiva e il comportamento visivo del controllo creando un nuovo ControlTemplate anziché scrivere codice. È necessario fornire un contratto di controllo che indica agli autori dell'applicazione quali FrameworkElement oggetti e stati devono essere definiti in ControlTemplate. È consigliabile seguire alcune procedure consigliate quando si interagisce con le parti in ControlTemplate in modo che il controllo gestisca correttamente un oggetto incompleto ControlTemplate. Se si seguono questi tre principi, gli autori di applicazioni saranno in grado di creare un ControlTemplate per il controllo altrettanto facilmente come fanno per i controlli forniti con WPF. La sezione seguente illustra in dettaglio ognuna di queste raccomandazioni.

Definizione della struttura visiva e del comportamento visivo di un controllo in un ControlTemplate

Quando si crea un controllo personalizzato usando il modello di parti e stati, si può definire la struttura visiva e il comportamento visivo del controllo in ControlTemplate invece che nella logica. La struttura visiva di un controllo è la composizione di FrameworkElement oggetti che costituiscono il controllo. Il comportamento visivo è il modo in cui il controllo viene visualizzato quando si trova in un determinato stato. Per altre informazioni sulla creazione di un ControlTemplate oggetto che specifica la struttura visiva e il comportamento visivo di un controllo, vedere Creare un modello per un controllo.

Nell'esempio del NumericUpDown controllo la struttura visiva include due RepeatButton controlli e un TextBlock. Se si aggiungono questi controlli nel codice del NumericUpDown controllo nel relativo costruttore, ad esempio le posizioni di tali controlli non saranno modificabili. Anziché definire la struttura visiva e il comportamento visivo del controllo nel codice, è necessario definirlo in ControlTemplate. Poi uno sviluppatore di applicazioni può personalizzare la posizione dei pulsanti e TextBlock specificare quale comportamento si verifica quando Value è negativo, poiché ControlTemplate può essere sostituito.

Nell'esempio seguente viene illustrata la struttura visiva del NumericUpDown controllo , che include un RepeatButton oggetto per aumentare Value, un RepeatButton oggetto per ridurre Valuee un TextBlock oggetto da visualizzare 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>

Un aspetto visivo del comando NumericUpDown è che il valore appare in rosso se è negativo. Se si modifica il Foreground del TextBlock nel codice quando il Value è negativo, il NumericUpDown mostrerà sempre un valore negativo rosso. È possibile specificare il comportamento visivo del controllo in ControlTemplate aggiungendo VisualState oggetti a ControlTemplate. Nell'esempio seguente vengono illustrati gli oggetti VisualState per gli stati Positive e Negative. Positive e Negative si escludono a vicenda (il controllo si trova sempre esattamente in uno dei due), quindi l'esempio inserisce gli VisualState oggetti in un singolo VisualStateGroupoggetto . Quando il controllo entra nello stato Negative, l'oggetto ForegroundTextBlock diventa rosso. Quando il controllo è nello Positive stato , Foreground restituisce al valore originale. La definizione di VisualState oggetti in un ControlTemplate è ulteriormente discussa in Creare un modello per un controllo.

Annotazioni

Assicurarsi di impostare la VisualStateManager.VisualStateGroups proprietà associata nella radice FrameworkElement dell'oggetto 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>

Uso di parti di ControlTemplate nel codice

Un autore ControlTemplate potrebbe omettere oggetti FrameworkElement o VisualState, intenzionalmente o per errore, ma la logica del controllo potrebbe richiedere tali parti per funzionare correttamente. Il modello parti e stati specifica che il controllo deve essere resiliente a un ControlTemplate che manca di oggetti FrameworkElement o VisualState. Il controllo non dovrebbe generare un'eccezione o segnalare un errore se FrameworkElement, VisualState o VisualStateGroup non è presente nel ControlTemplate. In questa sezione vengono descritte le procedure consigliate per interagire con FrameworkElement oggetti e stati di gestione.

Anticipare gli oggetti FrameworkElement mancanti

Quando si definiscono FrameworkElement oggetti in ControlTemplate, la logica del controllo potrebbe dover interagire con alcune di esse. Ad esempio, il NumericUpDown controllo si sottoscrive all'evento dei Click pulsanti per aumentare o diminuire Value e imposta la proprietà Text di TextBlock su Value. Se un oggetto personalizzato ControlTemplate omette i pulsanti TextBlock o altre funzioni, è accettabile che il controllo perda alcune delle sue funzionalità, ma è necessario assicurarsi che il controllo non provochi un errore. Ad esempio, se un oggetto ControlTemplate non contiene i pulsanti per modificare Value, perde NumericUpDown tale funzionalità, ma un'applicazione che usa l'oggetto ControlTemplate continuerà a essere eseguita.

Le procedure seguenti assicurano che il controllo risponda correttamente agli oggetti mancanti FrameworkElement :

  1. Impostare l'attributo x:Name per ogni FrameworkElement elemento a cui è necessario fare riferimento nel codice.

  2. Definire le proprietà private per ogni FrameworkElement oggetto con cui è necessario interagire.

  3. Sottoscrivere e annullare la sottoscrizione a tutti gli eventi gestiti dal controllo nella FrameworkElement funzione di accesso set della proprietà.

  4. Imposta le proprietà FrameworkElement che hai definito nel passaggio 2 nel metodo OnApplyTemplate. Questa è la prima volta che FrameworkElement nel ControlTemplate è disponibile per il controllo. Usa il x:Name del FrameworkElement per ottenerlo dal ControlTemplate.

  5. Verificare che FrameworkElement non sia null prima di accedere ai relativi membri. Se è null, non segnalare un errore.

Negli esempi seguenti viene illustrato come il NumericUpDown controllo interagisce con FrameworkElement gli oggetti in base alle raccomandazioni nell'elenco precedente.

Nell'esempio che definisce la struttura visiva del controllo NumericUpDown in ControlTemplate, il RepeatButton che aumenta Value ha il suo attributo x:Name impostato su UpButton. Nell'esempio seguente viene dichiarata una proprietà denominata UpButtonElement che rappresenta l'oggetto RepeatButton dichiarato in ControlTemplate. La set funzione di accesso annulla prima di tutto l'evento del Click pulsante se UpDownElement non nullè , quindi imposta la proprietà e quindi sottoscrive l'evento Click . Esiste anche una proprietà definita, ma non illustrata qui, per l'altro RepeatButton, denominato 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

Nell'esempio seguente viene mostrato OnApplyTemplate per il controllo NumericUpDown. Nell'esempio viene utilizzato il GetTemplateChild metodo per ottenere gli FrameworkElement oggetti da ControlTemplate. Si noti che l'esempio protegge i casi in cui GetTemplateChild trova un FrameworkElement oggetto con il nome specificato che non è del tipo previsto. È anche una buona pratica ignorare gli elementi che hanno il x:Name specificato ma sono di tipo errato.

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

Seguendo le procedure illustrate negli esempi precedenti, assicuri che il controllo continui a essere eseguito quando il ControlTemplate manca un FrameworkElement.

Usare VisualStateManager per gestire gli stati

VisualStateManager Tiene traccia degli stati di un controllo ed esegue la logica necessaria per la transizione tra stati. Quando si aggiungono gli oggetti VisualState a ControlTemplate, li si colloca in un VisualStateGroup e si assegna VisualStateGroup alla proprietà associata VisualStateManager.VisualStateGroups affinché VisualStateManager possa accedervi.

Nell'esempio seguente viene ripetuto l'esempio precedente che mostra gli VisualState oggetti che corrispondono agli stati Positive e Negative del controllo. Il Storyboard nel NegativeVisualState rende il Foreground del TextBlock rosso. Quando il NumericUpDown controllo è nello Negative stato, inizia il Negative storyboard. Il Storyboard nello stato Negative si arresta quando il controllo torna nello stato Positive. Il PositiveVisualState non deve contenere un Storyboard perché, quando il Storyboard per il Negative si arresta, il Foreground ritorna al colore originale.

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

Si noti che all'oggetto TextBlock viene assegnato un nome, ma TextBlock non è presente nel contratto di controllo per NumericUpDown perché la logica del controllo non fa mai riferimento a TextBlock. Gli elementi a cui viene fatto riferimento in ControlTemplate hanno nomi, ma non devono far parte del contratto di controllo perché un nuovo ControlTemplate oggetto per il controllo potrebbe non dover fare riferimento a tale elemento. Ad esempio, un utente che crea un nuovo ControlTemplate per NumericUpDown potrebbe decidere di non indicare che Value è negativo modificando il Foreground. In tal caso, né il codice né il ControlTemplate fa riferimento all'oggetto TextBlock per nome.

La logica del controllo è responsabile della modifica dello stato del controllo. Nell'esempio seguente viene illustrato che il NumericUpDown controllo chiama il GoToState metodo per passare Positive allo stato quando Value è 0 o maggiore e lo Negative stato quando Value è minore di 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

Il GoToState metodo esegue la logica necessaria per avviare e arrestare gli storyboard in modo appropriato. Quando un controllo chiama GoToState per modificarne lo stato, VisualStateManager esegue le operazioni seguenti:

  • Se il VisualState a cui sta andando il controllo ha un Storyboard, inizia lo storyboard. Quindi, se l'oggetto VisualState da cui proviene il controllo ha un Storyboard, lo storyboard termina.

  • Se il controllo è già nello stato specificato, GoToState non esegue alcuna azione e restituisce true.

  • Se lo stato specificato non esiste in ControlTemplate , controlGoToState non esegue alcuna azione e restituisce false.

Procedure consigliate per l'uso di VisualStateManager

È consigliabile eseguire le operazioni seguenti per mantenere gli stati del controllo:

  • Utilizzare le proprietà per tenere traccia dello stato.

  • Creare un metodo helper per eseguire la transizione tra stati.

Il NumericUpDown controllo usa la relativa Value proprietà per tenere traccia se si trova nello Positive stato o Negative . Il controllo NumericUpDown definisce gli stati Focused e anche UnFocused, che tengono traccia della proprietà IsFocused. Se si utilizzano stati che non corrispondono naturalmente a una proprietà del controllo, è possibile definire una proprietà privata per tenere traccia dello stato.

Un singolo metodo che aggiorna tutti gli stati centralizza le chiamate a VisualStateManager e mantiene gestibile il codice. Nell'esempio seguente viene illustrato il NumericUpDown metodo ausiliario del controllo UpdateStates. Quando Value è maggiore o uguale a 0, il Control è nello stato di Positive. Quando Value è minore di 0, il controllo si trova nello Negative stato . Quando IsFocused è true, il controllo è nello Focused stato; in caso contrario, si trova nello Unfocused stato . Il controllo può chiamare UpdateStates ogni volta che deve modificarne lo stato, indipendentemente dallo stato modificato.

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 si passa un nome di stato a GoToState quando il controllo è già in questo stato, GoToState non esegue alcuna operazione, quindi non è necessario verificare lo stato corrente del controllo. Ad esempio, se Value cambia da un numero negativo a un altro numero negativo, lo storyboard per lo Negative stato non viene interrotto e l'utente non visualizzerà una modifica nel controllo.

VisualStateManager usa oggetti VisualStateGroup per determinare da quale stato uscire quando si chiama GoToState. Il controllo è sempre in uno stato per ciascun VisualStateGroup definito nel relativo ControlTemplate e lascia uno stato solo quando entra in un altro stato dallo stesso VisualStateGroup. Ad esempio, il ControlTemplate del controllo NumericUpDown definisce gli oggetti Positive e NegativeVisualState in uno VisualStateGroup, e gli oggetti Focused e UnfocusedVisualState in un altro. È possibile visualizzare Focused e UnfocusedVisualState definiti nella sezione Esempio completo in questo argomento. Quando il controllo passa dallo Positive stato allo Negative stato, o viceversa, il controllo rimane nello Focused stato o Unfocused stato.

Esistono tre posizioni tipiche in cui lo stato di un controllo può cambiare:

  • Quando l'oggetto ControlTemplate viene applicato a Control.

  • Quando una proprietà viene modificata.

  • Quando si verifica un evento.

Negli esempi seguenti viene illustrato l'aggiornamento dello stato del NumericUpDown controllo in questi casi.

È necessario aggiornare lo stato del controllo nel OnApplyTemplate metodo in modo che il controllo venga visualizzato nello stato corretto quando ControlTemplate viene applicato . Nell'esempio seguente, UpdateStates viene chiamato all'interno di OnApplyTemplate per assicurarsi che il controllo si trovi negli stati appropriati. Si supponga, ad esempio, di creare un NumericUpDown controllo e quindi impostarlo Foreground su verde e Value su -5. Se non si chiama UpdateStates quando ControlTemplate è applicato al controllo NumericUpDown, il controllo non si trova nello stato Negative e il valore è verde anziché rosso. È necessario chiamare UpdateStates per inserire il controllo nello Negative stato .

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

Spesso è necessario aggiornare gli stati di un controllo quando viene modificata una proprietà. Nell'esempio seguente viene illustrato l'intero ValueChangedCallback metodo. Poiché ValueChangedCallback viene chiamato quando Value cambia, il metodo chiama UpdateStates nel caso Value cambi da positivo a negativo o viceversa. È accettabile chiamare UpdateStates quando Value cambia ma rimane positivo o negativo perché in tal caso il controllo non modificherà gli stati.

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

Potrebbe anche essere necessario aggiornare gli stati quando si verifica un evento. Nell'esempio seguente viene illustrato che NumericUpDown chiama UpdateStates su Control per gestire l'evento GotFocus.

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

VisualStateManager aiuta a gestire gli stati del controllo. Usando il VisualStateManager, si assicuri che il controllo passi correttamente da uno stato all'altro. Se si seguono le raccomandazioni descritte in questa sezione per lavorare con il VisualStateManager, il codice del controllo rimarrà leggibile e gestibile.

Fornire il contratto di controllo

Si fornisce un contratto di controllo in modo che ControlTemplate gli autori sappiano cosa inserire nel modello. Un contratto di controllo ha tre elementi:

  • Elementi visivi usati dalla logica del controllo.

  • Gli stati del controllo e il gruppo a cui appartiene ogni stato.

  • Proprietà pubbliche che influiscono visivamente sul controllo.

Un utente che crea un nuovo ControlTemplate oggetto deve conoscere gli FrameworkElement oggetti usati dalla logica del controllo, il tipo di ogni oggetto e il relativo nome. Un ControlTemplate autore deve anche conoscere il nome di ogni possibile stato in cui il controllo può trovarsi e in quale VisualStateGroup stato si trova.

Tornando all'esempio NumericUpDown , il controllo prevede che l'oggetto ControlTemplate abbia gli oggetti seguenti FrameworkElement :

Il controllo può trovarsi negli stati seguenti:

Per specificare gli FrameworkElement oggetti previsti dal controllo, utilizzare TemplatePartAttribute, che specifica il nome e il tipo degli elementi previsti. Per specificare gli stati possibili di un controllo, utilizzare , TemplateVisualStateAttributeche specifica il nome dello stato e a cui VisualStateGroup appartiene. Inserire TemplatePartAttribute e TemplateVisualStateAttribute nella definizione della classe del controllo.

Anche qualsiasi proprietà pubblica che influisce sull'aspetto del controllo fa parte del contratto di controllo.

Nell'esempio seguente vengono specificati l'oggetto FrameworkElement e gli stati per il NumericUpDown controllo .

[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

Esempio completo

L'esempio seguente è l'intero ControlTemplate per l'elemento di controllo 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>

Nell'esempio seguente viene illustrata la logica per .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

Vedere anche