Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way of setting style classes using bindings #2427

Closed
grokys opened this issue Apr 4, 2019 · 11 comments
Closed

Add a way of setting style classes using bindings #2427

grokys opened this issue Apr 4, 2019 · 11 comments
Assignees

Comments

@grokys
Copy link
Member

grokys commented Apr 4, 2019

We really need a way to be able to apply to apply style classes to controls based on values from view models. This would be needed to e.g. trigger animations from the view model.

AvaloniaBehaviors has a behavior for this, but I think we should really have something in-box.

A few ideas:

Simple binding

<Button Classes="{Binding ButtonClasses}">

Pros:

  • Simple

Cons:

  • Will need to either specify actual classes in view model or supply a converter
  • Would not be able to specify classes in XAML as well as get them from the binding

Class Setters

<Button>
  <Button.Classes>
    <ClassSetter Name="foo"/>
    <ClassSetter Name="animated" Trigger="{Binding IsButtonAnimating}"/>
  </Button.Casses>
</Button>

Access DataContext from Selectors

An alternative to binding classes would be to allow selectors to select into the DataContext.

Pros:

  • Probably pretty flexible

Cons:

  • Syntax?
@grokys grokys self-assigned this Apr 4, 2019
@stevozilik
Copy link

Yes please, that's exactly what's missing to replace DataTriggers

Pros

  • Simple and powerfull
  • Can style many properties (as selectors do)

Cons:

  • Just coding it? Assuming providing a few comparison operators: =, >, <, IN(), ! is enough?

Syntax:

Control(MyDataContextProperty=Value)

#1362

@balthild
Copy link

balthild commented Nov 4, 2019

Is there any workarounds now? Since a ViewModel cannot access Controls to call .Classes.Add/Remove(), it seems to be difficult to change controls' class when members in ViewModel are updated.

@Gillibald
Copy link
Contributor

You could write a behavior that is adding removing classes on some condition

@maxkatz6
Copy link
Member

It should be possible to solve problem from another issue with this feature #4184

<Button>
  <Button.Classes>
   <!-- Property is read from DataContext, Value could be Binding also -->
    <ClassSetter Name="PropEqualToValue" Property="{Binding Property}" Value="Value" />
  </Button.Casses>
</Button>
...
<Style Selector="Button.PropEqualToValue" />

Or with boolean operators #4122

<Button>
  <Button.Classes>
    <ClassSetter Name="PropEqualToValue" Trigger="{Binding Property == Value}" />
  </Button.Casses>
</Button>

Or with functions in binding #2493

<Button>
  <Button.Classes>
    <ClassSetter Name="PropEqualToValue" Trigger="{Binding Property.Equals(Value)}" />
  </Button.Casses>
</Button>

@maxkatz6
Copy link
Member

maxkatz6 commented Sep 9, 2020

Just created behavior for this.
Hope it will be useful for somebody
https://gist.github.com/maxkatz6/2c765560767f20cf0483be8fac29ff22

@maxkatz6
Copy link
Member

maxkatz6 commented Apr 27, 2021

Implemented with
#5710

New syntax is:

<Button Classes.myClass="{Binding xxx}"/>

@Yuuon
Copy link

Yuuon commented May 11, 2021

It should be possible to solve problem from another issue with this feature #4184

<Button>
  <Button.Classes>
   <!-- Property is read from DataContext, Value could be Binding also -->
    <ClassSetter Name="PropEqualToValue" Property="{Binding Property}" Value="Value" />
  </Button.Casses>
</Button>
...
<Style Selector="Button.PropEqualToValue" />

Or with boolean operators #4122

<Button>
  <Button.Classes>
    <ClassSetter Name="PropEqualToValue" Trigger="{Binding Property == Value}" />
  </Button.Casses>
</Button>

Or with functions in binding #2493

<Button>
  <Button.Classes>
    <ClassSetter Name="PropEqualToValue" Trigger="{Binding Property.Equals(Value)}" />
  </Button.Casses>
</Button>

Does the {binding property == value} get supported in current version(10.04)? Or there is no plan to implement on this?
working on a group of radio button, click a button will switch an image, but the image itself will change by time, so want the radio button update the selection at the same time. Need something like:
<RadioButton Classes="image" GroupName="Image" IsChecked="{Binding ImageIndex == 0}" Command="{Binding ImageScrollBtnOnClick}" CommandParameter="0"></RadioButton>
which I could use a single value to control this, otherwise I need to add each boolean for a radio button, I guess?

@maxkatz6
Copy link
Member

@Yuuon no, and there is plans to improve bindings, but there no clear design for that. This issue was about setting style classes with bindings. Personally I prefer UWP style, where you can write "{x:Bind ImageIndex.Equals(0)}", but there is no ETA for anything.

So far you can use converters or create additional boolean properties in your VM per each radiobutton.

@thomaslevesque
Copy link

New syntax is:

<Button Classes.myClass="{Binding xxx}"/>

This is nice for some scenarios, but not very convenient for others.
For instance, if I have several statuses, and I want a different style for each status, I have to do something like this:

    <Border Classes.status-draft="{Binding Status, Converter={StaticResource eq}, ConverterParameter={x:Static model:OrderStatus.Draft}}"
            Classes.status-abandoned="{Binding Status, Converter={StaticResource eq}, ConverterParameter={x:Static model:OrderStatus.Abandoned}}"
            Classes.status-paid="{Binding Status, Converter={StaticResource eq}, ConverterParameter={x:Static model:OrderStatus.Paid}}"
            Classes.status-delivered="{Binding Status, Converter={StaticResource eq}, ConverterParameter={x:Static model:OrderStatus.Delivered}}">
    ...

This is extremely verbose, and if I had a new status, I have to add one more line like this.

It would be nice to be able to do something like Classes="{Binding StatusClasses}" (I realize this exact syntax isn't perfect, since all the classes would need to be specified in StatusClasses...). Alternatively, having selectors based on bindings would be great (e.g. Selector="Border[DataContext.Status=Draft]")

@Rushberg
Copy link

Managed to get around it with attached property.

public class BindableStyleClasses
{
    static BindableStyleClasses()
    {
        ClassesProperty.Changed.Subscribe(x => HandleClassesChanged(x.Sender, x.NewValue.GetValueOrDefault<string>()));
    }

    public static readonly AttachedProperty<string> ClassesProperty = AvaloniaProperty.RegisterAttached<BindableStyleClasses, IStyledElement, string>(
        "Classes", default(string), false, BindingMode.OneTime);


    private static void HandleClassesChanged(IAvaloniaObject element, string? classes)
    {
        if (element is IStyledElement styled) styled.Classes = Classes.Parse(classes ?? "");
    }

    public static void SetClasses(AvaloniaObject element, ObjectTemplate commandValue)
    {
        element.SetValue(ClassesProperty, commandValue);
    }


    public static string GetClasses(AvaloniaObject element)
    {
        return element.GetValue(ClassesProperty);
    }
}

And the usage:

<UserControl.Styles>
    <Style Selector="TextBlock.ClassOne">
        <Setter Property="Background" Value="Red"/>
    </Style>
</UserControl.Styles>
<TextBlock Text="Red if CurrentClasses has ClassOne" ext:BindableStyleClasses.Classes="{Binding CurrentClasses}"/>

@Ready4Next
Copy link

I don't know if it's still relevant, but now, thanks to you, I can style my control based on an enum in a one-liner.

<Button ext:BindableStyleClasses.Classes="{Binding Game.State, Converter={StaticResource gamePlayingStateConverter}}" 																				
								HorizontalAlignment="Left" 
								VerticalAlignment="Center" 
								Tag="{Binding Game.Id}"/>

And the converter

public class GamePlayingStateConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null) 
                return null;

            if (value is GameState playState)
            {
                return playState switch 
                {
                    GameState.Running => "mainLogoBar green playing",
                    GameState.Available => "mainLogoBar blue available",
                    GameState.Updating => "mainLogoBar green updating",
                    _ => "mainLogoBar blue available"
                };
            }

            throw new NotSupportedException();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants