2

Problem:

I've created a custom WPF Button control with a ControlTemplate defined in a ResourceDictionary. This template uses DynamicResource for initial Background and Foreground properties (to support dynamic theme switching between Light/Dark modes) and StaticResource within VisualState.Storyboard animations for color changes on MouseOver, Pressed, etc.

The issue is that when one instance of my custom Button triggers a VisualState (e.g., MouseOver), the color change defined by StaticResource in that VisualState's Storyboard affects all other instances of the same custom Button on the screen. This is unexpected behavior as VisualStates should ideally be localized to the specific control instance.

Goal:

My primary goal is to have the custom Button dynamically change colors based on the active application theme (Light/Dark), and for its VisualState animations (e.g., hover, press) to only affect the specific button instance being interacted with, while still leveraging the themed SolidColorBrush resources.

Code Snippets (Simplified):

Github repo with full code.

ModernButton style

<Style x:Key="GithubDefaultButtonStyle" TargetType="mdr:ModernButton">
    <Setter Property="Padding" Value="12,3"/>
    <Setter Property="Background" Value="{DynamicResource ButtonDefaultBgColorRestBrush}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource ButtonDefaultBorderColorRestBrush}"/>
    <Setter Property="Foreground" Value="{DynamicResource ButtonDefaultFgColorRestBrush}"/>
    <Setter Property="CornerRadius" Value="4" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="mdr:ModernButton">
                <Border TextBlock.Foreground="{TemplateBinding Foreground}" x:Name="Border" 
                        Padding="{TemplateBinding Padding}" 
                        CornerRadius="{TemplateBinding CornerRadius}" 
                        Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ContentPresenter Margin="2" x:Name="ContentPresenter" 
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                          RecognizesAccessKey="True" />
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ColorAnimation Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                Storyboard.TargetName="Border" Duration="0:0:0.04"
                                To="{StaticResource ButtonDefaultBgColorHover}"/>
                                    <ColorAnimation Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
                                Storyboard.TargetName="Border" Duration="0:0:0.04"
                                To="{StaticResource ButtonDefaultBorderColorHover}"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

ModernButton.xaml.cs

public partial class ModernButton : Button
{
    #region Constructors
    static ModernButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ModernButton), new FrameworkPropertyMetadata(typeof(ModernButton)));
    }
    #endregion

    #region DependencyProperties
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.Register(
            nameof(CornerRadius),
            typeof(CornerRadius),
            typeof(ModernButton),
            new PropertyMetadata(new CornerRadius(0))); // Default corner radius value
    #endregion

    #region Properties
    public CornerRadius CornerRadius
    {
        get { return (CornerRadius)GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }
    #endregion
}

ThemeManager.cs

public class ThemeManager
{

#region Fields
    private readonly Dictionary<WpfTheme, string> AvailableThemes = new()
    {
        { WpfTheme.GithubDark, "pack://application:,,,/Wpf.Modern.Themes;component/Themes/Github/Themes/GithubDarkTheme.xaml" },
        { WpfTheme.GithubLight, "pack://application:,,,/Wpf.Modern.Themes;component/Themes/Github/Themes/GithubLightTheme.xaml" },
    };
    #endregion


    #region Consts

    private const string ThemeResourceKey = "CurrentThemeLibrary";
    #endregion

    #region Methods
    public void ApplyTheme(WpfTheme theme)
    {
        if (!AvailableThemes.ContainsKey(theme))
            throw new ArgumentException($"Theme '{theme}' is not available.");

        var app = Application.Current;
        var oldTheme = CurrentTheme;

        // Remove existing theme
        var existingTheme = app.Resources.MergedDictionaries
            .FirstOrDefault(d => d.Contains(ThemeResourceKey));

        if (existingTheme != null)
        {
            app.Resources.MergedDictionaries.Remove(existingTheme);
        }

        // Add new theme
        var themeUri = new Uri(AvailableThemes[theme], UriKind.Absolute);
        var newTheme = new ResourceDictionary { Source = themeUri };
        newTheme[ThemeResourceKey] = theme;

        app.Resources.MergedDictionaries.Add(newTheme);

        // Force refresh of all windows
        foreach (Window window in app.Windows)
        {
            window.InvalidateVisual();
        }
    }
    #endregion
}

I've tried to change initial properties to use StaticResource instead of DynamicResource, it solves the problem with triggering VisualState but it creates problem with dynamic theme switching.

2
  • 3
    Have you set x:Shared="false" on the Brush resource declarations? Commented Jul 7 at 9:24
  • @Clemens I didn't. I just tried and it fixed my problem. Commented Jul 7 at 9:28

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.