Bea Stollnitz tuvo una buena publicación de blog sobre el uso de una extensión de marcado para esto, bajo el encabezado "¿Cómo puedo configurar varios estilos en WPF?"
Ese blog está muerto ahora, así que estoy reproduciendo la publicación aquí.
WPF y Silverlight ofrecen la posibilidad de derivar un estilo de otro estilo a través de la propiedad "BasedOn". Esta característica permite a los desarrolladores organizar sus estilos usando una jerarquía similar a la herencia de clases. Considere los siguientes estilos:
<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 esta sintaxis, un botón que usa RedButtonStyle tendrá su propiedad de primer plano establecida en rojo y su propiedad de margen establecida en 10.
Esta característica ha estado presente en WPF durante mucho tiempo, y es nueva en Silverlight 3.
¿Qué sucede si desea establecer más de un estilo en un elemento? Ni WPF ni Silverlight proporcionan una solución para este problema fuera de la caja. Afortunadamente, hay formas de implementar este comportamiento en WPF, que analizaré en esta publicación de blog.
WPF y Silverlight usan extensiones de marcado para proporcionar propiedades con valores que requieren cierta lógica para obtener. Las extensiones de marcado son fácilmente reconocibles por la presencia de llaves que las rodean en XAML. Por ejemplo, la extensión de marcado {Binding} contiene lógica para obtener un valor de una fuente de datos y actualizarlo cuando ocurren cambios; la extensión de marcado {StaticResource} contiene lógica para obtener un valor de un diccionario de recursos basado en una clave. Afortunadamente para nosotros, WPF permite a los usuarios escribir sus propias extensiones de marcado personalizadas. Esta característica aún no está presente en Silverlight, por lo que la solución en este blog solo es aplicable a WPF.
Otros han escrito excelentes soluciones para fusionar dos estilos usando extensiones de marcado. Sin embargo, quería una solución que proporcionara la capacidad de fusionar un número ilimitado de estilos, lo cual es un poco más complicado.
Escribir una extensión de marcado es sencillo. El primer paso es crear una clase que se derive de MarkupExtension y usar el atributo MarkupExtensionReturnType para indicar que desea que el valor devuelto por su extensión de marcado sea de tipo Style.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Especificar entradas a la extensión de marcado
Nos gustaría ofrecer a los usuarios de nuestra extensión de marcado una forma sencilla de especificar los estilos que se fusionarán. Básicamente, hay dos formas en que el usuario puede especificar entradas para una extensión de marcado. El usuario puede establecer propiedades o pasar parámetros al constructor. Dado que en este escenario el usuario necesita la capacidad de especificar un número ilimitado de estilos, mi primer enfoque fue crear un constructor que tome cualquier número de cadenas usando la palabra clave "params":
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Mi objetivo era poder escribir las entradas de la siguiente manera:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Observe la coma que separa las diferentes teclas de estilo. Desafortunadamente, las extensiones de marcado personalizadas no admiten un número ilimitado de parámetros de constructor, por lo que este enfoque da como resultado un error de compilación. Si supiera de antemano cuántos estilos quería fusionar, podría haber usado la misma sintaxis XAML con un constructor que toma el número deseado de cadenas:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Como solución alternativa, decidí que el parámetro constructor tome una sola cadena que especifique los nombres de estilo separados por espacios. La sintaxis no es tan mala:
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.");
}
}
Cálculo de la salida de la extensión de marcado
Para calcular la salida de una extensión de marcado, necesitamos anular un método de MarkupExtension llamado "ProvideValue". El valor devuelto por este método se establecerá en el objetivo de la extensión de marcado.
Comencé creando un método de extensión para Style que sabe cómo combinar dos estilos. El código para este método es bastante 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];
}
}
Con la lógica anterior, el primer estilo se modifica para incluir toda la información del segundo. Si hay conflictos (por ejemplo, ambos estilos tienen un setter para la misma propiedad), gana el segundo estilo. Tenga en cuenta que, además de copiar estilos y desencadenantes, también tuve en cuenta los valores TargetType y BasedOn, así como cualquier recurso que pueda tener el segundo estilo. Para el TargetType del estilo combinado, utilicé el tipo más derivado. Si el segundo estilo tiene un estilo BasedOn, fusiono su jerarquía de estilos de forma recursiva. Si tiene recursos, los copio al primer estilo. Si se hace referencia a esos recursos mediante {StaticResource}, se resuelven estáticamente antes de que se ejecute este código de combinación y, por lo tanto, no es necesario moverlos. Agregué este código en caso de que estemos usando DynamicResources.
El método de extensión que se muestra arriba habilita la siguiente sintaxis:
style1.Merge(style2);
Esta sintaxis es útil siempre que tenga instancias de ambos estilos dentro de ProvideValue. Pues yo no. Todo lo que obtengo del constructor es una lista de claves de cadena para esos estilos. Si hubiera soporte para parámetros en los parámetros del constructor, podría haber usado la siguiente sintaxis para obtener las instancias de estilo reales:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Pero eso no funciona. E incluso si la limitación de parámetros no existiera, probablemente alcanzaríamos otra limitación de las extensiones de marcado, donde tendríamos que usar la sintaxis del elemento de propiedad en lugar de la sintaxis del atributo para especificar los recursos estáticos, que es detallado y engorroso (explico esto error mejor en una publicación de blog anterior ). E incluso si ambas limitaciones no existieran, aún preferiría escribir la lista de estilos usando solo sus nombres: es más corto y más fácil de leer que un StaticResource para cada uno.
La solución es crear una StaticResourceExtension usando código. Dada una clave de estilo de tipo cadena y un proveedor de servicios, puedo usar StaticResourceExtension para recuperar la instancia de estilo real. Aquí está la sintaxis:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Ahora tenemos todas las piezas necesarias para escribir el método 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;
}
Aquí hay un ejemplo completo del uso de la extensión de marcado 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" />