In my Avalonia app using the MVVM Community Toolkit, I want to have a number of buttons in multiple user controls in my application to have as their content a PathIcon
with a TextBlock
underneath. I want to create a style to apply to the buttons that sets the color of the PathIcon
and the text based on multiple viewmodel properties (likely from different viewmodels), with each button applying the style able to input a different property. Here is what I currently have for one button, preceded by the converter code:
public class FeatureActivatedThemeColorConverter : IMultiValueConverter {
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture) {
if (values[0] is bool isActive && values[1] is ThemeVariant theme) {
if (theme == ThemeVariant.Dark) {
if (isActive) {
return new SolidColorBrush(Colors.LightGreen);
} else {
return new SolidColorBrush(Colors.White);
}
} else if (isActive) {
return new SolidColorBrush(Colors.DarkGreen);
}
}
return new SolidColorBrush(Colors.DarkGray);
}
public object[] ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
In viewmodel, for what it's worth:
[ObservableProperty]
public bool _isCloseUpActive = false;
[RelayCommand]
public void ToggleCloseUp() {
IsCloseUpActive = !IsCloseUpActive;
}
XAML from App.axaml and user control, obviously:
<Application.Resources>
<vm:FeatureActivatedThemeColorConverter x:Key="FeatActTheme" />
</Application.Resources>
<UserControl.Styles>
<Style Selector="Button.ActiveAccented">
<Style Selector="^ PathIcon" >
<Setter Property="Foreground">
<Setter.Value>
<MultiBinding Converter="{StaticResource FeatActTheme}">
<Binding Path="IsCloseUpActive" />
<Binding Path="CurrentTheme" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
<Style Selector="^ TextBlock" >
<Setter Property="Foreground">
<Setter.Value>
<MultiBinding Converter="{StaticResource FeatActTheme}">
<Binding Path="IsCloseUpActive" />
<Binding Path="CurrentTheme" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</Style>
</UserControl.Styles>
...
<Button Classes="ActiveAccented" ToolTip.Tip="Show close-up next to cursor" Command="{Binding ToggleCloseUpActiveCommand}" >
<StackPanel>
<PathIcon Data="{StaticResource MagnifyingGlassStreamGeometryResource}" />
<TextBlock Text="Close-Up" />
</StackPanel>
</Button>
I wish to avoid duplicating the ActiveAccented
style for every Button
that indicates the activation state of a different feature, per its own viewmodel flag. I would rather apply a style to the effect of:
<Window.Styles>
<Style Selector="Button.ActiveAccented">
<Style Selector="^ PathIcon" >
<Setter Property="Foreground">
<Setter.Value>
<MultiBinding Converter="{StaticResource FeatActTheme}">
<Binding Path="{StyleParameter ActiveFlag}" />
<Binding Path="CurrentTheme" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
<Style Selector="^ TextBlock" >
<Setter Property="Foreground">
<Setter.Value>
<MultiBinding Converter="{StaticResource FeatActTheme}">
<Binding Path="{StyleParameter ActiveFlag}" />
<Binding Path="CurrentTheme" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</Style>
</Window.Styles>
...
<Button Classes="ActiveAccented" ActiveFlag="IsCloseUpActive" ToolTip.Tip="Show close-up next to cursor" Command="{Binding ToggleCloseUpActiveCommand}" >
<StackPanel>
<PathIcon Data="{StaticResource MagnifyingGlassStreamGeometryResource}" />
<TextBlock Text="Close-Up" />
</StackPanel>
</Button>
<Button Classes="ActiveAccented" ActiveFlag="IsZoomed100Pct" ToolTip.Tip="Zoom image to 100%" Command="{Binding Zoom100Command}" >
<StackPanel>
<PathIcon Data="{StaticResource OneToOneStreamGeometryResource}" />
<TextBlock Text="100%" />
</StackPanel>
</Button>
Bonus if I can reduce the amount of code that applies the same color effect to both the PathIcon
and the TextBlock
.
1 Answer 1
Your example is a perfect for a Templated (Lookless) Controls you can implement IconBtn
control which inherent from button with extra Icon
and IsActive
properties
<controls:IconBtn Icon="{StaticResource MagnifyingGlassStreamGeometryResource}"
Content="any content you want even if it complex" />
Add Template control in your project with the name IconBtn
this Button will have a Custom Pseudoclass :active
to help in style it
[PseudoClasses(":active")]
public class IconBtn : Button
{
public static readonly StyledProperty<Geometry> IconProperty =
AvaloniaProperty.Register<IconBtn, Geometry>(nameof(Icon));
public static readonly StyledProperty<bool> IsActiveProperty =
AvaloniaProperty.Register<IconBtn, bool>(nameof(IsActive), false, defaultBindingMode: BindingMode.TwoWay);
public Geometry Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public bool IsActive
{
get => GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
protected override void OnClick()
{
// Toggle the IsActive state when the button is clicked automatically
IsActive = !IsActive;
base.OnClick();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsActiveProperty && change.NewValue is bool newValue)
{
// Ensure the PseudoClass is updated when IsActive changes
PseudoClasses.Set(":active", newValue);
}
}
}
now try to add UI for IconBtn
control in IconBtn.axaml
add the following theme-variants inside the ResourceDictionary
and above ControlTheme
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key='Light'>
<SolidColorBrush x:Key='IconBtnBackground'>#eee</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnForeground'>Black</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnActiveBackground'>#afa</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnActiveForeground'>DarkGreen</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key='Dark'>
<SolidColorBrush x:Key='IconBtnBackground'>#333</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnForeground'>white</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnActiveBackground'>#234</SolidColorBrush>
<SolidColorBrush x:Key='IconBtnActiveForeground'>LightGreen</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
Take a look at how fluent themes implement buttons and now you can add the actual them as following
<ControlTheme x:Key="{x:Type controls:IconBtn}" TargetType="controls:IconBtn">
<Setter Property="Background" Value="{DynamicResource IconBtnBackground}" />
<Setter Property="Foreground" Value="{DynamicResource IconBtnForeground}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ButtonBorderThemeThickness}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="RenderTransform" Value="none" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:.075" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_BORDER"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}">
<StackPanel>
<PathIcon Name="ICON"
Foreground="{TemplateBinding Foreground}"
Data="{TemplateBinding Icon}" />
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
RecognizesAccessKey="True"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:active /template/ Border#PART_BORDER">
<Setter Property="Background" Value="{DynamicResource IconBtnActiveBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="^:active /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource IconBtnActiveForeground}" />
</Style>
<Style Selector="^:active /template/ PathIcon#ICON">
<Setter Property="Foreground" Value="{DynamicResource IconBtnActiveForeground}" />
</Style>
<Style Selector="^:pointerover /template/ Border#PART_BORDER">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="^:pressed /template/ Border#PART_BORDER">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
</Style>
<Style Selector="^:disabled /template/ Border#PART_BORDER">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
</Style>
</ControlTheme>
here is the following result of the color you can change the theme as you want by changing the theme-variants
3
don't forget to include this resources in your app.axaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="Icons.axaml" /> <!--Resource Dictionary where you add your Icons Gemotry -->
<ResourceInclude Source="Controls/IconBtn.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
if you want to use this control as like build-IN control <IconBtn ..>
instead of <controls:IconBtn ...>
without define controls namespace every time, add namespace-definitions to Properties/AssemblyInfo.cs
as following
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "<you project Namespace>.<where the control found>")]
-
\$\begingroup\$ So, create an inheriting control and use
PseudoClasses.Set
to tie a property to something you can set in a style? Pretty cool! I'm now able to bind my viewmodel flag to it and when it's set true in the click handler the foreground changes, so thanks. Although, even with two-way binding it doesn't seem to change if that flag is changed by another bound property. I can probably get around that, though. Now, if only there was a way to know whichDynamicResource
to set to be theForeground
depending on whether light or dark theme is active... \$\endgroup\$Eric Eggers– Eric Eggers2025年07月22日 23:06:40 +00:00Commented Jul 22 at 23:06 -
1\$\begingroup\$ I have checked it and update the code with fixes ... the problem was that
IsActive
changes through Bindings it ignores the Setter ofIsActive
instead it callscontrol.SetValue(IsActiveProperty, value);
... to fix this problem response on the change inOnPropertyChanged
overload ... it should be working without problem \$\endgroup\$Ibram Reda– Ibram Reda2025年07月23日 06:02:56 +00:00Commented Jul 23 at 6:02