Cenni preliminari sulla modifica di controlli

Aggiornamento: novembre 2007

L'estensibilità del modello di controlli Windows Presentation Foundation (WPF) riduce notevolmente l'esigenza di creare un nuovo controllo. Tuttavia, in certi casi può ancora essere necessario creare un controllo personalizzato. In questo argomento vengono illustrate le funzionalità che riducono al minimo l'esigenza di creare un controllo personalizzato e i diversi modelli di modifica dei controlli in Windows Presentation Foundation (WPF). In questo argomento viene inoltre illustrato come creare un nuovo controllo.

Nel presente argomento sono contenute le seguenti sezioni.

  • Alternative alla scrittura di un nuovo controllo
  • Modelli per la modifica dei controlli
  • Nozioni fondamentali sulla modifica dei controlli
  • Eredità da UserControl rispetto all'utilizzo di ControlTemplate
  • Argomenti correlati

Alternative alla scrittura di un nuovo controllo

In precedenza, per ottenere un'esperienza personalizzata da un controllo esistente, le modifiche erano limitate alle proprietà standard del controllo, ad esempio colore di sfondo, larghezza dei bordi e dimensioni del carattere. Per estendere l'aspetto o il comportamento di un controllo oltre tali parametri predefiniti era necessario creare un nuovo controllo, generalmente ereditando da un controllo esistente ed eseguendo l'override del metodo responsabile della creazione del controllo. Sebbene ciò sia ancora possibile, WPF consente di personalizzare i controlli esistenti mediante un modello di contenuto dettagliato, stili, modelli e trigger. Nell'elenco riportato di seguito vengono forniti alcuni esempi di utilizzo di tali funzionalità per la creazione di esperienze coerenti e personalizzate senza la necessità di creare un nuovo controllo.

  • Contenuto dettagliato Molti controlli WPF standard supportano il contenuto dettagliato. La proprietà di contenuto di Button, ad esempio, è di tipo Object. In teoria, pertanto, in Button è possibile visualizzare qualsiasi tipo di oggetto. Per visualizzare testo e immagini in un pulsante, è possibile aggiungere un'immagine e un oggetto TextBlock a StackPanel e assegnare StackPanel alla proprietà Content. Poiché i controlli sono in grado di visualizzare elementi visivi e dati arbitrari di WPF, vi è una minore esigenza di creare un nuovo controllo o di modificarne uno esistente per supportare una visualizzazione complessa. Per ulteriori informazioni sul modello di contenuto per Button e altri controlli, vedere Cenni preliminari sul modello di contenuto dei controlli. Per ulteriori informazioni su altri modelli di contenuto di WPF, vedere Modelli di contenuto.

  • StiliStyle è un insieme di valori che rappresentano le proprietà di un controllo. Utilizzando gli stili, è possibile creare una rappresentazione riutilizzabile dell'aspetto e del comportamento di un controllo desiderato senza scrivere un nuovo controllo. Si supponga, ad esempio, che tutti i controlli TextBlock debbano avere tipo di carattere Arial, di colore rosso e con una dimensione di 14 punti. È possibile creare uno stile come risorsa e impostare di conseguenza le opportune proprietà. Ogni oggetto TextBlock aggiunto all'applicazione avrà quindi lo stesso aspetto.

  • Modelli di datiDataTemplate consente di personalizzare la modalità di visualizzazione dei dati in un controllo. È ad esempio possibile utilizzare DataTemplate per specificare la modalità di visualizzazione dei dati in ListBox. Per un esempio, vedere Cenni preliminari sui modelli di dati. Oltre alla personalizzazione dell'aspetto dei dati, in DataTemplate possono essere inclusi elementi di interfaccia utente. Tale caratteristica aumenta la flessibilità nella personalizzazione dell'interfaccia utente stessa. Mediante DataTemplate, ad esempio, è possibile creare un oggetto ComboBox in cui ogni elemento contiene una casella di controllo.

  • Modelli di controllo. Per definire la struttura e l'aspetto di molti controlli WPF, viene utilizzato ControlTemplate, che separa l'aspetto di un controllo dalla relativa funzionalità. La ridefinizione di ControlTemplate consente di modificare radicalmente l'aspetto di un controllo. Si supponga, ad esempio, di voler utilizzare un controllo con l'aspetto di un semaforo. Il controllo dispone di una funzionalità e un'interfaccia utente semplici. È costituito da tre cerchi, che possono essere illuminati soltanto uno alla volta. RadioButton consente di fare in modo che venga selezionato un solo elemento alla volta, ma l'aspetto predefinito di RadioButton non ha nulla a che fare con le luci di un semaforo. Poiché per definire l'aspetto di RadioButton viene utilizzato un modello di controllo, è possibile ridefinire facilmente ControlTemplate in base ai requisiti del controllo e utilizzare pulsanti di opzione per realizzare il semaforo.

    Nota

    Anche se RadioButton può utilizzare DataTemplate, in questo esempio DataTemplate non è sufficiente. DataTemplate definisce l'aspetto del contenuto di un controllo. Nel caso di RadioButton, il contenuto è costituito da qualsiasi elemento visualizzato a destra del cerchio che indica la selezione di RadioButton. Nell'esempio del semaforo, il pulsante di opzione dovrà essere costituito semplicemente da un cerchio in grado di "illuminarsi". La notevole differenza tra il requisito relativo all'aspetto del semaforo e l'aspetto predefinito di RadioButton rende necessaria la ridefinizione di ControlTemplate. In genere, per la definizione del contenuto (o dei dati) di un controllo si utilizza DataTemplate, mentre per la definizione della struttura di un controllo si utilizza ControlTemplate.

  • TriggerTrigger consente di modificare in modo dinamico l'aspetto e il comportamento di un controllo senza creare un nuovo controllo. Si supponga, ad esempio, di avere più controlli ListBox nell'applicazione e che gli elementi selezionati di ciascun oggetto ListBox debbano essere visualizzati in grassetto e in rosso. La soluzione più ovvia sarebbe quella di creare una classe che eredita da ListBox e di eseguire l'override del metodo OnSelectionChanged per modificare l'aspetto dell'elemento selezionato. Tuttavia, un approccio migliore consiste nell'aggiunta di un trigger a uno stile di un oggetto ListBoxItem che modifichi l'aspetto dell'elemento selezionato. Un trigger consente di modificare i valori delle proprietà o di eseguire specifiche operazioni in base al valore di una proprietà. EventTrigger consente di intraprendere determinate azioni quando si verifica un evento.

Per ulteriori informazioni su stili, modelli e trigger, vedere Applicazione di stili e modelli.

In genere, se il controllo rispecchia la funzionalità di un controllo esistente ma si desidera che abbia un aspetto diverso, valutare innanzitutto la possibilità di utilizzare uno dei metodi descritti in questa sezione per modificare l'aspetto del controllo esistente.

Modelli per la modifica dei controlli

Modello di contenuto dettagliato, stili, modelli e trigger riducono al minimo la necessità di creare un nuovo controllo. Se, tuttavia, è necessario creare un nuovo controllo, è importante conoscere i diversi modelli di creazione dei controlli in WPF. In WPF sono disponibili tre modelli generali per la creazione di un controllo, ognuno dei quali offre un insieme di funzionalità e un livello di flessibilità differenti. Le classi base dei tre modelli sono: UserControl, Control e FrameworkElement.

Derivazione dall'oggetto UserControl

Il modo più semplice per creare un controllo in WPF consiste nel farlo derivare da UserControl. Nella compilazione di un controllo che eredita da UserControl, è possibile aggiungere componenti esistenti a UserControl, denominare i componenti e fare riferimento a gestori eventi nel codice Extensible Application Markup Language (XAML). Sarà quindi possibile fare riferimento agli elementi denominati e definire i gestori eventi nel codice. Tale modello di sviluppo è molto simile a quello utilizzato per lo sviluppo di applicazioni in WPF.

Se compilato correttamente, UserControl può sfruttare i vantaggi del contenuto dettagliato, degli stili e dei trigger. Se, tuttavia, il controllo eredita da UserControl, gli utenti di tale controllo non saranno in grado di utilizzare DataTemplate o ControlTemplate per personalizzarne l'aspetto. Per creare un controllo personalizzato che supporti i modelli, è necessario derivare dalla classe Control o da una delle relative classi derivate (diverse da UserControl).

Vantaggi della derivazione dall'oggetto UserControl

Si consideri di utilizzare la derivazione da UserControl in presenza di tutte le condizioni seguenti:

  • Si desidera compilare il controllo in modo analogo alla compilazione di un'applicazione.

  • Il controllo è costituito esclusivamente da componenti esistenti.

  • Non è necessario supportare una personalizzazione complessa.

Derivazione dall'oggetto Control

La derivazione dalla classe Control è il modello utilizzato dalla maggior parte dei controlli WPF esistenti. Quando si crea un controllo che eredita dalla classe Control, è possibile definirne l'aspetto mediante modelli. In tal modo, la logica operativa viene separata dalla rappresentazione visiva. È inoltre possibile ottenere la separazione tra logica e interfaccia utente mediante l'uso di comandi e associazioni anziché di eventi, evitando laddove possibile riferimenti agli elementi di ControlTemplate.  Se la logica e l'interfaccia utente del controllo sono separate in modo appropriato, l'utente del controllo sarà in grado di ridefinire l'oggetto ControlTemplate del controllo per personalizzarne l'aspetto. Sebbene la compilazione di un oggetto Control personalizzato non sia semplice come la compilazione di UserControl, un oggetto Control personalizzato offre la massima flessibilità.

Vantaggi della derivazione dall'oggetto Control

Si consideri di utilizzare la derivazione da Control anziché la classe UserControl in presenza di una qualsiasi delle condizioni seguenti:

  • Si desidera che l'aspetto del controllo sia personalizzabile tramite ControlTemplate.

  • Si desidera che il controllo supporti temi diversi.

Derivazione dall'oggetto FrameworkElement

I controlli che derivano da UserControl o Control si basano sulla composizione di elementi esistenti. Per molti scenari si tratta di una soluzione accettabile, poiché qualsiasi oggetto che eredita da FrameworkElement può essere incluso in ControlTemplate. A volte, tuttavia, le funzionalità della composizione semplice di elementi non sono sufficienti per definire l'aspetto di un controllo. Per questi scenari, basare un componente su FrameworkElement è la scelta giusta.

Esistono due metodi standard per compilare componenti basati su FrameworkElement: rendering diretto e composizione di elementi personalizzati. Il rendering diretto comporta l'esecuzione dell'override del metodo OnRender di FrameworkElement e la specifica di operazioni DrawingContext che definiscono in modo esplicito gli elementi visivi del componente. Questo è il metodo utilizzato da Image e Border. La composizione di elementi personalizzati comporta l'utilizzo di oggetti di tipo Visual per comporre l'aspetto del componente. Per un esempio, vedere Utilizzo degli oggetti DrawingVisual. Track è un esempio di controllo WPF che utilizza la composizione di elementi personalizzati. È inoltre possibile combinare il rendering diretto e la composizione di elementi personalizzati nello stesso controllo.

Vantaggi della derivazione dall'oggetto FrameworkElement

Si consideri di utilizzare la derivazione da FrameworkElement in presenza di una qualsiasi delle condizioni seguenti:

  • Si desidera disporre di un controllo più preciso sull'aspetto del controllo oltre a quello offerto dalla semplice composizione di elementi.

  • Si desidera definire l'aspetto del controllo definendo una logica di rendering personalizzata.

  • Si desidera comporre gli elementi esistenti in modalità nuove che vanno oltre le possibilità offerte da UserControl e Control.

Nozioni fondamentali sulla modifica dei controlli

Come illustrato in precedenza, tra le funzionalità più efficaci di WPF vi è la possibilità di andare oltre l'impostazione delle proprietà di base di un controllo per modificarne l'aspetto e il comportamento senza dover necessariamente creare un controllo personalizzato. Le funzionalità di applicazione di stili, associazione dati e trigger sono possibili grazie al sistema di proprietà WPF e al sistema di eventi WPF. Se nel controllo personalizzato si implementano le proprietà di dipendenza e gli eventi indirizzati, gli utenti potranno utilizzare tali funzionalità come se si trattasse di un controllo fornito con WPF, indipendentemente dal modello utilizzato per creare il controllo personalizzato. 

Utilizzare proprietà di dipendenza

Con le proprietà di dipendenza è possibile eseguire le operazioni indicate di seguito:

  • Impostare la proprietà in uno stile.

  • Associare la proprietà a un'origine dati.

  • Utilizzare una risorsa dinamica come valore della proprietà.

  • Animare la proprietà.

Perché una proprietà del controllo supporti una di tali funzionalità, è necessario implementarla come proprietà di dipendenza. Nell'esempio riportato di seguito viene definita una proprietà di dipendenza denominata Value effettuando le operazioni seguenti:

  • Definizione di un identificatore DependencyProperty denominato ValueProperty come campo staticpublic readonly.

  • Registrazione del nome della proprietà nel sistema di proprietà mediante una chiamata a DependencyProperty.Register per specificare gli elementi seguenti:

    • Nome della proprietà.

    • Tipo della proprietà.

    • Tipo proprietario della proprietà.

    • Metadati della proprietà. I metadati contengono il valore predefinito della proprietà, CoerceValueCallback e PropertyChangedCallback.

  • Definizione di una proprietà del "wrapper" CLR denominata Value, ovvero lo stesso nome utilizzato per la registrazione della proprietà di dipendenza, mediante l'implementazione delle funzioni di accesso get e set della proprietà. Si noti che le funzioni di accesso get e set sono unicamente in grado di chiamare rispettivamente GetValue e SetValue. È consigliabile che le funzioni di accesso delle proprietà di dipendenza non contengano logica aggiuntiva poiché i client e WPF possono ignorare le funzioni di accesso e chiamare direttamente GetValue e SetValue. Ad esempio, quando una proprietà è associata a un'origine dati, la funzione di accesso set della proprietà non viene chiamata. Anziché aggiungere logica aggiuntiva alle funzioni di accesso get e set, utilizzare i delegati ValidateValueCallback, CoerceValueCallback e PropertyChangedCallback per rispondere o controllare il valore in caso di modifica. Per ulteriori informazioni su tali callback, vedere Callback e convalida delle proprietà di dipendenza.

  • Definizione di un metodo per CoerceValueCallback denominato CoerceValue. CoerceValue fa in modo che Value sia maggiore o uguale a MinValue e minore o uguale a MaxValue.

  • Definizione di un metodo per PropertyChangedCallback denominato OnValueChanged. OnValueChanged crea un oggetto RoutedPropertyChangedEventArgs<T> e si prepara a generare l'evento indirizzato ValueChanged. Gli eventi indirizzati verranno illustrati nella sezione successiva.

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{          
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;         

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}

Per ulteriori informazioni, vedere Proprietà Dependency personalizzate.

Utilizzo di eventi indirizzati

Analogamente alle proprietà di dipendenza, che estendono il concetto di proprietà CLR con funzionalità aggiuntive, gli eventi indirizzati estendono il concetto di eventi CLR standard. Quando si crea un nuovo controllo WPF, è anche consigliabile implementare l'evento come evento indirizzato, in grado di supportare il comportamento seguente:

  • Possibilità di gestire eventi in un elemento padre di più controlli. Se si tratta di un evento di bubbling, un solo elemento padre nella struttura ad albero dell'elemento è in grado di sottoscrivere l'evento. Gli autori dell'applicazione potranno quindi utilizzare un solo gestore per rispondere all'evento di più controlli. Se, ad esempio, il controllo fa parte di ciascun elemento di ListBox in quanto incluso in DataTemplate, lo sviluppatore di applicazioni potrà definire il gestore eventi per l'evento del controllo in ListBox. Il gestore eventi viene chiamato ogni volta che l'evento si verifica in uno dei controlli.

  • Possibilità di utilizzare eventi indirizzati in EventSetter, che consente agli sviluppatori di applicazioni di specificare il gestore di un evento all'interno di uno stile.

  • Possibilità di utilizzare eventi indirizzati in EventTrigger, che consente l'animazione di proprietà mediante XAML. Per ulteriori informazioni, vedere Cenni preliminari sull'animazione.

Nell'esempio seguente viene definito un evento indirizzato effettuando le operazioni seguenti:

  • Definizione di un identificatore RoutedEvent denominato ValueChangedEvent come campo staticpublic readonly.

  • Registrazione di un evento indirizzato mediante la chiamata al metodo EventManager.RegisterRoutedEvent. Nell'esempio vengono specificate le informazioni indicate di seguito nella chiamata a RegisterRoutedEvent:

    • Il nome dell'evento è ValueChanged.

    • La strategia di routing è Bubble: vale a dire che viene chiamato innanzitutto un gestore eventi nell'origine (l'oggetto che genera l'evento); vengono quindi chiamati in successione i gestori eventi negli elementi padre dell'origine, a partire dal gestore eventi nell'elemento padre più vicino.

    • Il tipo del gestore eventi è RoutedPropertyChangedEventHandler<T>, costruito con un tipo Decimal.

    • Il tipo proprietario dell'evento è NumericUpDown.

  • Dichiarazione di un evento pubblico denominato ValueChanged e inclusione di dichiarazioni delle funzioni di accesso agli eventi. Nell'esempio viene chiamato l'oggetto AddHandler nella dichiarazione della funzione di accesso add e RemoveHandler nella dichiarazione della funzione di accesso remove per utilizzare i servizi eventi di WPF.

  • Creazione di un metodo virtuale protetto denominato OnValueChanged che genera l'evento ValueChanged.

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble, 
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}

Per ulteriori informazioni, vedere°Cenni preliminari sugli eventi indirizzati e Procedura: creare un evento indirizzato personalizzato.

Utilizzare l'associazione

Per separare l'interfaccia utente del controllo dalla relativa logica, è possibile utilizzare l'associazione dati. Tale operazione risulta particolarmente importante se si definisce l'aspetto del controllo mediante ControlTemplate. L'utilizzo dell'associazione dati consente di eliminare la necessità di fare riferimento a parti specifiche dell'interfaccia utente dal codice. È consigliabile non fare riferimento a elementi presenti in ControlTemplate poiché quando il codice fa riferimento a elementi presenti in ControlTemplate e ControlTemplate viene modificato, l'elemento a cui si fa riferimento deve essere incluso nel nuovo oggetto ControlTemplate.

Nell'esempio riportato di seguito viene aggiornato l'oggetto TextBlock del controllo NumericUpDown mediante l'assegnazione di un nome e l'uso del nome nel codice per fare riferimento alla casella di testo.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}

Negli esempi riportati di seguito viene utilizzata l'associazione per ottenere lo stesso risultato.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

Per ulteriori informazioni sull'associazione dati, vedere Cenni preliminari sull'associazione dati.

Definizione e utilizzo dei comandi

Allo scopo di implementare le funzionalità, si consiglia di definire e utilizzare comandi anziché gestire eventi. Quando si utilizzano i gestori eventi nel controllo, l'azione eseguita all'interno del gestore eventi non è accessibile alle applicazioni. Quando si implementano i comandi nel controllo, le applicazioni possono accedere alle funzionalità che altrimenti non sarebbe disponibili.

L'esempio seguente fa parte di un controllo che gestisce l'evento Click di due pulsanti che modificano il valore del controllo NumericUpDown. Indipendentemente dal fatto che il controllo sia un oggetto UserControl o un oggetto Control con un oggetto ControlTemplate, l'interfaccia utente e la logica sono strettamente collegate poiché il controllo utilizza i gestori eventi.

    <RepeatButton Name="upButton" Click="upButton_Click" 
                  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
                  
    <RepeatButton Name="downButton" Click="downButton_Click" 
                  Grid.Column="1" Grid.Row="1">Down</RepeatButton>
private void upButton_Click(object sender, EventArgs e)
{
        Value++;
}

private void downButton_Click(object sender, EventArgs e)
{
        Value--;
}

Nell'esempio seguente vengono definiti due comandi che modificano il valore del controllo NumericUpDown.

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    Value++;
}
protected virtual void OnDecrease()
{
    Value--;
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

Gli elementi nel modello possono quindi fare riferimento ai comandi, come illustrato nell'esempio riportato di seguito.

<RepeatButton 
    Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
    Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton 
    Command="{x:Static local:NumericUpDown.DecreaseCommand}"  
    Grid.Column="1" Grid.Row="1">Down</RepeatButton>

Le applicazioni possono ora fare riferimento alle associazioni per accedere alle funzionalità che non erano accessibili quando il controllo utilizzava i gestori eventi. Per ulteriori informazioni sui comandi, vedere Cenni preliminari sull'esecuzione di comandi.

Specificare che un elemento è obbligatorio in un oggetto ControlTemplate

Nelle sezioni precedenti è stato illustrato come utilizzare l'associazione dati e i comandi affinché un controllo non faccia riferimento agli elementi presenti nell'oggetto ControlTemplate dal codice. Tuttavia, possono verificarsi condizioni in cui è inevitabile fare riferimento a un elemento. Se si verifica questa situazione, è necessario applicare l'oggetto TemplatePartAttribute al controllo. Questo attributo indica agli autori del modello i tipi e i nomi degli elementi presenti nell'oggetto ControlTemplate. Non tutti gli elementi presenti in un oggetto ControlTemplate devono essere denominati in un oggetto TemplatePartAttribute. In realtà, è preferibile denominare quanti meno elementi possibili. Se però si fa riferimento all'elemento nel codice, è necessario utilizzare l'oggetto TemplatePartAttribute.

Per ulteriori informazioni sulla progettazione di un controllo che utilizza un oggetto ControlTemplate, vedere Linee guida per la progettazione di controlli a cui è possibile applicare degli stili.

Progettazione per le finestre di progettazione

Per ottenere supporto per i controlli WPF personalizzati in Progettazione Windows Presentation Foundation (WPF) per Visual Studio (ad esempio, la modifica delle proprietà con la finestra Proprietà), attenersi alle linee guida riportate di seguito. Per ulteriori informazioni sullo sviluppo per WPF Designer, vedere Progettazione WPF.

Proprietà di dipendenza

Implementare le funzioni di accesso CLR get e set come descritto in precedenza in "Utilizzare proprietà di dipendenza". Nelle finestre di progettazione è possibile utilizzare il wrapper per rilevare la presenza di una proprietà di dipendenza, ma, come accade per WPF e per i client del controllo, non è necessario chiamare le funzioni di accesso quando si ottiene o si imposta la proprietà.

Proprietà associate

È necessario implementare le proprietà associate sui controlli personalizzati attenendosi alle linee guida riportate di seguito:

  • Disporre di un oggetto DependencyProperty static public readonly nel formato NomeProprietàProperty creato utilizzando il metodo RegisterAttached. Il nome della proprietà passato a RegisterAttached deve corrispondere a NomeProprietà.

  • Implementare una coppia di metodi CLR publicstatic denominati SetNomeProprietà e GetNomeProprietà. Entrambi i metodi devono accettare una classe derivata da DependencyProperty come primo argomento. Il metodo SetNomeProprietà accetta anche un argomento il cui tipo corrisponde al tipo di dati registrato per la proprietà. Il metodo GetNomeProprietà deve restituire un valore dello stesso tipo. In assenza del metodo SetNomeProprietà, la proprietà viene contrassegnata come di sola lettura.

  • SetNomeProprietà e GetNomeProprietà devono essere indirizzati rispettivamente ai metodi GetValue e SetValue sull'oggetto di dipendenza di destinazione in modo diretto. Le finestre di progettazione consentono di accedere alla proprietà associata tramite chiamate rivolte al wrapper del metodo oppure tramite chiamata diretta all'oggetto di dipendenza di destinazione.

Per ulteriori informazioni sulle proprietà associate, vedere Cenni preliminari sulle proprietà associate.

Definizione e utilizzo di risorse condivise per il controllo

È possibile includere il controllo nello stesso assembly dell'applicazione o è possibile inserire il controllo in un package di un assembly separato che può essere utilizzato in più applicazioni. Nella maggior parte dei casi, le informazioni riportate in questo argomento si applicano indipendentemente dal metodo utilizzato. È importante tuttavia notare una differenza. Quando si inserisce un controllo nello stesso assembly di un'applicazione, è possibile aggiungere le risorse globali al file app.xaml. Il file app.xaml non è invece disponibile per gli assembly contenenti solo controlli che non dispongono di un oggetto Application ad essi associato.

Quando un'applicazione cerca una risorsa, la ricerca viene effettuata a tre livelli nell'ordine seguente:

  1. Livello elemento: il sistema inizia dall'elemento che fa riferimento alla risorsa, quindi ricerca le risorse dell'elemento padre logico continuando fino a raggiungere l'elemento radice.

  2. Livello applicazione: risorse definite dall'oggetto Application.

  3. Livello tema: i dizionari a livello tema vengono archiviati in una sottocartella denominata Themes. I file nella cartella Themes corrispondono ai temi. Potrebbero ad esempio essere presenti Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml e così via. È inoltre possibile disporre di un file denominato generic.xaml. Quando il sistema cerca una risorsa a livello tema, la ricerca viene effettuata prima nel file del tema specifico e poi nel file generic.xaml.

Quando il controllo si trova in un assembly separato dall'applicazione, è necessario inserire le risorse globali a livello elemento o a livello tema. Entrambi i metodi presentano dei vantaggi.

Definizione di risorse al livello elemento

È possibile definire le risorse condivise a livello elemento creando un dizionario risorse personalizzato e unendolo al dizionario risorse del controllo. Quando si utilizza questo metodo, è possibile assegnare al file di risorse qualsiasi nome e posizionarlo nella stessa cartella dei controlli. Le risorse a livello elemento possono inoltre utilizzare semplici stringhe come chiavi. Nell'esempio seguente viene creato un file di risorse di LinearGradientBrush denominato Dictionary1.XAML.

<ResourceDictionary 
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>

</ResourceDictionary>

Una volta definito il dizionario, è necessario unirlo al dizionario risorse del controllo. Questa operazione può essere effettuata mediante XAML o il codice.

Nell'esempio seguente viene unito un dizionario risorse mediante XAML.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

Questo approccio comporta però la creazione di un oggetto ResourceDictionary ogni volta che vi si fa riferimento. Ad esempio, se si dispone di 10 controlli personalizzati nella libreria e si uniscono i dizionari delle risorse condivise di ogni controllo mediante XAML, si creano 10 oggetti ResourceDictionary identici. È possibile evitare questo problema creando una classe statica che unisce le risorse nel codice e restituisce l'oggetto ResourceDictionary risultante.

Nell'esempio seguente viene creata una classe che restituisce un oggetto ResourceDictionary condiviso.

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml", 
                                    System.UriKind.Relative);

                _sharedDictionary = 
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

Nell'esempio seguente viene unita la risorsa condivisa alle risorse di un controllo personalizzato nel costruttore del controllo prima che venga chiamato InitilizeComponent. Poiché SharedDictionaryManager.SharedDictionary è una proprietà statica, l'oggetto ResourceDictionary viene creato solo una volta. Poiché il dizionario risorse è stato unito prima che venisse chiamato InitializeComponent, le risorse sono disponibili al controllo nel file XAML.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();

}

Definizione di risorse al livello tema

WPF consente di creare risorse per diversi temi di Windows.  Come autore dei controlli, è possibile definire una risorsa per un tema specifico in modo da modificare l'aspetto del controllo a seconda del tema utilizzato. Ad esempio, l'aspetto di un oggetto Button nel tema Windows Classico, il tema predefinito per Windows 2000, differisce da un oggetto Button nel tema Windows Luna, il tema predefinito per Windows XP, poiché l'oggetto Button utilizza un oggetto ControlTemplate diverso per ogni tema.  

Le risorse specifiche di un tema vengono inserite in un dizionario risorse con un nome file specifico. Questi file devono risiedere in una cartella denominata Themes che è una sottocartella della cartella contenente il controllo. Nella tabella seguente sono elencati i file del dizionario risorse e il tema associato a ogni file:

Nome del file del dizionario risorse

Tema di Windows

Classic.xaml

Aspetto "classico" di Windows 9x/2000 in Windows XP

Luna.NormalColor.xaml

Tema blu predefinito in Windows XP

Luna.Homestead.xaml

Tema verde oliva in Windows XP

Luna.Metallic.xaml

Tema argento in Windows XP

Royale.NormalColor.xaml

Tema predefinito in Windows XP Media Center Edition

Aero.NormalColor.xaml

Tema predefinito in Windows Vista

Non è necessario definire una risorsa per ogni tema. Se non viene definita una risorsa per un tema specifico, il controllo utilizza la risorsa generica che risiede in un file del dizionario risorse denominato generic.xaml nella stessa cartella dei file dei dizionari risorse specifici del tema. Sebbene generic.xaml non corrisponda a un tema di Windows specifico, è ancora un dizionario a livello tema.

Esempio di controllo personalizzato NumericUpDown con supporto per temi e animazione interfaccia utente contiene due dizionari risorse per il controllo NumericUpDown: uno in generic.xaml e l'altro in Luna.NormalColor.xaml. È possibile eseguire l'applicazione e passare dal tema argento in Windows XP a un altro tema per vedere la differenza tra i due modelli del controllo. Se si utilizza Windows Vista, è possibile rinominare Luna.NormalColor.xaml in Aero.NormalColor.xaml e passare da un tema all'altro, ad esempio Windows Classic e il tema predefinito per Windows Vista.

Quando si inserisce un oggetto ControlTemplate in uno dei file dei dizionari risorse specifici del tema, è necessario creare un costruttore statico per il controllo e chiamare il metodo OverrideMetadata(Type, PropertyMetadata) su DefaultStyleKey, come illustrato nell'esempio seguente.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

Definizione delle chiavi e relativo riferimento per le risorse del tema

Quando si definisce una risorsa a livello elemento, è possibile assegnare una stringa come chiave e accedere alla risorsa tramite la stringa. Quando si definisce una risorsa a livello tema, è necessario utilizzare un oggetto ComponentResourceKey come chiave. Nell'esempio seguente viene definita una risorsa nel file generic.xaml.

Nell'esempio seguente viene fatto riferimento alla risorsa specificando l'oggetto ComponentResourceKey come chiave.

Specifica del percorso delle risorse del tema

Per trovare le risorse per un controllo, è necessario indicare all'applicazione host che l'assembly contiene risorse specifiche del controllo. È possibile eseguire questa operazione aggiungendo l'oggetto ThemeInfoAttribute all'assembly che contiene il controllo. L'oggetto ThemeInfoAttribute contiene una proprietà GenericDictionaryLocation che specifica il percorso delle risorse generiche e una proprietà ThemeDictionaryLocation che specifica il percorso delle risorse specifiche del tema.

Nell'esempio seguente vengono impostate le proprietà GenericDictionaryLocation e ThemeDictionaryLocation su SourceAssembly, per specificare che le risorse generiche e quelle specifiche del tema risiedono nello stesso assembly del controllo.

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
           ResourceDictionaryLocation.SourceAssembly)]

Eredità da UserControl rispetto all'utilizzo di ControlTemplate

Diversi esempi dimostrano metodi differenti per la scrittura e l'assemblaggio del controllo NumericUpDown. In Esempio di controllo utente NumericUpDown con proprietà di dipendenza ed eventi indirizzati, NumericUpDown eredita da UserControl; in Esempio di controllo personalizzato NumericUpDown con supporto per temi e animazione interfaccia utente, NumericUpDown eredita da Control e utilizza un oggetto ControlTemplate. In questa sezione vengono descritte brevemente alcune differenze tra i due metodi e viene illustrato il motivo per cui il controllo che utilizza un oggetto ControlTemplate è più estendibile.

La differenza principale consiste nel fatto che NumericUpDown che eredita da UserControl non utilizza un oggetto ControlTemplate mentre il controllo che eredita direttamente da Control lo utilizza. Nell'esempio seguente viene illustrato il codice XAML del controllo che eredita da UserControl. Come si può notare, il codice XAML è molto simile a quello risultante quando si crea un'applicazione e si inizia a utilizzare un oggetto Window o Page.

<!--XAML for NumericUpDown that inherits from UserControl.-->
<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUserControl">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
                Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <!--Bind the TextBlock to the Value property-->
            <TextBlock 
                Width="60" TextAlignment="Right" Padding="5"
                Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                               AncestorType={x:Type local:NumericUpDown}}, 
                               Path=Value}"/>

        </Border>

        <RepeatButton Name="upButton" Click="upButton_Click" 
                      Grid.Column="1" Grid.Row="0">Up</RepeatButton>

        <RepeatButton Name="downButton" Click="downButton_Click" 
                      Grid.Column="1" Grid.Row="1">Down</RepeatButton>

    </Grid>
</UserControl>

Nell'esempio seguente viene illustrato il codice ControlTemplate del controllo che eredita da Control. L'oggetto ControlTemplate è simile al codice XAML dell'oggetto UserControl, salvo alcune differenze di sintassi.

<!--ControlTemplate for NumericUpDown that inherits from
    Control.-->
<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="HorizontalAlignment" Value="Center"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Margin="3">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>

          <Border BorderThickness="1" BorderBrush="Gray" 
                  Margin="2" Grid.RowSpan="2" 
                  VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}" 
                       Width="60" TextAlignment="Right" Padding="5"/>
          </Border>

          <RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
                        Grid.Column="1" Grid.Row="0">Up</RepeatButton>

          <RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"
                        Grid.Column="1" Grid.Row="1">Down</RepeatButton>

        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

La differenza maggiore tra i due esempi precedenti consiste nel fatto che nell'esempio in cui viene utilizzato l'oggetto ControlTemplate si ottiene un aspetto personalizzabile mentre nell'esempio che eredita da UserControl ciò non si verifica. Nel caso in cui NumericUpDown eredita da UserControl, l'aspetto del controllo non può essere modificato dallo sviluppatore di applicazioni. In realtà, sebbene NumericUPDown disponga di una proprietà ControlTemplate, poiché UserControl eredita da Control, viene generata un'eccezione in fase di esecuzione se si tenta di impostarla. Diversamente, uno sviluppatore di applicazioni che utilizza l'oggetto NumericUpDown che eredita da Control può creare un nuovo oggetto ControlTemplate per il controllo. È possibile ad esempio creare un oggetto ControlTemplate che posiziona i pulsanti a sinistra e a destra dell'oggetto TextBlock anziché al di sopra e al di sotto dell'oggetto.

La differenza tra i due approcci è evidente nella differenza sintattica degli esempi precedenti. Il controllo che utilizza un oggetto ControlTemplate imposta la proprietà Template in un oggetto Style per NumericUpDown. Si tratta di un metodo comune per la creazione di modelli del controllo. Se si imposta la proprietà Template in uno stile, si specifica che tutte le istanze del controllo dovranno utilizzare tale oggetto ControlTemplate. Gli sviluppatori di applicazioni possono modificare la proprietà Template di un oggetto NumericcUpDown per personalizzarne l'aspetto. Il codice XAML del controllo che eredita da UserControl popola invece la proprietà Content di NumericUpDown (<UserControl.Content> è implicito nel codice XAML). Se uno sviluppatore di applicazioni non può modificare la proprietà Content, l'oggetto NumericUpDown non potrà essere utilizzato.

Un'altra differenza tra gli esempi è costituita dal modo in cui i controlli rispondono ai pulsanti Su e Giù. Il controllo che eredita da UserControl gestisce l'evento Click mentre il controllo che utilizza un oggetto ControlTemplate implementa i comandi ed esegue l'associazione ai comandi nel relativo oggetto ControlTemplate. Di conseguenza, uno sviluppatore di applicazioni che crea un nuovo oggetto ControlTemplate per NumericUpDown può inoltre eseguire l'associazione ai comandi e mantenere la funzionalità del controllo. Se l'oggetto ControlTemplate gestisce l'evento Click anziché l'associazione ai comandi, lo sviluppatore di applicazioni dovrà implementare i gestori eventi quando viene creato un nuovo oggetto ControlTemplate, interrompendo quindi l'incapsulamento di NumericUpDown.

Un'altra differenza è la sintassi dell'associazione tra la proprietà Text dell'oggetto TextBlock e la proprietà Value. Se si utilizza UserControl, l'associazione specifica che RelativeSource è il controllo NumericUpDown padre e viene eseguita l'associazione alla proprietà Value. Se invece si utilizza ControlTemplate, RelativeSource è il controllo a cui appartiene il modello. Viene eseguita la stessa operazione utilizzando però una sintassi di associazione diversa nei due esempi.

In Esempio di controllo personalizzato NumericUpDown con supporto per temi e animazione interfaccia utente, il controllo NumericUpDown risiede in un assembly separato dall'applicazione e definisce e utilizza le risorse a livello tema, mentre in Esempio di controllo utente NumericUpDown con proprietà di dipendenza ed eventi indirizzati, il controllo NumericUpDown risiede nello stesso assembly dell'applicazione e non definisce o utilizza le risorse a livello tema.

Infine, Esempio di controllo personalizzato NumericUpDown con supporto per temi e animazione interfaccia utente mostra come creare un oggetto AutomationPeer per il controllo NumericUpDown. Per ulteriori informazioni sul supporto di automazione interfaccia utente per i controlli personalizzati, vedere Automazione interfaccia utente di un controllo personalizzato WPF.

Vedere anche

Concetti

URI di tipo pack in Windows Presentation Foundation

Altre risorse

Progettazione WPF

Personalizzazione dei controlli

Esempi di personalizzazione dei controlli