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
Oggetto CheckBox che utilizza 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
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.
Nota |
---|
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.
Nota |
---|
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:
Impostare l'attributo x:Name per ogni oggetto FrameworkElement a cui è necessario fare riferimento nel codice.
Definire proprietà private per ogni oggetto FrameworkElement con cui è necessario interagire.
Sottoscrivere e annullare la sottoscrizione di qualsiasi evento gestito dal controllo nella funzione di accesso set della proprietà dell'oggetto FrameworkElement.
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.
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:
Oggetto RepeatButton denominato UpButton.
Oggetto RepeatButton denominato DownButton.
Il controllo può essere negli stati seguenti:
In ValueStates VisualStateGroup
Positive
Negative
In FocusStates VisualStateGroup
Focused
Unfocused
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; }
}
}
}