Bea Stollnitz a publié un bon article sur l'utilisation d'une extension de balisage pour cela, sous le titre "Comment puis-je définir plusieurs styles dans WPF?"
Ce blog est mort maintenant, donc je reproduis le message ici
WPF et Silverlight offrent tous deux la possibilité de dériver un style d'un autre style via la propriété «BasedOn». Cette fonctionnalité permet aux développeurs d'organiser leurs styles en utilisant une hiérarchie similaire à l'héritage de classe. Considérez les styles suivants:
<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>
Avec cette syntaxe, un Button qui utilise RedButtonStyle aura sa propriété Foreground définie sur Red et sa propriété Margin définie sur 10.
Cette fonctionnalité existe dans WPF depuis longtemps et elle est nouvelle dans Silverlight 3.
Que faire si vous souhaitez définir plusieurs styles sur un élément? Ni WPF ni Silverlight ne fournissent une solution à ce problème prête à l'emploi. Heureusement, il existe des moyens d'implémenter ce comportement dans WPF, dont je parlerai dans ce billet de blog.
WPF et Silverlight utilisent des extensions de balisage pour fournir des propriétés avec des valeurs qui nécessitent une certaine logique pour obtenir. Les extensions de balisage sont facilement reconnaissables par la présence d'accolades les entourant en XAML. Par exemple, l'extension de balisage {Binding} contient une logique permettant d'extraire une valeur d'une source de données et de la mettre à jour lorsque des modifications se produisent; l'extension de balisage {StaticResource} contient une logique permettant de récupérer une valeur dans un dictionnaire de ressources en fonction d'une clé. Heureusement pour nous, WPF permet aux utilisateurs d'écrire leurs propres extensions de balisage personnalisées. Cette fonctionnalité n'est pas encore présente dans Silverlight, donc la solution de ce blog n'est applicable qu'à WPF.
D'autres ont écrit d'excellentes solutions pour fusionner deux styles à l'aide d'extensions de balisage. Cependant, je voulais une solution qui offre la possibilité de fusionner un nombre illimité de styles, ce qui est un peu plus délicat.
L'écriture d'une extension de balisage est simple. La première étape consiste à créer une classe qui dérive de MarkupExtension et à utiliser l'attribut MarkupExtensionReturnType pour indiquer que vous souhaitez que la valeur renvoyée par votre extension de balisage soit de type Style.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Spécification des entrées de l'extension de balisage
Nous aimerions donner aux utilisateurs de notre extension de balisage un moyen simple de spécifier les styles à fusionner. Il existe essentiellement deux façons dont l'utilisateur peut spécifier des entrées dans une extension de balisage. L'utilisateur peut définir des propriétés ou transmettre des paramètres au constructeur. Étant donné que dans ce scénario, l'utilisateur a besoin de la possibilité de spécifier un nombre illimité de styles, ma première approche a été de créer un constructeur qui prend n'importe quel nombre de chaînes en utilisant le mot-clé «params»:
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Mon objectif était de pouvoir écrire les entrées comme suit:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Notez la virgule séparant les différentes clés de style. Malheureusement, les extensions de balisage personnalisées ne prennent pas en charge un nombre illimité de paramètres de constructeur, donc cette approche entraîne une erreur de compilation. Si je savais à l'avance combien de styles je voulais fusionner, j'aurais pu utiliser la même syntaxe XAML avec un constructeur prenant le nombre de chaînes souhaité:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Pour contourner ce problème, j'ai décidé que le paramètre constructeur prenne une seule chaîne qui spécifie les noms de style séparés par des espaces. La syntaxe n'est pas trop mauvaise:
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.");
}
}
Calcul de la sortie de l'extension de balisage
Pour calculer la sortie d'une extension de balisage, nous devons remplacer une méthode de MarkupExtension appelée «provideValue». La valeur renvoyée par cette méthode sera définie dans la cible de l'extension de balisage.
J'ai commencé par créer une méthode d'extension pour Style qui sait fusionner deux styles. Le code de cette méthode est assez simple:
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];
}
}
Avec la logique ci-dessus, le premier style est modifié pour inclure toutes les informations du second. S'il y a des conflits (par exemple, les deux styles ont un setter pour la même propriété), le second style l'emporte. Notez qu'en plus de copier les styles et les déclencheurs, j'ai également pris en compte les valeurs TargetType et BasedOn ainsi que toutes les ressources que le second style peut avoir. Pour le TargetType du style fusionné, j'ai utilisé le type le plus dérivé. Si le deuxième style a un style BasedOn, je fusionne sa hiérarchie de styles de manière récursive. S'il a des ressources, je les copie dans le premier style. Si ces ressources sont référencées à l'aide de {StaticResource}, elles sont résolues statiquement avant l'exécution de ce code de fusion, et il n'est donc pas nécessaire de les déplacer. J'ai ajouté ce code au cas où nous utiliserions DynamicResources.
La méthode d'extension illustrée ci-dessus active la syntaxe suivante:
style1.Merge(style2);
Cette syntaxe est utile à condition que j'aie des instances des deux styles dans FournirValue. Eh bien, non. Tout ce que j'obtiens du constructeur est une liste de clés de chaîne pour ces styles. S'il y avait un support pour les paramètres dans les paramètres du constructeur, j'aurais pu utiliser la syntaxe suivante pour obtenir les instances de style réelles:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Mais ça ne marche pas. Et même si la limitation des paramètres n'existait pas, nous frapperions probablement une autre limitation des extensions de balisage, où nous devrions utiliser la syntaxe d'élément de propriété au lieu de la syntaxe d'attribut pour spécifier les ressources statiques, ce qui est verbeux et encombrant (j'explique cela bug mieux dans un précédent article de blog ). Et même si ces deux limitations n'existaient pas, je préférerais quand même écrire la liste des styles en utilisant uniquement leurs noms - elle est plus courte et plus simple à lire qu'une StaticResource pour chacun.
La solution consiste à créer une StaticResourceExtension à l'aide de code. Étant donné une clé de style de type string et un fournisseur de services, je peux utiliser StaticResourceExtension pour récupérer l'instance de style réelle. Voici la syntaxe:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Nous avons maintenant toutes les pièces nécessaires pour écrire la méthode 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;
}
Voici un exemple complet de l'utilisation de l'extension de balisage 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" />