If I understand correctly, there are no Triggers in Avalonia right now. I think it would be beneficial to support them since they allow for dynamic UI that's dependant on a model or another control's state.
Here are few examples of how I use Triggers in my projects. The syntax is Ammy, but I think it's understandable enough.
alias TopBarButton(buttonName) {
Button {
Style: Style {
TargetType: Button
Triggers: [
@DataTrigger_SetProperty(bind "SelectedView", $buttonName, "BorderThickness", "1")
]
}
}
}
This trigger watches SelectedView property on a ViewModel and when it equals $buttonName (passed parameter), it would set BorderThickness to 1.
Or even something like this, which changes the Buttons content depending on IsCollapsed property:
Style: Style {
TargetType: Button
#Setter("Content", "â–²")
#Add_DataTrigger_SetProperty(bind IsCollapsed, true, "Content", "â–¼")
}
In some cases, you can replace triggers with Binding Converters, but it is more cumbersome, and not always possible (for example if you set the ControlTemplate).
Triggers are currently supported through the https://github.com/wieslawsoltes/AvaloniaBehaviors project.
Also, with our style selectors, you can make your second example using our built-in selector system. The selector Button[IsCollapsed=true] for a style should work.
Hello Jeremy,
Avalonia Behaviours seem like exactly what I would need. One thing that is missing though (if I understand correctly) is Enter/ExitActions. Can you somehow emulate them with Avalonia?
Thanks!
@ionoy does Enter/ExitActions refer to mouse over? If so, there is the :pointerover pseudoclass (which I'm actually thinking we should rename to :hover like in HTML):
<Style Selector="Border#myBorder:pointerover">
<Setter Property="BorderThickness" Value="2"/>
</Style>
As an aside, in longer term thinking I'd love to be able to have expressions in bindings so you could do stuff like (totally inventing a syntax on the spot here):
<Border BorderThickness="{{IsSelected ? 2 : unset}}/>
These actions are for executing Storyboards with animations before or after the Trigger effect. You can, for example, make button slide down a few pixels when the cursor hovers over it. Or gradually change the background. There are tons of useful stuff you can do with it.
We don't currently have Storyboard support. Our animation support is pretty limited right now. The infrastructure is in place, but we haven't build XAML elements to declaratively define it.
Declarative Storyboards would be a great feature to have.
@ionoy We implemented Avalonia's animations subsystem with CSS as the primary inspiration instead of WPF's since we already have a CSS-like styling subsystem. If you want to know more about it then please ping us up @ gitter :smile:
@jmacato That's great news! I'll take a look.
Do you have any plans to implement a native trigger support?
AvaloniaBehaviors has a clumpsy syntax even compatible with WPF, require bindings that affect performance, (maybe im wrong, just a quick look) not integrated well with animation system and more limited than wpf trigger system.
I've been working a bit in WPF and got reminded where triggers can come in useful, especially data triggers:
<Style.Triggers>
<DataTrigger Binding="{Binding HasNormalMap}" Value="False">
<Setter Property="Opacity" Value="0.3"/>
</DataTrigger>
<DataTrigger Binding="{Binding HasNormalMap}" Value="{x:Null}">
<Setter Property="Foreground" Value="{DynamicResource ValidationBrush5}"/>
<Setter Property="ToolTip" Value="Warning: not all tiles have normal maps"/>
</DataTrigger>
</Style.Triggers>
I don't think WPF's syntax is very nice here because the above code could be expressed in a more ideal markup language (e.g. something like polymer) as something like:
<Button Opacity="[[HasNormalMap ? 1 : 0.3]]"
Foreground="[[HasNormalMap == null ? {DynamicResource ValidationBrush5} : {unset}]]"
Tooltip="[[HasNormalMap == null ? 'Warning: not all tiles have normal maps' : {unset}]]">
I've not done much work in UWP: how would one do this in UWP (which doesn't have triggers afaik)?
I've been thinking about this some more and I think allowing more powerful expressions in bindings would be preferable to triggers, and not a whole lot more work. Regarding syntax, what if we allowed if statements in bindings? e.g.
<Button Opacity="{Binding if HasNormalMap then 0.3}"
Foreground="{Binding if HasNormalMap == null then {DynamicResource ValidationBrush5}}"
Tooltip="{Binding if HasNormalMap == null then 'Warning: not all tiles have normal maps'}">
Syntax would need to be iterated upon but the reason for moving away from C# syntax here would be:
() around the conditional expression causes ambiguities with attached property syntax? :) because the else clause can be omittedSomething to think about...
You cant implement a lot of scenarios with bindings
<Style x:Key="baseStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Red"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Green"/>
</Trigger>
<DataTrigger Binding="{Binding Path=(test:Attached.Property).IsEnabled, RelativeSource={RelativeSource Self}}" Value="true">
<Setter Property="Background" Value="Yellow"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="derivedStyle" TargetType="{x:Type Button}" BasedOn="{StaticResource baseStyle}">
<Setter Property="Background" Value="Black"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=VMProperty}" Value="123">
<Setter Property="Background" Value="Pink"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="{dxt:DefaultStyleThemeKey FullName=System.Windows.Controls.Button, ThemeName=DeepBlue}" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="10,1,10,1"/>
<Setter Property="Template" Value="{DynamicResource {dxt:ButtonThemeKey ResourceKey=ButtonControlTemplate, ThemeName=DeepBlue}}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=Foreground, ThemeName=DeepBlue}}"/>
<Style.Triggers>
<Trigger Property="dx:ThemeManager.IsTouchEnabled" Value="True">
<Setter Property="Padding" Value="25,11,25,11"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="ToggleButton.IsChecked" Value="False"/>
<Condition Property="IsPressed" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=MouseOverForeground, ThemeName=DeepBlue}}"/>
</MultiTrigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=CheckedForeground, ThemeName=DeepBlue}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=PressedForeground, ThemeName=DeepBlue}}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="{dxt:DefaultStyleThemeKey FullName=System.Windows.Controls.Button, ThemeName=LightGray}" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="11,2,11,2"/>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=Foreground, ThemeName=LightGray}}"/>
<Setter Property="Template" Value="{DynamicResource {dxt:ButtonThemeKey ResourceKey=ButtonControlTemplate, ThemeName=LightGray}}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Style.Triggers>
<Trigger Property="dx:ThemeManager.IsTouchEnabled" Value="True">
<Setter Property="Padding" Value="27,12,27,12"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="ToggleButton.IsChecked" Value="False"/>
<Condition Property="IsPressed" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=MouseOverForeground, ThemeName=LightGray}}"/>
</MultiTrigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=CheckedForeground, ThemeName=LightGray}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=PressedForeground, ThemeName=LightGray}}"/>
</Trigger>
</Style.Triggers>
</Style>
You are limited to a python style one-liners that cant be overriden by theming because of value source.
I think if we were able to access the DataContext frome a selector we can cover everything that a wpf trigger is able to do for us. That would be equal to a DataTrigger.
@Xarlot please note that we've not made a decision to not include triggers, we're just exploring alternatives.
I've taken a look at your examples and a lot of that can already be done in Avalonia even without conditional bindings. You just need to take advantage of our styling system. However I'll add in conditional bindings using the invented syntax above for the stuff that can't currently be done:
WPF:
<Style x:Key="baseStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Red"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Green"/>
</Trigger>
<DataTrigger Binding="{Binding Path=(test:Attached.Property).IsEnabled, RelativeSource={RelativeSource Self}}" Value="true">
<Setter Property="Background" Value="Yellow"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
Avalonia:
<Style Selector="Button">
<Setter Property="Background" Value="{Binding if $self.(test:Attached.Property).IsEnabled then 'Yellow' else 'Red'}"/>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="Green"/>
</Style>
WPF:
<Style x:Key="derivedStyle" TargetType="{x:Type Button}" BasedOn="{StaticResource baseStyle}">
<Setter Property="Background" Value="Black"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=VMProperty}" Value="123">
<Setter Property="Background" Value="Pink"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
Avalonia:
<Style Selector="Button">
<Setter Property="Background" Value="{Binding if VMProperty == 123 then 'Pink' else 'Black'}"/>
</Style>
WPF:
<Style x:Key="{dxt:DefaultStyleThemeKey FullName=System.Windows.Controls.Button, ThemeName=DeepBlue}" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="10,1,10,1"/>
<Setter Property="Template" Value="{DynamicResource {dxt:ButtonThemeKey ResourceKey=ButtonControlTemplate, ThemeName=DeepBlue}}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=Foreground, ThemeName=DeepBlue}}"/>
<Style.Triggers>
<Trigger Property="dx:ThemeManager.IsTouchEnabled" Value="True">
<Setter Property="Padding" Value="25,11,25,11"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="ToggleButton.IsChecked" Value="False"/>
<Condition Property="IsPressed" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=MouseOverForeground, ThemeName=DeepBlue}}"/>
</MultiTrigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=CheckedForeground, ThemeName=DeepBlue}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=PressedForeground, ThemeName=DeepBlue}}"/>
</Trigger>
</Style.Triggers>
</Style>
Avalonia:
<Style Selector="Button">
<Setter Property="Padding" Value="10,1,10,1"/>
</Style>
<Style Selector="Button[dx:ThemeManager.IsTouchEnabled]">
<Setter Property="Padding" Value="25,11,25,11"/>
</Style>
<Style Selector=":is(Button):pointerover:checked:pressed">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=MouseOverForeground, ThemeName=DeepBlue}}"/>
<Style Selector=":is(Button):checked">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=CheckedForeground, ThemeName=DeepBlue}}"/>
</Style>
<Style Selector="Button:pressed">
<Setter Property="Foreground" Value="{DynamicResource ResourceKey={dxt:DropDownButtonThemeKey ResourceKey=PressedForeground, ThemeName=DeepBlue}}"/>
</Style>
I think if we were able to access the DataContext frome a selector we can cover everything that a wpf trigger is able to do for us. That would be equal to a DataTrigger.
Yes, I think so though I'm worried that the syntax for this could get quite unweildy!
The other thing to consider is EventTrigger which we don't have an equivalent for, but I've never really used them apart from to trigger animations.
I've tried to make a simple app with Avalonia which present some data to the user and allows to edit it and not having Triggers in DataTemplate means I can't easily change the DataTemplate content from TextBlock to TextBox when a property of the DataContext changes.
I'm sure I can do that in code, I'll try to implement it by having a property in the ViewModel which return the control to present based on the property and bind it to a ContentPresenter but that would be much easier if triggers were implemented.
Just looking into this, migrating my WPF into Avalonia, and assesing how to replace DataTriggers doing stuff like
if DataContext.Property == "ValueA" -> StyleA
if DataContext.Property == "ValueB" -> StyleB
it seems that you almost have what's needed if SELECTORS supported DataContext values
i.e. today we can do: if Button[IsDefault=true] -> StyleA
if we could do Button.DataContext.IsOrderActive -> StyleA e.g. Button(IsOrderActive=true)
that would solve this problem in a I dare to say much more elegant and simpler way then inventing new syntax in Bindings
({Binding if $self.(test:Attached.Property).IsEnabled then 'Yellow' else 'Red'})
Given observing DataContext value changes is already coded, it feels like it should be doable to reuse this ability in Selectors
Regards,
Stefan
I've tried @grokys solution in Avalonia 0.9.3 and it doesn't work:
<Style Selector="Button">
<Setter Property="Background" Value="{Binding if VMProperty == true then 'Pink' else 'Black'}"/>
</Style>
throws
Error Unexpected token EqualSign (line 20 position 33) Line 20, position 33.
And
<Style Selector="Button">
<Setter Property="Background" Value="{Binding if VMProperty then 'Pink' else 'Black'}"/>
</Style>
throws
Quote characters out of place (line 20 position 33) Line 20, position 33.
I've taken a look at the Avalonia homepage about bindings and styling https://avaloniaui.net/docs/styles/ but something like this isn't mentione there.
How can I achieve / convert a WPF application code where I change the background of a button via datacontext binding (without converter):
<Button>
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{x:type Button}>
<Style.Triggers>
<DataTrigger Binding="{Binding VMProperty}" Value="true">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Has there been any progress on this topic so far?
Afraid not, we've been putting out fires left and right to try and get 0.10 released. Hopefully once that's out I can start work on it.
Ok, thank you for your answer. Is there at the moment any possibility to apply a style depending on a value of the data binding in XAML, like if a property is less than 0? Or do i need to listen on events in code behind and apply the style manually?
@MopsiMauser for the moment you can use behaviors: the AvaloniaBehaviors repository has the DataTriggerBehavior that I think does what you want. See examples here: https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/master/samples/BehaviorsTestApplication/Pages/DataTriggerBehaviorControl.xaml
Most helpful comment
I've tried @grokys solution in Avalonia 0.9.3 and it doesn't work:
throws
Error Unexpected token EqualSign (line 20 position 33) Line 20, position 33.And
throws
Quote characters out of place (line 20 position 33) Line 20, position 33.I've taken a look at the Avalonia homepage about bindings and styling https://avaloniaui.net/docs/styles/ but something like this isn't mentione there.
How can I achieve / convert a WPF application code where I change the background of a button via datacontext binding (without converter):