Creazione di un controllo dall'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 oggetto CheckBox oltre a quale impostazione verranno eseguite le proprietà creando un nuovo ControlTemplateoggetto . Nella figura seguente viene illustrato un CheckBox oggetto che usa un valore predefinito ControlTemplate e un CheckBox oggetto che usa un oggetto personalizzato ControlTemplate.

A checkbox with the default control template. Controllo CheckBox che usa il modello di controllo predefinito

A checkbox with a custom control template. Controllo 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 il modello di parti e stati, pertanto 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.

NumericUpDown custom control. Controllo NumericUpDown personalizzato

Questo argomento include 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.

Nota

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 è un controllo che può essere creato rapidamente, ma non usa e ControlTemplate non è possibile personalizzarne l'aspetto.

Parti e stati del modello

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 in ControlTemplate di un controllo .

  • 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 oggetto per il controllo così facilmente possibile 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 il controllo personalizzato usando il modello di parti e stati, si definisce la struttura visiva e il comportamento visivo del controllo anziché ControlTemplate 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 oggetto 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. Quindi uno sviluppatore di applicazioni per personalizzare la posizione dei pulsanti e TextBlock specificare il comportamento che si verifica quando Value è negativo perché 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 comportamento visivo del NumericUpDown controllo è che il valore è in un tipo di carattere rosso se è negativo. Se si modifica l'oggetto TextBlockForeground di nel codice quando è Value negativo, l'oggetto 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 VisualState oggetti per gli Positive stati 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 Negative stato, l'oggetto dell'oggetto ForegroundTextBlock diventa rosso. Quando il controllo è nello Positive stato , Foreground restituisce al valore originale. La definizione di VisualState oggetti in un ControlTemplate oggetto è illustrata in Creare un modello per un controllo .

Nota

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 ControlTemplate autore potrebbe omettere o VisualState oggettiFrameworkElement, intenzionalmente o per errore, ma la logica del controllo potrebbe richiedere il corretto funzionamento di tali parti. Il modello parti e stati specifica che il controllo deve essere resiliente a un oggetto mancante FrameworkElement o VisualState a oggettiControlTemplate. Il controllo non deve generare un'eccezione o segnalare un errore se FrameworkElementun oggetto , VisualStateo VisualStateGroup non è presente nell'oggetto ControlTemplate. In questa sezione vengono descritte le procedure consigliate per interagire con FrameworkElement oggetti e stati di gestione.

Prevedere 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 sottoscrive l'evento dei Click pulsanti per aumentare o diminuire Value e imposta la Text proprietà di TextBlock su Value. Se un oggetto personalizzato ControlTemplate omette i TextBlock pulsanti o , è accettabile che il controllo perda alcune funzionalità, ma è necessario assicurarsi che il controllo non causi 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. Impostare le FrameworkElement proprietà definite nel passaggio 2 nel OnApplyTemplate metodo . Questo è il primo elemento FrameworkElement in ControlTemplate cui è disponibile per il controllo . Usare l'oggetto x:NameFrameworkElement di per ottenerlo da ControlTemplate.

  5. Verificare che non null sia prima di FrameworkElement 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 NumericUpDown controllo in ControlTemplate, l'oggetto che aumenta ha Value il RepeatButton relativo x:Name attributo 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 illustrato l'oggetto OnApplyTemplate per il NumericUpDown controllo . 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 consigliabile ignorare gli elementi con l'oggetto specificato x:Name , 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, assicurarsi che il controllo continui a essere eseguito quando manca ControlTemplate un FrameworkElementoggetto .

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 VisualState oggetti a ControlTemplate, aggiungerli a un VisualStateGroup oggetto e aggiungerli VisualStateGroup alla VisualStateManager.VisualStateGroups proprietà associata in modo che l'oggetto VisualStateManager abbia accesso a tali oggetti.

Nell'esempio seguente viene ripetuto l'esempio precedente che mostra gli VisualState oggetti che corrispondono agli Positive stati e Negative del controllo . L'oggetto Storyboard nell'oggettoVisualStateNegativediventa il Foreground rossoTextBlock. Quando il NumericUpDown controllo è nello Negative stato , inizia lo Negative storyboard nello stato . Quindi, nello StoryboardNegative stato si arresta quando il controllo torna allo Positive stato . L'oggetto PositiveVisualState non deve contenere un oggetto Storyboard perché, quando l'oggetto Storyboard per l'oggetto Negative si arresta, restituisce il Foreground 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 oggetto per NumericUpDown potrebbe decidere di non indicare che Value è negativo modificando .Foreground In tal caso, né il codice né il fa riferimento all'oggetto ControlTemplate in base al TextBlock 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 l'oggetto VisualState che il controllo avrà , Storyboardinizia lo storyboard. Quindi, se l'oggetto VisualState da cui proviene il controllo ha un Storyboardoggetto , 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 NumericUpDown controllo definisce anche gli Focused stati e UnFocused , che tiene traccia della IsFocused proprietà . 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 helper del controllo , UpdateStates. Quando Value è maggiore o uguale a 0, è Control nello Positive stato . 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 quale stato uscire quando si chiama GoToState. Il controllo è sempre in uno stato per ogni VisualStateGroup oggetto definito nel relativo ControlTemplate e lascia solo uno stato quando entra in un altro stato dallo stesso VisualStateGroupoggetto . Ad esempio, l'oggetto NumericUpDownControlTemplate del controllo definisce gli Positive oggetti eVisualStateNegativein uno VisualStateGroup e gli Focused oggetti e UnfocusedVisualState in un altro. È possibile visualizzare Focused e UnfocusedVisualState definito 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 .

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 viene chiamato UpdateStates per OnApplyTemplate assicurarsi che il controllo si trova 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 l'oggetto ControlTemplate viene applicato al NumericUpDown controllo, il controllo non è nello Negative stato 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 le modifiche ma rimangono positive o negative 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 le NumericUpDown chiamate 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 Consente di gestire gli stati del controllo. Usando , VisualStateManagerassicurarsi che il controllo passi correttamente da uno stato all'altro. Se si seguono le raccomandazioni descritte in questa sezione per l'uso di , il VisualStateManagercodice 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 include tre elementi:

  • Gli elementi visivi usati dalla logica del controllo.

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

  • Le 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 e TemplatePartAttributeTemplateVisualStateAttribute 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 il NumericUpDown controllo .

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

Vedi anche