Search code examples
wpfxamlstyles

XAML Combine styles going beyond BasedOn?


Is there any way to combine mutliple styles in XAML to make a new style that has all of the desired settings?

For example (pseudo code);

<Style x:key="A">
 ...
</Style>

<Style x:key="B">
 ...
</Style>

<Style x:key="Combined">
 <IncludeStyle Name="A"/>
 <IncludeStyle Name="B"/>
 ... other properties.
</Style>

I know that there is a BasedOn property for styles, but that feature will only take you so far. I am really just looking for an easy way (in XAML) to create these 'combined' styles. But like I said before, I doubt it exists, unless anyone has heard of such a thing??


Solution

  • You can make a custom markup extensions that will merge styles properties and triggers into a single style. All you need to do is add a MarkupExtension-derived class to your namespace with the MarkupExtensionReturnType attribute defined and you're off and running.

    Here is an extension that will allow you to merge styles using a "css-like" syntax.

    MultiStyleExtension.cs

    [MarkupExtensionReturnType(typeof(Style))]
    public class MultiStyleExtension : MarkupExtension
    {
        private string[] resourceKeys;
    
        /// <summary>
        /// Public constructor.
        /// </summary>
        /// <param name="inputResourceKeys">The constructor input should be a string consisting of one or more style names separated by spaces.</param>
        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.");
        }
    
        /// <summary>
        /// Returns a style that merges all styles with the keys specified in the constructor.
        /// </summary>
        /// <param name="serviceProvider">The service provider for this markup extension.</param>
        /// <returns>A style that merges all styles with the keys specified in the constructor.</returns>
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            Style resultStyle = new Style();
            foreach (string currentResourceKey in resourceKeys)
            {
                object key = currentResourceKey;
                if (currentResourceKey == ".")
                {
                    IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
                    key = service.TargetObject.GetType();
                }
                Style currentStyle = new StaticResourceExtension(key).ProvideValue(serviceProvider) as Style;
                if (currentStyle == null)
                    throw new InvalidOperationException("Could not find style with resource key " + currentResourceKey + ".");
                resultStyle.Merge(currentStyle);
            }
            return resultStyle;
        }
    }
    
    public static class MultiStyleMethods
    {
        /// <summary>
        /// Merges the two styles passed as parameters. The first style will be modified to include any 
        /// information present in the second. If there are collisions, the second style takes priority.
        /// </summary>
        /// <param name="style1">First style to merge, which will be modified to include information from the second one.</param>
        /// <param name="style2">Second style to merge.</param>
        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];
        }
    }
    

    Your example would then be solved by going:

    <Style x:key="Combined" BasedOn="{local:MultiStyle A B}">
          ... other properties.
    </Style>
    

    We have defined a new style named "Combined" by merging two other styles "A" and "B" within the built-in BasedOn attribute (used for style inheritance). We can optionally add other properties to the new "Combined" style as per usual.

    Other Examples:

    Here, we define 4 button styles, and can use them in various combinations with little repetition:

    <Window.Resources>
        <Style TargetType="Button" x:Key="ButtonStyle">
            <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="RedButtonStyle">
            <Setter Property="Foreground" Value="Red" />
        </Style>
        <Style TargetType="Button" x:Key="BoldButtonStyle">
            <Setter Property="FontWeight" Value="Bold" />
        </Style>
    </Window.Resources>
    
    <Button Style="{local:MultiStyle ButtonStyle GreenButtonStyle}" Content="Green Button" />
    <Button Style="{local:MultiStyle ButtonStyle RedButtonStyle}" Content="Red Button" />
    <Button Style="{local:MultiStyle ButtonStyle GreenButtonStyle BoldButtonStyle}" Content="green, bold button" />
    <Button Style="{local:MultiStyle ButtonStyle RedButtonStyle BoldButtonStyle}" Content="red, bold button" />
    

    You can even use the "." syntax to merge the "current" default style for a type (context-dependent) with some additional styles:

    <Button Style="{local:MultiStyle . GreenButtonStyle BoldButtonStyle}"/>
    

    The above will merge the default style for TargetType="{x:Type Button}" with the two supplemental styles.

    Credit

    I found the original idea for the MultiStyleExtension at bea.stollnitz.com and modified it to support the "." notation to reference the current style.