54

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??

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
A.R.
  • 15,405
  • 19
  • 77
  • 123
  • Does this answer your question? [How to apply multiple styles in WPF](https://stackoverflow.com/questions/16096/how-to-apply-multiple-styles-in-wpf) – StayOnTarget Aug 16 '21 at 18:15

2 Answers2

66

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.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Alain
  • 26,663
  • 20
  • 114
  • 184
  • booya! Just what I was looking for. – A.R. Sep 22 '11 at 12:40
  • 4
    I have successfully used this to combine 2 styles; however, I have encountered a small hitch. The VS 2010 WPF Designer has a problem with this approach. I can combine the styles and use the MultiStyle exactly as detailed here, and build/run the code with no problems. But the WPF designer complains about using this approach inside a DataTemplate. Has anyone encountered/solved this problem? – Joe K Dec 04 '12 at 16:00
  • 2
    @JoeK I had the exact same problem, and posted a question about it here: http://stackoverflow.com/questions/8731547/markup-extension-staticresourceextension-requires-ixamlschemacontextprovider. So far the only solution I have is to disable the extension during design mode, which is less than ideal. – Alain Dec 04 '12 at 16:14
  • It's line a CSS classnames. Pretty awesome. – Даниил Пронин Dec 09 '15 at 06:46
  • 4
    There is a bug when using ``BasedOn`` in styles defined in resource dictionaries for the dot notation. Here's the fix: ``IProvideValueTarget service = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget)); if (service.TargetObject is Style) { key = ((Style)service.TargetObject).TargetType; } else { key = service.TargetObject.GetType(); }`` – MoonStom Feb 26 '16 at 07:14
  • 3
    I have modified your code so it now can accept up to 10 Style objects as parameter instead of using style keys for refactoring purpose + merging occurs in constructor: https://dotnetfiddle.net/r464VS – Epsil0neR Sep 06 '17 at 11:02
  • @Epsil0neR I have tried your solution in VS2017 with resharper. It's working nicely but designer is complaining: `System.Windows.StaticResourceExtension' cannot be converted to type System.Windows.Style`. I am using it like this: `Style="{local:MultiStyle {StaticResource A}, {StaticResource B}}"`. Also the designer doesn't reflect the styles applied... – stambikk Oct 12 '17 at 20:17
  • @stambikk, I just checked in my VS2017 with R# and it does not complain regarding wrong type. Maybe you missed attribute ```[MarkupExtensionReturnType(typeof(Style))]```? I don't think that it is possible to fix problem with showing combined desing in designer. Btw, my solution is just updated @Alain solution, but instead of accepting style key you provide whole Style objects. – Epsil0neR Oct 13 '17 at 09:42
  • 1
    If someone interested, I put this Epsiloner.Wpf.Extensions.MultiStyleExtension to my WPF related lib: https://github.com/Epsil0neR/Epsiloner.Wpf.Core or https://www.nuget.org/packages/Epsiloner.Wpf.Core/ – Epsil0neR Aug 27 '18 at 19:32
  • This worked perfectly for me in my simple situation (i.e. not in one of the scenarios here). But you have to specify the styles by string name and don't get any intellisense. Is there a way to do this in a type safe way? – Joshua Frank Apr 02 '21 at 14:55
3

You can use BasedOn property in style, for example:

<Style x:Key="BaseButtons" TargetType="{x:Type Button}">
        <Setter Property="BorderThickness" Value="0"></Setter>
        <Setter Property="Background" Value="Transparent"></Setter>
        <Setter Property="Cursor" Value="Hand"></Setter>
        <Setter Property="VerticalAlignment" Value="Center"></Setter>
</Style>
<Style x:Key="ManageButtons" TargetType="{x:Type Button}" BasedOn="{StaticResource BaseButtons}">
        <Setter Property="Height" Value="50"></Setter>
        <Setter Property="Width" Value="50"></Setter>
</Style>
<Style x:Key="ManageStartButton" TargetType="{x:Type Button}" BasedOn="{StaticResource BaseButtons}">
        <Setter Property="FontSize" Value="16"></Setter>
</Style>

and use:

<Button Style="{StaticResource ManageButtons}"></Button>
<Button Style="{StaticResource ManageStartButton}"></Button>
billw
  • 120
  • 1
  • 9