Condividi tramite


Creazione di un controllo dall'aspetto personalizzabile

In Windows Presentation Foundation (WPF) è possibile creare un controllo dall'aspetto personalizzabile. È ad esempio possibile modificare l'aspetto di un oggetto CheckBox ben oltre le possibilità offerte dall'impostazione delle proprietà creando un nuovo oggetto ControlTemplate. Nella figura seguente viene illustrato un oggetto CheckBox che utilizza un oggetto ControlTemplate predefinito e un oggetto CheckBox che utilizza un oggetto ControlTemplate personalizzato.

Oggetto CheckBox che utilizza il modello di controllo predefinito

Casella di controllo con il modello di controllo predefinito.

Oggetto CheckBox che utilizza un modello di controllo personalizzato

Casella di controllo con un modello di controllo personalizzato.

Se durante la creazione di un controllo si segue il modello delle parti e degli stati, l'aspetto del controllo risulterà personalizzabile. Alcuni strumenti di progettazione, ad esempio quali Microsoft Expression Blend, supportano il modello delle parti e degli stati e pertanto l'utilizzo di questo modello renderà personalizzabile il controllo in tali tipi di applicazioni. In questo argomento viene illustrato il modello delle parti e degli stati e viene spiegato come utilizzarlo quando si crea il controllo personalizzato. Per illustrare la filosofia di questo modello, in questo argomento è incluso un esempio di controllo personalizzato, ovvero NumericUpDown. Il controllo NumericUpDown consente di visualizzare un valore numerico che un utente può aumentare o ridurre facendo clic sui pulsanti del controllo. Nell'immagine seguente viene illustrato il controllo NumericUpDown descritto in questo argomento.

Controllo NumericUpDown personalizzato

Controllo personalizzato NumericUpDown.

In questo argomento sono incluse le sezioni seguenti:

  • Prerequisiti

  • Modello delle parti e degli stati

  • Definizione della struttura e del comportamento visuali di un controllo in un oggetto ControlTemplate

  • Utilizzo di parti dell'oggetto ControlTemplate nel codice

  • Disponibilità del contratto di controllo

  • Esempio completo

Prerequisiti

In questo argomento si suppone che l'utente sia in grado di creare un nuovo oggetto ControlTemplate per un controllo esistente, che abbia familiarità con gli elementi di un contratto di controllo e che abbia compreso i concetti illustrati in Personalizzazione dell'aspetto di un controllo esistente mediante la creazione di un oggetto ControlTemplate.

NotaNota

Per creare un controllo di cui sia possibile personalizzare l'aspetto, è necessario creare un controllo che erediti dalla classe Control o da una delle relative sottoclassi diverse da UserControl.Un controllo che eredita da UserControl è un controllo che può essere creato con rapidità, ma poiché non utilizza un oggetto ControlTemplate non è possibile personalizzarne l'aspetto.

Modello delle parti e degli stati

Il modello delle parti e degli stati specifica come definire la struttura visuale e il comportamento visivo di un controllo. L'adesione al modello delle parti e degli stati comporta l'esecuzione delle operazioni seguenti:

  • Definizione della struttura visuale e del comportamento visivo nell'oggetto ControlTemplate di un controllo.

  • Adesione a determinate procedure consigliate quando la logica del controllo interagisce con parti del modello di controllo.

  • Disponibilità di un contratto di controllo per la specifica degli elementi da includere nell'oggetto ControlTemplate.

Quando si definiscono la struttura e il comportamento visuali nell'oggetto ControlTemplate di un controllo, gli autori delle applicazioni possono modificare la struttura visuale e il comportamento visivo del controllo creando un nuovo oggetto ControlTemplate anziché scrivendo codice. È necessario fornire un contratto di controllo che indichi agli autori delle applicazioni quali oggetti FrameworkElement e stati devono essere definiti nell'oggetto ControlTemplate. Quando si interagisce con le parti incluse nell'oggetto ControlTemplate, è necessario utilizzare alcune procedure consigliate, in modo che il controllo sia in grado di gestire correttamente un oggetto ControlTemplate incompleto. Se si seguono questi tre principi, gli autori delle applicazioni saranno in grado di creare un oggetto ControlTemplate per il controllo con la stessa facilità con cui è possibile crearlo per i controlli forniti con WPF. Ogni suggerimento viene descritto in dettaglio nella sezione seguente.

Definizione della struttura e del comportamento visuali di un controllo in un oggetto ControlTemplate

La creazione del controllo personalizzato mediante il modello delle parti e degli stati comporta la definizione della struttura visuale e del comportamento visivo del controllo nel relativo oggetto ControlTemplate anziché nella logica corrispondente. La struttura visuale di un controllo è formata dagli oggetti FrameworkElement che costituiscono il controllo. Il comportamento visivo è il modo in cui il controllo viene visualizzato quando si trova in un determinato stato. Per ulteriori informazioni sulla creazione di un oggetto ControlTemplate che specifica la struttura visuale e il comportamento visivo di un controllo, vedere Personalizzazione dell'aspetto di un controllo esistente mediante la creazione di un oggetto ControlTemplate.

Nell'esempio del controllo NumericUpDown la struttura visuale include due controlli RepeatButton e un oggetto TextBlock. L'aggiunta questi controlli nel codice del controllo NumericUpDown, ad esempio nel relativo costruttore, determinerebbe l'impossibilità di modificare le posizioni di tali controlli. Anziché definire la struttura visuale e il comportamento visivo del controllo nel relativo codice, è necessario definirli nell'oggetto ControlTemplate. Poiché l'oggetto ControlTemplate può essere sostituito, uno sviluppatore di applicazioni può personalizzare la posizione dei pulsanti e dell'oggetto TextBlock e specificare quale comportamento si verifica quando Value è negativo.

Nell'esempio seguente viene illustrata la struttura visuale del controllo NumericUpDown che include un oggetto RepeatButton per aumentare Value, un oggetto RepeatButton per ridurre Value e un oggetto TextBlock per 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 comportamento visivo del controllo NumericUpDown prevede che il valore venga visualizzato con un tipo di carattere rosso quando è negativo. Se si modifica la proprietà Foreground dell'oggetto TextBlock nel codice quando Value è negativo, NumericUpDown visualizzerà sempre un valore negativo di colore rosso. Il comportamento visivo del controllo nell'oggetto ControlTemplate viene specificato aggiungendo oggetti VisualState all'oggetto ControlTemplate. Nell'esempio seguente vengono illustrati gli oggetti VisualState per gli stati Positive e Negative. Positive e Negative si escludono a vicenda (lo stato del controllo corrisponde sempre a uno dei due) e pertanto nell'esempio gli oggetti VisualState vengono inseriti in un unico oggetto VisualStateGroup. Quando il controllo passa nello stato Negative, la proprietà Foreground dell'oggetto TextBlock diventa di colore rosso. Quando il controllo è nello stato Positive, la proprietà Foreground ritorna al valore originale. La definizione di oggetti VisualState in un oggetto ControlTemplate viene discussa ulteriormente in Personalizzazione dell'aspetto di un controllo esistente mediante la creazione di un oggetto ControlTemplate.

NotaNota

Assicurarsi di impostare la proprietà associata VisualStateManager.VisualStateGroups sulla 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>

Utilizzo di parti dell'oggetto ControlTemplate nel codice

Un autore di ControlTemplate potrebbe omettere oggetti FrameworkElement o VisualState intenzionalmente o per errore, ma per il corretto funzionamento è possibile che la logica del controllo necessiti di tali parti. Il modello delle parti e degli stati specifica che il controllo deve essere resiliente a un oggetto ControlTemplate in cui mancano oggetti FrameworkElement o VisualState. Il controllo non deve generare un'eccezione o segnalare un errore se un oggetto FrameworkElement, VisualState o VisualStateGroup risulta mancante da ControlTemplate. In questa sezione vengono illustrate le procedure consigliate per l'interazione con gli oggetti FrameworkElement e la gestione degli stati.

Prevedere la mancanza di oggetti FrameworkElement

Quando si definiscono oggetti FrameworkElement in ControlTemplate, la logica del controllo potrebbe dover interagire con alcuni questi oggetti. Ad esempio, il controllo NumericUpDown sottoscrive l'evento Click dei pulsanti per aumentare o diminuire Value e imposta la proprietà Text dell'oggetto TextBlock su Value. Se un oggetto ControlTemplate personalizzato omette TextBlock o i pulsanti, è plausibile che il controllo perda alcuna delle relative funzionalità, ma è necessario verificare che il controllo non causi un errore. Ad esempio, se un oggetto ControlTemplate non contiene i pulsanti per la modifica di Value, NumericUpDown perderà tale funzionalità, ma un'applicazione che utilizza ControlTemplate continuerà a essere in esecuzione.

L'utilizzo delle procedure seguenti garantirà che il controllo risponda correttamente agli oggetti FrameworkElement mancanti:

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

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

  3. Sottoscrivere e annullare la sottoscrizione di qualsiasi evento gestito dal controllo nella funzione di accesso set della proprietà dell'oggetto FrameworkElement.

  4. Impostare le proprietà dell'oggetto FrameworkElement di cui è stata effettuata la definizione nel passaggio 2 nel metodo OnApplyTemplate. Si tratta del primo oggetto FrameworkElement in ControlTemplate disponibile per il controllo. Utilizzare l'attributo x:Name dell'oggetto FrameworkElement per ottenere il valore corrispondente da ControlTemplate.

  5. Verificare che il valore di FrameworkElement non sia null prima dell'accesso ai relativi membri. Se è null, non segnalare un errore.

Negli esempi seguenti viene illustrata l'interazione del controllo NumericUpDown con gli oggetti FrameworkElement in base ai suggerimenti riportati nell'elenco precedente.

Nell'esempio che definisce la struttura visuale del controllo NumericUpDown nell'oggetto ControlTemplate l'oggetto RepeatButton che determina l'aumento di Value dispone dell'attributo x:Name impostato su UpButton. Nell'esempio seguente viene dichiarata una proprietà denominata UpButtonElement che rappresenta l'oggetto RepeatButton dichiarato in ControlTemplate. La funzione di accesso set annulla innanzitutto la sottoscrizione dell'evento Click del pulsante se UpDownElement non è null, quindi imposta la proprietà e infine sottoscrive l'evento Click. Viene inoltre definita una proprietà, non illustrata in questo esempio, per l'altro oggetto RepeatButton denominato DownButtonElement.

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 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);
        }
    }
}

Nell'esempio seguente viene illustrato il metodo OnApplyTemplate per il controllo NumericUpDown. Nell'esempio viene utilizzato il metodo GetTemplateChild per ottenere gli oggetti FrameworkElement da ControlTemplate. Si noti che nell'esempio viene evitato che si verifichino casi in cui il metodo GetTemplateChild trova un oggetto FrameworkElement con il nome specificato che non corrisponde al tipo previsto. La procedura consigliata prevede inoltre di ignorare gli elementi che dispongono dell'attributo x:Name ma che sono del tipo non corretto.

Public Overloads Overrides Sub OnApplyTemplate()

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

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

    UpdateStates(false);
}

L'utilizzo delle procedure illustrate nell'esempio precedente assicura che il controllo continuerà a essere eseguito quando da ControlTemplate risulta mancante un oggetto FrameworkElement.

Utilizzare l'oggetto VisualStateManager per gestire gli stati

L'oggetto VisualStateManager consente di tenere traccia degli stati di un controllo e di eseguire la logica necessaria per la transizione tra gli stati. L'aggiunta di oggetti VisualState a ControlTemplate comporta la loro aggiunta a un oggetto VisualStateGroup e l'aggiunta dell'oggetto VisualStateGroup alla proprietà collegata VisualStateManager.VisualStateGroups in modo che VisualStateManager disponga dell'accesso a tali oggetti.

Nell'esempio seguente viene ripetuto l'esempio precedente in cui vengono illustrati gli oggetti VisualState che corrispondono agli stati Positive e Negative del controllo. L'oggetto Storyboard nell'oggetto VisualState Negative trasforma in rosso la proprietà Foreground dell'oggetto TextBlock. Quando il controllo NumericUpDown è nello stato Negative, lo storyboard nello stato Negative viene avviato. L'oggetto Storyboard nello stato Negative viene quindi interrotto quando il controllo torna allo stato Positive. Non è necessario che l'oggetto VisualState Positive contenga un oggetto Storyboard, poiché quando Storyboard per lo stato Negative viene interrotto, la proprietà Foreground ritorna al relativo 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 è incluso nel contratto di controllo per NumericUpDown poiché la logica del controllo non fa mai riferimento a TextBlock. Gli elementi a cui viene fatto riferimento in ControlTemplate dispongono di nomi, ma non devono far parte del contratto di controllo, poiché un nuovo ControlTemplate per il controllo potrebbe non avere necessità di fare riferimento a tale elemento. Ad esempio, l'autore di un nuovo oggetto ControlTemplate per NumericUpDown potrebbe decidere di non indicare che Value è negativo mediante la modifica di Foreground. In tal caso, né il codice né l'oggetto ControlTemplate fanno riferimento a TextBlock in base al nome.

La logica del controllo è responsabile della modifica dello stato del controllo. Nell'esempio seguente viene illustrata la chiamata del controllo NumericUpDown al metodo GoToState per il passaggio allo stato Positive quando Value è 0 o maggiore e allo stato Negative quando Value è minore di 0.

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

Il metodo GoToState esegue la logica necessaria per avviare e interrompere lo storyboard in modo appropriato. Quando un controllo chiama il metodo GoToState per modificare il relativo stato, l'oggetto VisualStateManager effettua le operazioni seguenti:

  • Se l'oggetto VisualState di destinazione del controllo dispone di un oggetto Storyboard, lo storyboard verrà avviato. Quindi, se l'oggetto VisualState da cui il controllo proviene dispone di un oggetto Storyboard, lo storyboard verrà interrotto.

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

  • Se lo stato specificato non esiste nell'oggetto ControlTemplate di control, il metodo GoToState non eseguirà alcuna azione e restituirà false.

Procedure consigliate per l'utilizzo dell'oggetto VisualStateManager

Per gestire gli stati del controllo, è consigliabile effettuare le operazioni seguenti:

  • Utilizzare le proprietà per tenere traccia dello stato corrispondente.

  • Creare un metodo di supporto per la transizione tra gli stati.

Il controllo NumericUpDown utilizza la proprietà Value corrispondente per tenere traccia dello stato Positive o Negative. Il controllo NumericUpDown definisce inoltre gli stati Focused e UnFocused che consentono di tenere traccia della proprietà IsFocused. Se si utilizzano stati che non corrispondono per natura a una proprietà del controllo, sarà possibile definire una proprietà privata per tenere traccia dello stato.

Un singolo metodo che aggiorna tutti gli stati consente di centralizzare le chiamate a VisualStateManager e di mantenere gestibile il codice. Nell'esempio seguente viene illustrato il metodo di supporto del controllo NumericUpDown, ovvero UpdateStates. Quando Value è maggiore o uguale a 0, Control è nello stato Positive. Quando Value è minore di 0, il controllo è nello stato Negative. Quando IsFocused è true, il controllo è nello stato Focused. In caso contrario, è nello stato Unfocused. Il controllo può chiamare UpdateStates quando è necessario modificare lo stato, indipendentemente dallo stato che viene modificato.

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
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);
    }

}

Se si passa un nome di stato a GoToState quando il controllo è già in tale stato, GoToState non eseguirà alcuna operazione e non sarà quindi 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 stato Negative non verrà interrotto e il controllo visualizzato all'utente non presenterà alcuna modifica.

VisualStateManager utilizza oggetti VisualStateGroup per determinare quale stato disattivare quando si chiama GoToState. Il controllo è sempre in un unico stato per ogni VisualStateGroup definito nel relativo ControlTemplate e uno stato viene disattivato solo quando il controllo passa in un altro stato dallo stesso VisualStateGroup. Ad esempio, l'oggetto ControlTemplate del controllo NumericUpDown definisce gli oggetti VisualState con gli stati Positive e Negative in un oggetto VisualStateGroup e gli oggetti VisualState con gli stati Focused e Unfocused in un altro. È possibile osservare Focused e UnfocusedVisualState definiti nella sezione Esempio completo di questo argomento. Quando il controllo passa dallo stato Positive allo stato Negative o viceversa, il controllo rimane nello stato Focused o Unfocused.

Lo stato di un controllo può cambiare nelle tre situazioni tipiche seguenti:

  • Quando l'oggetto ControlTemplate viene applicato a Control.

  • Quando il valore di una proprietà cambia.

  • Quando si verifica un evento.

Negli esempi seguenti viene illustrato l'aggiornamento dello stato del controllo NumericUpDown nei casi descritti.

È necessario aggiornare lo stato del controllo nel metodo OnApplyTemplate in modo che il controllo venga visualizzato nello stato corretto quando ControlTemplate viene applicato. Nell'esempio seguente viene chiamato UpdateStates nel metodo OnApplyTemplate per assicurarsi che il controllo sia negli stati appropriati. Si supponga, ad esempio, di creare un controllo NumericUpDown, quindi di impostarne la proprietà Foreground su verde e Value su -5. Se non si chiama UpdateStates quando l'oggetto ControlTemplate viene applicato al controllo NumericUpDown, lo stato del controllo non sarà Negative e il valore sarà verde anziché rosso. Per fare in modo che il controllo passi nello stato Negative, è necessario chiamare UpdateStates.

Public Overloads Overrides Sub OnApplyTemplate()

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

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

    UpdateStates(false);
}

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

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
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));
}

Potrebbe inoltre essere necessario aggiornare gli stati quando si verifica un evento. Nell'esempio seguente viene illustrata la chiamata a UpdateStates da parte di NumericUpDown sull'oggetto Control per gestire l'evento GotFocus.

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

L'oggetto VisualStateManager consente di gestire gli stati del controllo. Tramite l'oggetto VisualStateManager è possibile garantire la transizione corretta del controllo tra gli stati. Se si seguono i suggerimenti descritti in questa sezione per l'utilizzo dell'oggetto VisualStateManager, il codice del controllo risulterà leggibile e gestibile.

Disponibilità del contratto di controllo

Un contratto di controllo viene reso disponibile in modo che gli autori dell'oggetto ControlTemplate siano a conoscenza degli elementi da inserire nel modello. In un contratto di controllo sono disponibili tre elementi:

  • Gli elementi visivi utilizzati dalla logica del controllo.

  • Gli stati del controllo e il gruppo al quale appartiene ogni stato.

  • Le proprietà pubbliche che influiscono a livello visivo sul controllo.

Quando si crea un nuovo oggetto ControlTemplate è necessario conoscere gli oggetti FrameworkElement utilizzati dalla logica del controllo, il tipo di ogni oggetto e il relativo nome. Un autore di ControlTemplate deve inoltre conoscere il nome di ogni possibile stato del controllo e lo stato in cui si trova l'oggetto VisualStateGroup.

Nell'esempio precedente relativo a NumericUpDown il controllo prevede che l'oggetto ControlTemplate contenga gli oggetti FrameworkElement seguenti:

Il controllo può essere negli stati seguenti:

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

Qualsiasi proprietà pubblica che influisce sull'aspetto del controllo fa anche parte del contratto di controllo.

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

<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 BackgroundProperty As DependencyProperty
    Public Shared ReadOnly BorderBrushProperty As DependencyProperty
    Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
    Public Shared ReadOnly FontFamilyProperty As DependencyProperty
    Public Shared ReadOnly FontSizeProperty As DependencyProperty
    Public Shared ReadOnly FontStretchProperty As DependencyProperty
    Public Shared ReadOnly FontStyleProperty As DependencyProperty
    Public Shared ReadOnly FontWeightProperty As DependencyProperty
    Public Shared ReadOnly ForegroundProperty As DependencyProperty
    Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
    Public Shared ReadOnly PaddingProperty As DependencyProperty
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty
    Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty


    Private _Background As Brush
    Public Property Background() As Brush
        Get
            Return _Background
        End Get
        Set(ByVal value As Brush)
            _Background = value
        End Set
    End Property

    Private _BorderBrush As Brush
    Public Property BorderBrush() As Brush
        Get
            Return _BorderBrush
        End Get
        Set(ByVal value As Brush)
            _BorderBrush = value
        End Set
    End Property

    Private _BorderThickness As Thickness
    Public Property BorderThickness() As Thickness
        Get
            Return _BorderThickness
        End Get
        Set(ByVal value As Thickness)
            _BorderThickness = value
        End Set
    End Property

    Private _FontFamily As FontFamily
    Public Property FontFamily() As FontFamily
        Get
            Return _FontFamily
        End Get
        Set(ByVal value As FontFamily)
            _FontFamily = value
        End Set
    End Property

    Private _FontSize As Double
    Public Property FontSize() As Double
        Get
            Return _FontSize
        End Get
        Set(ByVal value As Double)
            _FontSize = value
        End Set
    End Property

    Private _FontStretch As FontStretch
    Public Property FontStretch() As FontStretch
        Get
            Return _FontStretch
        End Get
        Set(ByVal value As FontStretch)
            _FontStretch = value
        End Set
    End Property

    Private _FontStyle As FontStyle
    Public Property FontStyle() As FontStyle
        Get
            Return _FontStyle
        End Get
        Set(ByVal value As FontStyle)
            _FontStyle = value
        End Set
    End Property

    Private _FontWeight As FontWeight
    Public Property FontWeight() As FontWeight
        Get
            Return _FontWeight
        End Get
        Set(ByVal value As FontWeight)
            _FontWeight = value
        End Set
    End Property

    Private _Foreground As Brush
    Public Property Foreground() As Brush
        Get
            Return _Foreground
        End Get
        Set(ByVal value As Brush)
            _Foreground = value
        End Set
    End Property

    Private _HorizontalContentAlignment As HorizontalAlignment
    Public Property HorizontalContentAlignment() As HorizontalAlignment
        Get
            Return _HorizontalContentAlignment
        End Get
        Set(ByVal value As HorizontalAlignment)
            _HorizontalContentAlignment = value
        End Set
    End Property

    Private _Padding As Thickness
    Public Property Padding() As Thickness
        Get
            Return _Padding
        End Get
        Set(ByVal value As Thickness)
            _Padding = value
        End Set
    End Property

    Private _TextAlignment As TextAlignment
    Public Property TextAlignment() As TextAlignment
        Get
            Return _TextAlignment
        End Get
        Set(ByVal value As TextAlignment)
            _TextAlignment = value
        End Set
    End Property

    Private _TextDecorations As TextDecorationCollection
    Public Property TextDecorations() As TextDecorationCollection
        Get
            Return _TextDecorations
        End Get
        Set(ByVal value As TextDecorationCollection)
            _TextDecorations = value
        End Set
    End Property

    Private _TextWrapping As TextWrapping
    Public Property TextWrapping() As TextWrapping
        Get
            Return _TextWrapping
        End Get
        Set(ByVal value As TextWrapping)
            _TextWrapping = value
        End Set
    End Property

    Private _VerticalContentAlignment As VerticalAlignment
    Public Property VerticalContentAlignment() As VerticalAlignment
        Get
            Return _VerticalContentAlignment
        End Get
        Set(ByVal value As VerticalAlignment)
            _VerticalContentAlignment = value
        End Set
    End Property
End Class
[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; }
}

Esempio completo

Nell'esempio seguente viene illustrato l'oggetto ControlTemplate completo per il controllo NumericUpDown.

<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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.

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
    Private _value As Integer

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

        _value = num
        RoutedEvent = id
    End Sub

    Public ReadOnly Property Value() As Integer
        Get
            Return _value
        End Get
    End Property
End Class
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; }
        }
    }
}

Vedere anche

Concetti

Personalizzazione dell'aspetto di un controllo esistente mediante la creazione di un oggetto ControlTemplate

Altre risorse

Personalizzazione dei controlli