简体   繁体   中英

Is there a way to change specific properties of a ControlTemplate defined in a Base-Style, without having to copy-paste the entire code?

I want to create a style for two Buttons, with both of them having exactly the same properties, except for their colors. See this image for reference.

Ideally, I would like to create a Base-Style with a shared ControlTemplate for these Buttons once and then switch out the colors in some way.

My initial thought was to create said Base-Style which uses previously defined SolidColorBrush resources for the colors and then add two additional styles (Accent1Button-Style and Accent2Button-Style) which overwrite these SolidColorBrush resources. The code would look like this:

<!-- Define color resources which are used by the Base-Button Style -->
<SolidColorBrush x:Key="ButtonBackgroundBrush" Color="{Binding Color, Source={StaticResource BaseMediumBrush}}" />
<SolidColorBrush x:Key="ButtonBackgroundHoverBrush" Color="{Binding Color, Source={StaticResource BaseHighBrush}}" />
<SolidColorBrush x:Key="ButtonBackgroundPressedBrush" Color="{Binding Color, Source={StaticResource BaseLowBrush}}" />
<SolidColorBrush x:Key="ButtonBackgroundDisabledBrush" Color="{Binding Color, Source={StaticResource BaseHighBrush}}" />

<SolidColorBrush x:Key="ButtonBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="ButtonBorderHoverBrush" Color="Transparent" />
<SolidColorBrush x:Key="ButtonBorderPressedBrush" Color="Transparent" />
<SolidColorBrush x:Key="ButtonBorderDisabledBrush" Color="Transparent" />

<Style x:Key="StandardButton" TargetType="Button">
    <!-- Properties removed to shorten the code -->
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border x:Name="Bd"
                        CornerRadius="{StaticResource ButtonCornerRadius}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        Background="{TemplateBinding Background}"
                        Padding="{TemplateBinding Padding}">
                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                      RecognizesAccessKey="True" />
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualStateGroup.Transitions>
                                <VisualTransition GeneratedDuration="0:0:0.2" />
                                <VisualTransition GeneratedDuration="0" To="Disabled" />
                            </VisualStateGroup.Transitions>
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                                  Storyboard.TargetProperty="Background.Color">
                                        <EasingColorKeyFrame KeyTime="0" />
                                    </ColorAnimationUsingKeyFrames>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                                  Storyboard.TargetProperty="BorderBrush.Color">
                                        <EasingColorKeyFrame KeyTime="0" Value="{Binding Color, Source={StaticResource ButtonBorderHoverBrush}}" />
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Pressed">
                                <!-- ... -->
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <!-- ... -->
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="Button" BasedOn="{StaticResource StandardButton}">
    <Style.Resources>
        <!-- Overwrite the above colors here -->
        <SolidColorBrush x:Key="ButtonBackgroundBrush" Color="{Binding Color, Source={StaticResource Accent1MediumBrush}}" />
        <SolidColorBrush x:Key="ButtonBackgroundHoverBrush" Color="{Binding Color, Source={StaticResource Accent1HighBrush}}" />
        <!-- And so on... -->
    </Style.Resources>
</Style>

However, this doesn't work, since WPF doesn't overwrite the resources.

Another approach was to create a helper-class which offers an attached property (eg ColorHelper.HoverColor ). I thought about using this property in the template and setting it in the Accent1/Accent2 styles, but that is not possible, because it would require TemplateBindings, which is not supported by the ColorAnimation used in the template.

Right now, the only option I see is to copy/paste the whole template and switch out the colors at the appropriate places (inside the animation, for example). I would like to avoid that, however.

So finally, is there a way to only define my Base-Style once, while also creating separate "sub-styles" which change template-specific properties that are not part of the Button-class (like the HoverColor , PressedColor , ...)?

Edit: To clarify why an approach with attached properties won't work: Assuming that I have introduced a class ColorHelper with the following attached property:

public static readonly DependencyProperty HoverColorProperty =
        DependencyProperty.RegisterAttached("HoverColor", typeof(Color), typeof(ColorHelper), new PropertyMetadata(Colors.Black));

public static Color GetHoverColor(DependencyObject obj)
{
    return (Color)obj.GetValue(HoverColorProperty);
}

public static void SetHoverColor(DependencyObject obj, Color value)
{
    obj.SetValue(HoverColorProperty, value);
}

I could, in theory, set this property in my sub-styles like this:

<Style TargetType="Button" BasedOn="{StaticResource StandardButton}">
    <Setter Property="local:ColorHelper.HoverColor" Value="Red" />
</Style>

<Style TargetType="Button" BasedOn="{StaticResource StandardButton>}">
    <Setter Property="local:ColorHelper.HoverColor" Value="Blue" />
</Style>

The problem with this solution lies in the Base-Style though, to be precise, in the following part of the code:

<!-- snip -->
<VisualState x:Name="MouseOver">
    <Storyboard>
        <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd" Storyboard.TargetProperty="Background.Color">
            <EasingColorKeyFrame KeyTime="0" 
                                 Value="{Binding Path=(local:ColorHelper.HoverColor), RelativeSource={RelativeSource TemplatedParent}}" />
        </ColorAnimationUsingKeyFrames>
    </Storyboard>
</VisualState>
<!-- snip -->

As you can see, the EasingColorKeyFrame 's value is bound via a Binding to the TemplatedParent (and I see no other way for doing this). This isn't supported by WPF though (because of the way Freezeables work together with animations) and the color never gets used in the animation. I also get the following error message in my Output window: System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=(0); DataItem=null; target element is 'EasingColorKeyFrame' (HashCode=8140857); target property is 'Value' (type 'Color') System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=(0); DataItem=null; target element is 'EasingColorKeyFrame' (HashCode=8140857); target property is 'Value' (type 'Color')

After three days of looking for a solution, I stumbled upon the following post: Bind to Control's Property inside VisualStateManager

I could solve my problem by using the exact same approach with the BindingProxy (I only needed to switch from the DoubleAnimation to the ColorAnimation).

Edit: After some time, I discovered a better way. While the BindingProxy solution is working, it is much easier to simply put the Storyboard instances into the resources of an element which appears in the visual tree. By doing this, the Bindings will be able to find the TemplatedParent .

See the example here:

<Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ButtonBase">
                    <Grid>
                        <Grid.Resources>
                            <Storyboard x:Key="MouseOverStoryboard">
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="Background.Color">
                                    <EasingColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:MouseOverProperties.BackgroundColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="BorderBrush.Color">
                                    <EasingColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:MouseOverProperties.BorderColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                            <Storyboard x:Key="PressedStoryboard">
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="Background.Color">
                                    <DiscreteColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:MouseOverProperties.BackgroundColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="BorderBrush.Color">
                                    <DiscreteColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:MouseOverProperties.BorderColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                            <Storyboard x:Key="DisabledStoryboard">
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="Background.Color">
                                    <EasingColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:DisabledProperties.BackgroundColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                              Storyboard.TargetProperty="BorderBrush.Color">
                                    <EasingColorKeyFrame KeyTime="0" Value="{Binding Path=(theming:DisabledProperties.BorderColor), RelativeSource={RelativeSource TemplatedParent}}" />
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                        </Grid.Resources>

                        <!-- ... -->
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="{StaticResource ColorAnimationDuration}" />
                                    <VisualTransition To="Disabled" GeneratedDuration="0" />
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="MouseOver" Storyboard="{StaticResource MouseOverStoryboard}" />
                                <VisualState x:Name="Pressed" Storyboard="{StaticResource PressedStoryboard}" />
                                <VisualState x:Name="Disabled" Storyboard="{StaticResource DisabledStoryboard}" />
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM