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):
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.
x:Shared="false"on the Brush resource declarations?