EventBinder icon indicating copy to clipboard operation
EventBinder copied to clipboard

Passing {Binding} arguments doesn't work in Avalonia 11

Open Athari opened this issue 5 months ago • 1 comments

I can't get {Binding} arguments to work in Avalonia 11.3.0 when using EventBinder 2.5.3-11.0.0-preview1 or the master branch.

When I use {e:EventBinding Method, {CompiledBinding}} (or {e:EventBinding Method, {Binding}} with compiled bindings as a default), EventBinder tries passing a CompiledBinding instance to the method. If the argument's type is the binding's expected value, System.MissingMethodException is thrown due to argument type mismatch. If the argument's type is object, I receive the CompiledBinding instance which I don't know what to do with.

When I use {e:EventBinding Method, {ReflectionBinding}}, I get System.TypeLoadException with this message: "Could not load type 'Avalonia.Controls.IControl' from assembly 'Avalonia.Controls, Version=11.3.0.0, Culture=neutral, PublicKeyToken=c8d484a7012f9a8b'".

All tests in the solution in the master branch still pass. If I update the Avalonia package dependency in the EventBinder.AvaloniaPreview project from <PackageReference Include="Avalonia" Version="11.0.0-preview1" /> to <PackageReference Include="Avalonia" Version="11.0.0" />, I receive the following errors in Emitter.cs:

  • The type or namespace name 'IStyledElement' does not exist in the namespace 'Avalonia' (are you missing an assembly reference?)
  • Argument 1: cannot convert from 'Avalonia.Visual' to 'Avalonia.IStyledElement'
  • The type or namespace name 'IControl' could not be found (are you missing a using directive or an assembly reference?)
  • The type or namespace name 'IControl' could not be found (are you missing a using directive or an assembly reference?)

Furthermore, Binding.Initiate method EventBinder relies on is currently marked as obsolete with [Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] which is "This API may be removed in Avalonia 12. If you depend on this API, please open an issue with details of your use-case.". The same attribute is on the Initiate methods of other Binding types and on BindingOperations.Apply.

Related issues:

  • AvaloniaUI/Avalonia#14791
  • AvaloniaUI/Avalonia#15270
Demo code

Here I'm using DataContext property to make the root model accessible to EventBinding as it doesn't seem to support referencing element names like #root. Please correct me if that's not the optimal or correct solution.

MainWindow.axaml

<Window x:Class="EventBinder.Demo.Avalonia.MainWindow" Name="root"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:my="using:EventBinder.Demo.Avalonia"
        xmlns:e="using:EventBinder"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:DataType="my:ToDoListModel"
        Title="ToDo"
        Padding="8">
    <Design.DataContext>
        <my:ToDoListModel />
    </Design.DataContext>
    <Window.Styles>
        <Style Selector="StackPanel">
            <Setter Property="Spacing" Value="4" />
        </Style>
    </Window.Styles>
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <Button Content="Add" Click="{e:EventBinding AddToDoItem}" />
            <Button Content="Clear" Click="{e:EventBinding ClearToDoItems}" />
        </StackPanel>
        <ItemsControl ItemsSource="{Binding ToDoItems}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="my:ToDoItemModel">
                    <StackPanel Orientation="Horizontal">
                        <CheckBox IsChecked="{Binding IsDone}" />
                        <TextBox Text="{Binding Text}" MinWidth="200" />
                        <StackPanel Orientation="Horizontal">
                            <Button Content="X"
                                    DataContext="{Binding #root.DataContext}" x:DataType="my:ToDoListModel"
                                    Click="{e:EventBinding RemoveToDoItem, {Binding}}" />
                            <Button Content="X"
                                    DataContext="{CompiledBinding #root.DataContext}" x:DataType="my:ToDoListModel"
                                    Click="{e:EventBinding RemoveToDoItem, {CompiledBinding}}" />
                            <Button Content="X"
                                    DataContext="{ReflectionBinding #root.DataContext}" x:DataType="my:ToDoListModel"
                                    Click="{e:EventBinding RemoveToDoItem, {ReflectionBinding}}" />
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</Window>

App.axaml.cs

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;

namespace EventBinder.Demo.Avalonia;

public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
            desktop.MainWindow = new MainWindow { DataContext = new ToDoListModel() };
        base.OnFrameworkInitializationCompleted();
    }
}

ToDoListModel.cs

using System.Collections.ObjectModel;

namespace EventBinder.Demo.Avalonia;

public class ToDoListModel : ModelBase
{
    public ObservableCollection<ToDoItemModel> ToDoItems { get; } = [
        new() { Text = "Wake up", IsDone = true },
        new() { Text = "Eat an egg" },
        new() { Text = "Drink a cup of coffee" },
    ];

    public void AddToDoItem() =>
        ToDoItems.Add(new() { Text = $"ToDo #{ToDoItems.Count + 1}" });

    public void ClearToDoItems() =>
        ToDoItems.Clear();

    public void RemoveToDoItem(ToDoItemModel item) =>
        ToDoItems.Remove(item);
}

ToDoItemModel.cs

namespace EventBinder.Demo.Avalonia;

public class ToDoItemModel : ModelBase
{
    public string Text
    {
        get;
        set => SetField(ref field, value);
    } = "";

    public bool IsDone
    {
        get;
        set => SetField(ref field, value);
    } = false;
}

ModelBase.cs

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace EventBinder.Demo.Avalonia;

public class ModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

Athari avatar Jun 28 '25 10:06 Athari

Tried fixing the problem by applying straightforward changes:

 EventBinder.AvaloniaPreview/EventBinder.AvaloniaPreview.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/EventBinder.AvaloniaPreview/EventBinder.AvaloniaPreview.csproj b/EventBinder.AvaloniaPreview/EventBinder.AvaloniaPreview.csproj
index 15570e7..b3a554a 100644
--- a/EventBinder.AvaloniaPreview/EventBinder.AvaloniaPreview.csproj
+++ b/EventBinder.AvaloniaPreview/EventBinder.AvaloniaPreview.csproj
@@ -12,7 +12,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Avalonia" Version="11.0.0-preview1" />
+    <PackageReference Include="Avalonia" Version="11.0.0" />
   </ItemGroup>
 
 </Project>
 EventBinder.Shared/Emitter.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/EventBinder.Shared/Emitter.cs b/EventBinder.Shared/Emitter.cs
index 1cafaec..869bf42 100644
--- a/EventBinder.Shared/Emitter.cs
+++ b/EventBinder.Shared/Emitter.cs
@@ -7,7 +7,7 @@ using System.Threading;
 #if AVALONIA
 	using Avalonia.Controls;
 using Avalonia.Data;
-using XamlControlBase = Avalonia.IStyledElement;
+	using XamlControlBase = Avalonia.StyledElement;
 	using XamlControl = Avalonia.Visual;
 	using XamlBinding = Avalonia.Data.Binding;
 #else
@@ -285,8 +285,8 @@ namespace EventBinder
 			else
             {
 #if AVALONIA
-	            var root = GetRootParent(_source) as IControl;
-	            context = root.FindControl<IControl>(binding.ElementName);
+	            var root = GetRootParent(_source) as Control;
+	            context = root.FindControl<Control>(binding.ElementName);
 #else
 				var root = System.Windows.Window.GetWindow(_source) ?? (XamlControl)GetRootParent(_source);
 				context = root.FindName(binding.ElementName) as System.Windows.DependencyObject;

It kinda works for ReflectionBinding, but support for ElementName is broken. If I use straightforward code:

<DataTemplate DataType="my:ToDoItemModel">
  <StackPanel Orientation="Horizontal" x:Name="rootPanel">
    ...
    <Button Content="X"
      DataContext="{Binding #root.DataContext}" x:DataType="my:ToDoListModel"
      Click="{e:EventBinding RemoveToDoItem, {ReflectionBinding DataContext, ElementName=rootPanel}}" />

Then root.FindControl<Control>(binding.ElementName) throws InvalidOperationException("Could not find parent name scope.") from ControlExtensions.FindControl. I tried solving this, but:

  1. Name scopes are managed in Avalonia with IL generated at runtime
  2. Nobody else on the whole Internet has ever seen this exception, except for one irrelevant discussion in Avalonia project.

I have no idea how to even approach this problem and it's kinda crucial for what I want. I managed to hack together a dirty workaround with converters and markup extensions and ended up with this code:

<Button Content="X"
  x:DataType="my:ToDoListModel+ListAndItem"
  DataContext="{my:MultiBinding {Binding #root.DataContext}, {Binding #rootPanel.DataContext},
    Converter={my:ToDoListModel+ListAndItem}}"
  Click="{e:EventBinding V1.RemoveToDoItem, {ReflectionBinding V2}}" />

or without a nested subclass:

<Button Content="X"
  DataContext="{my:MultiBinding {Binding #root.DataContext}, {Binding #rootPanel.DataContext},
    Converter={my:MultiValue, x:TypeArguments='my:ToDoListModel, my:ToDoItemModel'}}"
  Click="{e:EventBinding V1.RemoveToDoItem, {ReflectionBinding V2}}" />
Support code

MultiValue.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
using AvaloniaMultiBinding = Avalonia.Data.MultiBinding;

namespace EventBinder.Demo.Avalonia;

public class MultiBinding : AvaloniaMultiBinding
{
    public MultiBinding(IBinding b1, IBinding b2) =>
        new List<IBinding>([ b1, b2 ]).ForEach(Bindings.Add);

    public MultiBinding(IBinding b1, IBinding b2, IBinding b3) =>
        new List<IBinding>([ b1, b2, b3 ]).ForEach(Bindings.Add);

    public MultiBinding(IBinding b1, IBinding b2, IBinding b3, IBinding b4) =>
        new List<IBinding>([ b1, b2, b3, b4 ]).ForEach(Bindings.Add);

    public object ProvideValue(IServiceProvider provider) => this;
}

public record class MultiValue<T1, T2> : IMultiValueConverter
{
    public T1 V1 { get; set; }
    public T2 V2 { get; set; }

    public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture) =>
        new MultiValue<T1, T2> { V1 = (T1)values[0]!, V2 = (T2)values[1]! };

    public object ProvideValue(IServiceProvider serviceProvider) => this;
}

ToDoListModel.cs

using System.Collections.ObjectModel;

namespace EventBinder.Demo.Avalonia;

public class ToDoListModel : ModelBase
{
    public ObservableCollection<ToDoItemModel> ToDoItems { get; } = [
        new() { Text = "Wake up", IsDone = true },
        new() { Text = "Eat an egg" },
        new() { Text = "Drink a cup of coffee" },
    ];

    public void AddToDoItem() =>
        ToDoItems.Add(new() { Text = $"ToDo #{ToDoItems.Count + 1}" });

    public void ClearToDoItems() =>
        ToDoItems.Clear();

    public void RemoveToDoItem(ToDoItemModel item) =>
        ToDoItems.Remove(item);

    //public void RemoveToDoItem(object item) { }

    public record class ListAndItem : MultiValue<ToDoListModel, ToDoItemModel>;
}

I can submit a pull request with the 3-line changeset above, but:

  1. I'm not sure whether there's any point as it's trivial
  2. It breaks support for Avalonia 9 and I don't know whether you plan to support the old version too
  3. Support for ElementName needs to be fixed
  4. Would be nice if CompiledBinding was supported too

Athari avatar Jun 28 '25 12:06 Athari