Utilizzare gli stili in Windows Presentation Foundation

Di Corrado Cavalli, Microsoft MVP

I fogli di stile (css) sono da lungo tempo disponibili agli sviluppatori WEB i quali sanno benissimo quali sono i vantaggi derivanti dal loro utilizzo, era perciò facile prevedere l’introduzione dello stesso concetto, anche se in forma riveduta e corretta, in Windows Presentation Foundation.

In questa pagina

Stilizzare un’applicazione Windows Forms Stilizzare un’applicazione Windows Forms
Definizione e applicazione di uno stile in WPF Definizione e applicazione di uno stile in WPF
Triggers e Stili Triggers e Stili
Visibilità di uno stile Visibilità di uno stile
Gestione dinamica degli stili Gestione dinamica degli stili
Conclusioni Conclusioni

Stilizzare un’applicazione Windows Forms

Iniziamo supponendo di volere unificare alcuni aspetti grafici di un’applicazione Windows Forms, ad esempio supponendo di volere che il colore di sfondo di tutte le textbox contenute in un Form sia di colore giallo. Per ottenere questo abbiamo a disposizione diverse soluzioni, partendo dal modificare singolarmente la proprietà Backcolor di ogni singola textbox attraverso la finestra delle proprietà di ogni singolo controllo, fino a una routine più sofisticata tipo:

C#
public void ApplyStyle(Form source)
 {
  foreach (Control ctrl in source.Controls)
  {
    TextBox txt = ctrl as TextBox;
    if (txt != null) txt.BackColor = Color.Yellow;
  }
 }

FakePre-e93d3333d0e64329902130c02b2efe09-a5ce9c7c092c4cd8b16b8454ecab462f

La quale imposta a giallo il colore di sfondo delle textbox contenute in un form.

La soluzione appena proposta ha però qualche limitazione:

  • Il colore non è parametrizzato.

  • Il colore è applicato a tutte le textbox indistintamente.

  • Non modifica il colore delle textbox contenute all’interno di eventuali controlli utente.

Ovviamente è possibile ovviare a questi limiti ma, come facilmente intuite, man mano che lo scenario si complica (ad esempio perché le proprietà da impostare sono parecchie) il codice necessario per poter “stilizzare” un’applicazione Winform cresce a dismisura.
Vediamo com’è possibile ottenere lo stesso risultato in un’applicazione Windows Presentation Foundation (WPF).

 

Definizione e applicazione di uno stile in WPF

In WPF uno stile è definito come un insieme di proprietà e azioni associate a un elemento, non a caso la classe FrameworkElement, dalla quale ereditano tutti i controlli, espone una proprietà Style grazie alla quale questo frammento di XAML:

<TextBox Background="Yellow" FontFamily="Tahoma">Hello</TextBox>

Può essere riscritto in questa nuova forma applicando quello che viene definito Inline Style.

<TextBox>Hello
  <TextBox.Style>
   <Style>
<Setter Property="Control.Background" Value="Yellow" />
<Setter Property="Control.FontFamily" Value="Tahoma" />
   </Style>
 </TextBox.Style>

Ovviamente quest’approccio, oltre che essere significativamente più verboso rispetto al precedente, ha come ulteriore svantaggio quello che lo stile non è condivisibile da più controlli, ecco perché gli inlines styles sono poco utilizzati e generalmente sostituiti dai Named Styles, ovvero da stili ai quali è associato un identificativo e che sono definiti all’interno di un area che ne permette la condivisione, nello specifico l’area delle risorse di un FrameworkElement. Nell’esempio che segue, l’area utilizzata è quella dell’elemento Window rendendo lo stile disponibile a tutti i controlli contenuti nella finestra stessa.

<Window x:Class="StyleOnResources.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="StyleOnResources" Height="300" Width="300"
    >
<Window.Resources>
<Style x:Key="MyStyle">
<Setter Property="Control.Background" Value="Yellow" />
<Setter Property="Control.FontFamily" Value="Tahoma" />
</Style>
</Window.Resources>
<StackPanel>
<TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
<TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
</StackPanel>
</Window>

L’esempio definisce uno stile denominato MyStyle il quale, attraverso l’estensione di markup {StaticResource} è associato alla proprietà Style di entrambe le textbox che quindi condivideranno lo stesso aspetto. In caso di modifica a runtime delle caratteristiche di uno stile queste non saranno propagate ai rispettivi controlli perché StaticResource non tiene traccia di eventuali cambiamenti, per ottenere questa funzionalità StaticResource deve essere sostituita dall’estensione di markup DynamicResource .

Come potete notare, ora le proprietà applicate alle textbox sono definite in un unico punto all’interno dello stile, e avendo prefissato il nome della proprietà con la classe Control (esempio: Control.Background) lo stesso stile può essere applicato anche a controlli diversi.
Ad esempio, potremmo modificare il contenuto dello StackPanel in questo modo:

<StackPanel>
  <TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
  <TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
  <CheckBox Style="{StaticResource MyStyle}" IsChecked="True" Margin="10">Admin</CheckBox>
</StackPanel>

Ottenendo il risultato di figura 1.


Figura 1

Volendo è possibile realizzare degli stili misti, cioè che contengono proprietà dipendenti da controlli diversi come nell’esempio che segue:

<Style x:Key="MyStyle">
<Setter Property="Control.Background" Value="Yellow" />
<Setter Property="Control.FontFamily" Value="Tahoma" />
<Setter Property="CheckBox.IsChecked" Value="true" />
</Style>

In questo caso le proprietà non applicabili a un determinato tipo di controllo vengono ignorate, è però evidente che uno stile di questo tipo è abbastanza “anomalo” in quanto per uniformità è consuetudine associare ad ogni tipo il relativo stile.
Un aspetto sicuramente interessante è la possibilità di avere stili che ereditano da altri, quindi l’esempio precedente potrebbe essere riscritto come segue, ottenendo lo stesso risultato ma con un’organizzazione decisamente migliore.

<Window.Resources>
  <Style x:Key="MyStyle">
  <Setter Property="Control.Background" Value="Yellow" />
  <Setter Property="Control.FontFamily" Value="Tahoma" />
 </Style>
 <Style x:Key="CheckBoxStyle" BasedOn="{StaticResource MyStyle}">
  <Setter Property="CheckBox.IsChecked" Value="true" />
 </Style>
</Window.Resources>

<StackPanel>
  <TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
  <TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
  <CheckBox Style="{StaticResource CheckBoxStyle}" Margin="10">Admin</CheckBox>
</StackPanel>

Onde evitare di dover prefissare ogni proprietà con il nome della classe e per rafforzare l’associazione di uno stile con un ben preciso elemento è possibile definire, a livello di stile, il tipo destinatario come mostrato nel seguente esempio:

<Window.Resources>
<Style x:Key="MyStyle" TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="Yellow" />
<Setter Property="FontFamily" Value="Tahoma" />
</Style>
<Style x:Key="CheckBoxStyle" TargetType="{x:Type CheckBox}">
<Setter Property="Background" Value="Yellow" />
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="IsChecked" Value="true" />
</Style>
<Style x:Key="MyStyle2" TargetType="{x:Type TextBox}" BasedOn="{StaticResource MyStyle}">
<Setter Property="Background" Value="Red" />
</Style>
</Window.Resources>
<StackPanel>
<TextBox Style="{StaticResource MyStyle}" Margin="10">Hello</TextBox>
<TextBox Style="{StaticResource MyStyle2}" Margin="10">Hello</TextBox>
<CheckBox Style="{StaticResource CheckBoxStyle}" Margin="10">Admin</CheckBox>
</StackPanel>

In questo caso lo stile dal quale ereditare deve essere di tipo compatibile e non è possibile associare a determinato controllo uno stile che ha proprietà TargetType incompatibile. Ad esempio, utilizzando lo stile precedente, questo frammento di XAML non compila:

<CheckBox Style="{StaticResource MyStyle}" Margin="10">Admin</CheckBox>

Definendo la sola proprietà TargetType si ottiene quello che è definito “stile di tipo” il quale viene automaticamente applicato a tutti gli elementi che sono dello stesso tipo definito in TargetType.
L’esempio che segue dimostra come lo stile sia applicato indistintamente a entrambe le textbox contenute nella finestra.

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="Yellow" />
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="10" />
</Style>
</Window.Resources>

<StackPanel>
<TextBox>Hello</TextBox>
<TextBox>Hello</TextBox>
</StackPanel>

Il risultato è visibile in figura 2:


Figura 2

Nel caso in cui si voglia specializzare un controllo, oppure evitare che a un controllo sia applicato un determinato stile di tipo è possibile agire sul controllo stesso come mostrato di seguito dove solo la textbox txt2 utilizzerà lo stile di tipo mostrato in precedenza poiché eventuali impostazioni locali hanno priorità maggiore rispetto allo stile.

<StackPanel>
<TextBox Background="Red" x:Name="txt1">Hello1</TextBox>
<TextBox x:Name="txt2">Hello2</TextBox>
<TextBox Style="{x:Null}" x:Name="txt3">Hello3</TextBox>
</StackPanel>

 

Triggers e Stili

Quanto visto, seppure con una sintassi diversa, non discosta molto rispetto a quanto già disponibile nei fogli di stile WEB, ciò che differenzia gli stili di Windows Presentation Foundation è la possibilità di definire dei Triggers.

Finora i vari setters associati a un trigger venivano applicati incondizionatamente, un trigger permette l’applicazione di un insieme di proprietà in presenza di determinate condizioni.

Le condizioni che determinano l’applicazione di uno stile sono:

  • Valorizzazione di una proprietà (Property Trigger).
  • Cambio di valore di una proprietà associata via databinding (Data Trigger).
  • Generazione di un determinato evento (Event Trigger).

Analizziamo il primo caso supponendo di voler far cambiare il colore di sfondo delle nostre textbox quando ricevono il focus, quello che dobbiamo fare è inserire un trigger nella definizione dello stile come mostrato di seguito:

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="10" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>

<StackPanel>
<TextBox>Hello1</TextBox>
<TextBox>Hello2</TextBox>
</StackPanel>

Ovviamente è possibile avere più Property Triggers all’interno dello stesso stile, l’esempio che segue cambia il colore di sfondo a verde chiaro quanto la textbox ha il focus e modifica la dimensione del font quando il mouse è sopra di essa (vedi figura 3).

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="10" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="LightGreen" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="FontSize" Value="24" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>

Figura 3

Attraverso la definizione di un MultiTrigger è possibile definire più condizioni ognuna delle quali deve essere soddisfatta affinché le relative proprietà vengano applicate.

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="10" />
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsFocused" Value="True" />
<Condition Property="IsMouseOver" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="LightGreen" />
<Setter Property="FontSize" Value="24" />
</MultiTrigger>
</Style.Triggers>
</Style>
</Window.Resources>

<StackPanel>
<TextBox>Hello1</TextBox>
<TextBox>Hello2</TextBox>
</StackPanel>

In questo caso, quando il mouse si troverà sopra una textbox con il focus, il colore di sfondo e la dimensione del font saranno modificati.

I DataTriggers sono triggers dipendenti da proprietà il cui valore è ottenuto mediante DataBinding come nell’esempio che segue:

<Window x:Class="StyleOnResources.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:l="clr-namespace:DemoDataTrigger" 
    Title="Demo" Height="300" Width="300"
    >
<Window.Resources>
<l:Person x:Key="PersonInstance" />

<Style TargetType="{x:Type TextBox}">
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="10" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Name}" Value="Corrado">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>

<StackPanel DataContext="{StaticResource PersonInstance}">
<TextBox x:Name="txt1" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>

La fonte dati è associata alla proprietà DataContext dello StackPanel che contiene la textbox “txt1” ed è rappresentata da un istanza di una classe Person che espone una proprietà Name, la quale è a sua volta in binding con la proprietà Text della textbox. Quando la proprietà Name assume il valore “Corrado”, il DataTrigger scatta e il colore di sfondo della textbox viene modificato in giallo.

L’ultimo esempio di Trigger è legato allo scattare di un determinato evento, l’esempio sotto riportato esegue un’animazione al verificarsi dell’evento MouseEnter della textbox.

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="Yellow" />
<Setter Property="FontFamily" Value="Tahoma" />
<Setter Property="Margin" Value="30" />
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="400" Duration="0:0:0.5" AutoReverse="True" Storyboard.TargetProperty="Width">
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Window.Resources>

<StackPanel>
<TextBox Width="100">Hello</TextBox>
</StackPanel>

Volendo è possibile associare dei gestori evento a tutti gli elementi cha hanno associato un determinato stile mediante l’impiego di un EventSetter. L’esempio che segue invoca la routine HandleMouseEnter quando il mouse entra in una Textbox che ha associato uno stile di tipo.

<Window.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="FontFamily" Value="Tahoma" />
<EventSetter Event="MouseEnter" Handler="HandleMouseEnter" />
</Style>
</Window.Resources>

<StackPanel>
<TextBox />
<TextBox />
</StackPanel>

FakePre-b6a781d47ddd4b72b05ca061c7761b18-d5209c615edf4b38b03a168cb1632273 FakePre-18aa5f2fa1f84f1c86451eaf43bd4653-5bc90fa264b94b13b5f5dbb105918a4d

 

Visibilità di uno stile

Finora abbiamo utilizzato stili definiti all’interno delle risorse di una Window, questo rende disponibili gli stili a tutti gli elementi presenti al suo interno, è però probabile che si voglia renderli condivisibili da tutti gli elementi di un’applicazione, in questo caso è necessario definire gli stessi all’interno del file di applicazione app.xaml, così facendo tutte le textbox presenti nell’applicazione e non dipendenti da altri stili, utilizzeranno quello dichiarato a livello applicativo come nel seguente esempio:

File: app.xaml
<Application x:Class="StyleOnResources.App"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml"
    >
    <Application.Resources>
 <Style TargetType="{x:Type TextBox}">
 <Setter Property="Background" Value="Yellow" />
 <Setter Property="FontFamily" Value="Tahoma" />
 <Setter Property="Margin" Value="10" />
 </Style>
 </Application.Resources>
</Application>

 

Gestione dinamica degli stili

Essendo gli stili parte integrante dell’interfaccia utente è probabile che si voglia poterli sostituire a runtime senza dover ricompilare l’intera applicazione, immaginiamo quindi di voler sostituire lo stile citato in precedenza con un altro a runtime. Partiamo col definire il nuovo stile all’interno di un elemento ResourceDictionary contenuto all’interno del file NewStyle.xaml

File: NewStyle.xaml
<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    >
<Style TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="LightGreen" />
<Setter Property="FontFamily" Value="Times New Roman" />
<Setter Property="FontSize" Value="24" />
<Setter Property="Margin" Value="10" />
</Style>    
</ResourceDictionary>

A questo punto carichiamo a runtime lo stile in un ResourceDictionary:

ResourceDictionary rd=null;
using (FileStream fs = new FileStream("NewStyle.xaml",FileMode.Open,FileAccess.Read))
{
  rd = XamlReader.Load(fs) as ResourceDictionary;
}

E concludiamo sostituendo le risorse di applicazione con quelle contenute nel nuovo ResourceDictionary, con l’attenzione che, tutte le risorse disponibili in precedenza siano comunque disponibili nel file appena caricato in quanto l’associazione che segue sostituisce tutte le risorse di applicazione.

Application.Current.Resources = rd;

 

Conclusioni

La presenza degli stili rende la gestione dell’aspetto dei vari elementi presenti in un’applicazione Windows Presentation Foundation sicuramente più semplice rispetto all’equivalente Windows Forms.
La possibilità di centralizzare tutte le caratteristiche di un controllo in un unico punto, eventualmente esterno al programma stesso con la possibilità di decidere quali condizioni ne determinano l’applicazione apre degli scenari finora difficilmente attuabili, il tutto senza dovere scrivere codice, e quindi realizzabile attraverso strumenti di design come Expression Blend.