New Color from existing Color, making a renamed/copy/alias in xaml

1.3k Views Asked by At

I am attempting to follow the Don't Repeat Yourself principle i XAML, but I've reached a point where I'm struggling.

I am defining colors for skinning. Say I have defined the colors I want to base my skin on:

<ResourceDictionary>
    ...
    <Color x:Key="B+">#FF999999</Color>
    <Color x:Key="B">#FF808080</Color>
    <Color x:Key="B-">#FF666666</Color>
    ...
</ResourceDictionary>

Now say I want to use the color B as my border color, I'd style the BorderBrush of my textboxs, buttons etc.:

<Style TargetType="TextBox">
    <Setter Property="BorderBrush">
        <Setter.Value>
            <SolidColorBrush Color="{StaticResource B}" />
        </Setter.Value>
    </Setter> 
</Style>
<Style TargetType="Button">
    <Setter Property="BorderBrush">
        <Setter.Value>
            <SolidColorBrush Color="{StaticResource B}" />
        </Setter.Value>
    </Setter> 
</Style>

Now suppose I want to use a darker tone, B-, and so instead of having (to remember) to change the resource for all controls, I would rather have an alternative named "placeholder" color from say B, so I'd only have to change it once:

<Color x:Key="controlBorderColor">B</Color><!-- Not working -->

I am at a loss on how to do this. Is there a general "pattern" or way of defining "constants" in XAML?


Workaround solution

Not possible the way I want to, see answers.

Workaround in short (Thanks to @dotsven for the neat trick to handle brightness): Define all colors as brushes, and use markup extensions to retrieve the Color property.

AFAIK Color doesn't support it assignment of content, so there is no easy way from XAML to define or bind to it without doing it individually to the A,R,G,B etc. attributes. The brush sort of serves as a wrapper here so we can bind to Color.

Define all colors as Brushes

<SolidColorBrush x:Key="B">
    <SolidColorBrush.Color>
        #FF808080
    </SolidColorBrush.Color>
</SolidColorBrush>

Create a MarkupExtension to retrieve Color from the SolidColorBrush:

[MarkupExtensionReturnType(typeof(Color))]
public class SkinColorExtension : MarkupExtension {
  public SkinColorExtension() {
  }

  public SkinColorExtension(SolidColorBrush baseColor, int brightness) {
    this.BaseColor = baseColor;
    this.Brightness = brightness;
  }

  [ConstructorArgument("baseColor")]
  public SolidColorBrush BaseColor { get; set; }
  [ConstructorArgument("brightness")]
  public int Brightness { get; set; }

  public override object ProvideValue(IServiceProvider serviceProvider) {
    return SkinColor_Helper.ApplyBrightness(BaseColor.Color, Brightness);
  }
}

static class SkinColor_Helper {
  public static Color ApplyBrightness(Color c, int b) {
    Func<int, byte> Scale = (val) => {
      int scaledVal = val + b;
      if (scaledVal < 0) return 0;
      else if (scaledVal > 255) return 255;
      else return (byte)scaledVal;
    };
    return Color.FromArgb(c.A, Scale(c.R), Scale(c.G), Scale(c.B));
  }
}

Then create my skinning colors by brightness offsets as such:

<SolidColorBrush x:Key="controlBorder" Color="{ext:SkinColor BaseColor={StaticResource B}, Brightness=-25}"/>
<SolidColorBrush x:Key="controlBackground" Color="{ext:SkinColor BaseColor={StaticResource B}, Brightness=-100}" />

When controls need a brush, e.g. Background, just bind to {StaticResource controlBackground}.

If you have to bind to something that needs a Color, bind to {ext:SkinColor BaseColor={StaticResource controlBackground}} to retrieve it from the brush instead.

2

There are 2 best solutions below

6
On BEST ANSWER

Unfortunately there's no direct support for this. But by using a custom markup extension you get a fexible workaround. Create a markup extension like this:

[MarkupExtensionReturnType(typeof(Color))]
public class RelativeColorExtension : MarkupExtension
{
    public RelativeColorExtension()
    {
    }

    public RelativeColorExtension(Color baseColor)
    {
        this.BaseColor = baseColor;
    }

    [ConstructorArgument("baseColor")]
    public Color BaseColor { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Color.FromArgb(this.BaseColor.A, (Byte)(this.BaseColor.R / 2), (Byte)(this.BaseColor.G /2), (Byte)(this.BaseColor.B / 2));
    }
}

Then you can use it as follows:

    <Grid.Resources>
        <Color x:Key="Color1" R="10" G="10" B="128" A="255" />
        <SolidColorBrush x:Key="Color1Brush" Color="{StaticResource Color1}" />
        <SolidColorBrush x:Key="Color2Brush" Color="{ext:RelativeColor BaseColor={StaticResource Color1}}" />            
    </Grid.Resources>

It's important to note that you've got to implement the markup extension in a separate assembly. You'd extend the markuop extension with properties that allow you to dim, brighten or manipulate the base color in a number of ways.

1
On

Unfortunately, it's not possible to create a static resource that's an alias for another. At least not in XAML.

If you can access the resource dictionary from code, then you can just use its Add method. It's a dictionary after all.

For some types, like Styles, you can fake aliases using inheritance. For example:

<Style x:Key="Foo">...</Style>
<Style x:Key="Bar" BasedOn="{StaticResource Foo}"/>
<!-- now Bar is an "alias" for Foo -->