Bea Stollnitz ha pubblicato un buon post sul blog sull'utilizzo di un'estensione di markup per questo, sotto la voce "Come posso impostare più stili in WPF?"
Quel blog è morto ora, quindi sto riproducendo il post qui
WPF e Silverlight offrono entrambi la possibilità di derivare uno stile da un altro stile tramite la proprietà "BasedOn". Questa funzionalità consente agli sviluppatori di organizzare i propri stili utilizzando una gerarchia simile all'eredità delle classi. Considera i seguenti stili:
<Style TargetType="Button" x:Key="BaseButtonStyle">
<Setter Property="Margin" Value="10" />
</Style>
<Style TargetType="Button" x:Key="RedButtonStyle" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Foreground" Value="Red" />
</Style>
Con questa sintassi, un pulsante che utilizza RedButtonStyle avrà la proprietà Foreground impostata su Red e la proprietà Margin impostata su 10.
Questa funzione è presente in WPF da molto tempo ed è una novità di Silverlight 3.
Cosa succede se si desidera impostare più di uno stile su un elemento? Né WPF né Silverlight offrono una soluzione pronta per questo problema. Fortunatamente ci sono modi per implementare questo comportamento in WPF, di cui parlerò in questo post sul blog.
WPF e Silverlight utilizzano le estensioni di markup per fornire proprietà con valori che richiedono logica per ottenere. Le estensioni di markup sono facilmente riconoscibili dalla presenza di parentesi graffe che le circondano in XAML. Ad esempio, l'estensione di markup {Binding} contiene la logica per recuperare un valore da un'origine dati e aggiornarlo quando si verificano modifiche; l'estensione di markup {StaticResource} contiene la logica per acquisire un valore da un dizionario di risorse basato su una chiave. Fortunatamente per noi, WPF consente agli utenti di scrivere le proprie estensioni di markup personalizzate. Questa funzione non è ancora presente in Silverlight, quindi la soluzione in questo blog è applicabile solo a WPF.
Altri hanno scritto grandi soluzioni per unire due stili usando le estensioni di markup. Tuttavia, volevo una soluzione che offrisse la possibilità di unire un numero illimitato di stili, che è un po 'più complicato.
Scrivere un'estensione di markup è semplice. Il primo passaggio consiste nel creare una classe derivata da MarkupExtension e utilizzare l'attributo MarkupExtensionReturnType per indicare che si intende che il valore restituito dall'estensione di markup sia di tipo Style.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Specifica degli input per l'estensione di markup
Vorremmo offrire agli utenti della nostra estensione markup un modo semplice per specificare gli stili da unire. Esistono essenzialmente due modi in cui l'utente può specificare input per un'estensione di markup. L'utente può impostare proprietà o passare parametri al costruttore. Poiché in questo scenario l'utente ha bisogno della capacità di specificare un numero illimitato di stili, il mio primo approccio è stato quello di creare un costruttore che prende un numero qualsiasi di stringhe usando la parola chiave "params":
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Il mio obiettivo era poter scrivere gli input come segue:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Notare la virgola che separa i diversi tasti di stile. Sfortunatamente, le estensioni di markup personalizzate non supportano un numero illimitato di parametri del costruttore, quindi questo approccio provoca un errore di compilazione. Se avessi saputo in anticipo quanti stili avrei voluto unire, avrei potuto usare la stessa sintassi XAML con un costruttore che prendesse il numero desiderato di stringhe:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Per ovviare al problema, ho deciso di fare in modo che il parametro del costruttore prendesse una singola stringa che specifica i nomi degli stili separati da spazi. La sintassi non è poi così male:
private string[] resourceKeys;
public MultiStyleExtension(string inputResourceKeys)
{
if (inputResourceKeys == null)
{
throw new ArgumentNullException("inputResourceKeys");
}
this.resourceKeys = inputResourceKeys.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (this.resourceKeys.Length == 0)
{
throw new ArgumentException("No input resource keys specified.");
}
}
Calcolo dell'output dell'estensione di markup
Per calcolare l'output di un'estensione di markup, dobbiamo sovrascrivere un metodo da MarkupExtension chiamato "ProvideValue". Il valore restituito da questo metodo verrà impostato nella destinazione dell'estensione di markup.
Ho iniziato creando un metodo di estensione per Style che sappia unire due stili. Il codice per questo metodo è abbastanza semplice:
public static void Merge(this Style style1, Style style2)
{
if (style1 == null)
{
throw new ArgumentNullException("style1");
}
if (style2 == null)
{
throw new ArgumentNullException("style2");
}
if (style1.TargetType.IsAssignableFrom(style2.TargetType))
{
style1.TargetType = style2.TargetType;
}
if (style2.BasedOn != null)
{
Merge(style1, style2.BasedOn);
}
foreach (SetterBase currentSetter in style2.Setters)
{
style1.Setters.Add(currentSetter);
}
foreach (TriggerBase currentTrigger in style2.Triggers)
{
style1.Triggers.Add(currentTrigger);
}
// This code is only needed when using DynamicResources.
foreach (object key in style2.Resources.Keys)
{
style1.Resources[key] = style2.Resources[key];
}
}
Con la logica sopra, il primo stile viene modificato per includere tutte le informazioni dal secondo. Se ci sono conflitti (ad es. Entrambi gli stili hanno un setter per la stessa proprietà), vince il secondo stile. Si noti che oltre a copiare stili e trigger, ho anche preso in considerazione i valori TargetType e BasedOn e tutte le risorse che il secondo stile potrebbe avere. Per il TargetType dello stile unito, ho usato il tipo più derivato. Se il secondo stile ha uno stile BasedOn, unisco la sua gerarchia di stili in modo ricorsivo. Se ha risorse, le copio nel primo stile. Se si fa riferimento a tali risorse utilizzando {StaticResource}, vengono risolte staticamente prima dell'esecuzione di questo codice di unione e pertanto non è necessario spostarle. Ho aggiunto questo codice nel caso in cui stiamo utilizzando DynamicResources.
Il metodo di estensione mostrato sopra abilita la seguente sintassi:
style1.Merge(style2);
Questa sintassi è utile a condizione che io abbia istanze di entrambi gli stili in ProvideValue. Beh, io no. Tutto quello che ottengo dal costruttore è un elenco di chiavi di stringa per quegli stili. Se ci fosse supporto per i parametri nei parametri del costruttore, avrei potuto usare la sintassi seguente per ottenere le istanze di stile effettive:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Ma questo non funziona. E anche se la limitazione dei parametri non esistesse, probabilmente colpiremmo un'altra limitazione delle estensioni di markup, dove dovremmo usare la sintassi dell'elemento proprietà invece della sintassi dell'attributo per specificare le risorse statiche, che sono verbose e ingombranti (lo spiego bug migliore in un precedente post sul blog ). E anche se entrambe queste limitazioni non esistessero, preferirei comunque scrivere l'elenco di stili usando solo i loro nomi: è più breve e più semplice da leggere rispetto a uno StaticResource per ognuno.
La soluzione è creare un StaticResourceExtension usando il codice. Data una chiave di stile di tipo stringa e un fornitore di servizi, posso usare StaticResourceExtension per recuperare l'istanza di stile effettiva. Ecco la sintassi:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Ora abbiamo tutti i pezzi necessari per scrivere il metodo ProvideValue:
public override object ProvideValue(IServiceProvider serviceProvider)
{
Style resultStyle = new Style();
foreach (string currentResourceKey in resourceKeys)
{
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
if (currentStyle == null)
{
throw new InvalidOperationException("Could not find style with resource key " + currentResourceKey + ".");
}
resultStyle.Merge(currentStyle);
}
return resultStyle;
}
Ecco un esempio completo dell'uso dell'estensione markup MultiStyle:
<Window.Resources>
<Style TargetType="Button" x:Key="SmallButtonStyle">
<Setter Property="Width" Value="120" />
<Setter Property="Height" Value="25" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style TargetType="Button" x:Key="GreenButtonStyle">
<Setter Property="Foreground" Value="Green" />
</Style>
<Style TargetType="Button" x:Key="BoldButtonStyle">
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Window.Resources>
<Button Style="{local:MultiStyle SmallButtonStyle GreenButtonStyle BoldButtonStyle}" Content="Small, green, bold" />