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

Extend Classes.bind syntax to support enum and string bindings #18068

Open
maxkatz6 opened this issue Jan 29, 2025 · 3 comments
Open

Extend Classes.bind syntax to support enum and string bindings #18068

maxkatz6 opened this issue Jan 29, 2025 · 3 comments

Comments

@maxkatz6
Copy link
Member

maxkatz6 commented Jan 29, 2025

Is your feature request related to a problem? Please describe.

Currenly Classes.className syntax only supports boolean bindings, which toggle specified class name on the control:

<Border Classes.className="{Binding BooleanProp}" />

By itself this syntax is very convenient, but also very limiting for enum and string bindings.
For example, if developer wants to toggle classes depending on enum value without any C# code, they should somehow convert it to boolean expression, which can be written this:

<Border Classes.is-verbose="{Binding Verbosity, ConverterParameter={x:Static LogEventLevel.Verbose}, Converter={x:Static ObjectConverters.Equal}}" />

With custom converters this syntax can be slightly improved, but far from ideal.

Describe the solution you'd like

Extend existing syntax by allowing non-boolean bindings. Combined with StringFormat when desired to customize property name.

<Border Classes.bind="{Binding Verbosity, StringFormat='vebosity-{0}'}" />

Where:

  1. "Classes.bind" is a reserved property.
  2. StringFormat is optional, when return property type is string or enum. Required for any other type.
  3. Flags enums are not supported (unless a custom converter).
  4. Single binding defines a group of class names, where only single one is set at a time.

It should also work with setters:

<Setter Property="Classes.bind" Value="{Binding Verbosity}" />

Problems:

  1. The tricky part is to draw a line between "this is a boolean binding" and a "this is an enum/string binding", especially when binding return type is unknown (reflection binding or converter are used). Unless we introduce a breaking change for any app that already used "Classes.bind" as a classname.
  2. With this syntax only single class binding is allowed per control. Since duplicated xml attributes are not allowed.

Describe alternatives you've considered

No response

Additional context

No response

@MrJul
Copy link
Member

MrJul commented Jan 29, 2025

Alternative syntax: allow binding Classes directly, e.g; Classes="{Binding Verbosity}".
This keeps the Classes.attr syntax for single classes only without needing to special case anything, and feels quite natural.

Also, regardless of the syntax, allow returning a space delimited string to apply several classes at once. Said another way, any string valid in Classes="" should be able to be returned by that binding.

(Optionally, for performance, we might also accept a property or converter returning a IReadOnlyList<string>.)

@kebox7
Copy link

kebox7 commented Jan 29, 2025

Alternative syntax: allow binding Classes directly, e.g; Classes="{Binding Verbosity}".

Well, something similar can be implemented now.

public static class BindableStyle {
    public static readonly AttachedProperty<string> ClassesProperty =
        AvaloniaProperty.RegisterAttached<StyledElement, string>("Classes", typeof(BindableStyle), string.Empty);

    public static void SetClasses(AvaloniaObject element, string value) =>
        element.SetValue(ClassesProperty, value);

    public static string GetClasses(AvaloniaObject element) =>
        element.GetValue(ClassesProperty);

    static BindableStyle() =>
        ClassesProperty.Changed.AddClassHandler<StyledElement>(OnClassesAttachedPropertyChanged);

    private static void OnClassesAttachedPropertyChanged(AvaloniaObject o, AvaloniaPropertyChangedEventArgs e) {
        if (o is StyledElement styled) {
            styled.Classes.Replace((e.GetNewValue<string>() ?? string.Empty).Split(' '));
        }
    }
}
<Rectangle>
    <ui:BindableStyle.Classes>
        <MultiBinding Converter="{StaticResource Converters.StatusItemToClasses}">
            <Binding Path="Value" />
            <Binding Path="Warning" />
        </MultiBinding>
    </ui:BindableStyle.Classes>
</Rectangle>

But I agree, it would have been better without the additional code.

ADD:
This approach has a disadvantage. We can only completely change all classes, and if it is necessary to add one to the existing ones or remove it, this can create problems.

@robloo
Copy link
Contributor

robloo commented Jan 30, 2025

Yea, we should allow a way to set one or more string value to Classes directly. The existing syntax was kind-of hacky as well.

However, I don't have a good idea in mind for this either but I would go a different route as @MrJul said. It makes the most sense to handle all the calculation and conversion of properties to class "string name" in the view model (no need to StringFormat). Then just support binding that view model string name to Classes property on controls.

In fact, the real issue is perhaps we should have made Classes a styles property somehow already. If it took an IEnumerable we could also bind a list of strings to it. I guess the issue is how to differentiate between classes set by binding (which can/should be replaced) vs. classes set elsewhere which need to remain. The classes added in the view model should be appended to what is already there.

Edit: Ok, ugly idea, but what if we had something like ClassesSource like we have ItemsSource. We can bind a separate list to that and it will just append them to existing Classes automatically. Keeping the lists separate solves the issue where bound classes need to be replaced and others should always remain.

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

4 participants