Passing {Binding} arguments doesn't work in Avalonia 11
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;
}
}
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:
- Name scopes are managed in Avalonia with IL generated at runtime
- 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:
- I'm not sure whether there's any point as it's trivial
- It breaks support for Avalonia 9 and I don't know whether you plan to support the old version too
- Support for ElementName needs to be fixed
- Would be nice if
CompiledBindingwas supported too