Skip to content

Conversation

grokys
Copy link
Member

@grokys grokys commented Sep 3, 2025

What does the pull request do?

This is the first in a series of planned tidy-ups of our binding API.

This PR addresses a few problems that we have with our binding APIs:

  • We have a client-facing IBinding interface and internal IBinding2 interface containing the interface that is actually used. These interfaces need to be merged into an abstract BindingBase class, with the Instance method as an internal method on BindingBase
  • BindingBase and Binding are in Avalonia.Markup instead of Avalonia.Base: this was because originally I assumed that we wouldn't need bindings outside of markup as we'd be using rx. This didn't turn out to be the case, and Binding is very much a Base feature
  • BindingBase as it stands isn't really a base class for bindings: MultiBinding doesn't inherit from it for example. Instead it holds common properties shared between Binding and CompiledBindingExtension
  • InstancedBinding has been marked as obsolete since 11.0 and should be removed for 12.0

This PR makes the following changes to address these issues:

  • Move BindingBase and MultiBinding into Avalonia.Base
  • BindingBase becomes a true base class for all bindings, and contains only the Instance method
  • Properties common between reflection and compiled bindings are moved into StandardBindingBase it was decided that this was unnecessary
  • Binding is moved to Avalonia.Base and renamed to ReflectionBinding
  • A compatibility shim for Binding remains in Avalonia.Markup - this allows the existing usages of new Binding("Path.Here") to continue compiling
  • Remove IBinding and IBinding2
  • Remove ITreeDataTemplate's usage of InstancedBinding`
  • Remove NativeMenuBarPresenters usage of InstancedBinding
  • Remove InstancedBinding as it is now unused

Breaking changes

Lots. In particular IBinding -> BindingBase breaks a LOT of stuff.

Also: This required an update to the DataGrid submodule: cell data validation has been temporarily removed as this used InstancedBinding.

@grokys grokys requested review from MrJul, Copilot and maxkatz6 September 3, 2025 13:46
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR consolidates Avalonia's binding architecture by introducing a new BindingBase class and moving core binding components from Avalonia.Markup to Avalonia.Base. The primary goal is to simplify the binding API by removing the dual IBinding/IBinding2 interface pattern and establishing a cleaner inheritance hierarchy.

Key changes include:

  • Replacement of IBinding/IBinding2 interfaces with an abstract BindingBase class containing an internal Instance method
  • Introduction of ReflectionBinding as the new name for the previous Binding class, with StandardBindingBase containing common properties
  • Migration of binding-related code from Avalonia.Markup to Avalonia.Base for better architectural organization

Reviewed Changes

Copilot reviewed 83 out of 84 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Avalonia.Base/Data/BindingBase.cs New abstract base class for all bindings with core Instance method
src/Avalonia.Base/Data/ReflectionBinding.cs New reflection-based binding implementation (renamed from Binding)
src/Avalonia.Base/Data/StandardBindingBase.cs Common properties shared between reflection and compiled bindings
src/Markup/Avalonia.Markup/Data/Binding.cs Compatibility shim extending ReflectionBinding
Multiple test files Updates to use ReflectionBinding instead of Binding and BindingBase instead of IBinding

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


return true;
}
public BindingBase ProvideValue() => this;
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The ProvideValue() method should return TemplateBinding instead of BindingBase to maintain type safety and allow callers to access specific TemplateBinding properties if needed.

Suggested change
public BindingBase ProvideValue() => this;
public TemplateBinding ProvideValue() => this;

Copilot uses AI. Check for mistakes.

nativeItem.GetObservable(NativeMenuItem.HeaderProperty).ToBinding(),
[!MenuItem.IconProperty] = nativeItem.GetObservable(NativeMenuItem.IconProperty)
.Select(i => i is { } bitmap ? new Image { Source = bitmap } : null).ToBinding(),
[!!MenuItem.IsCheckedProperty] = nativeItem[!!NativeMenuItem.IsCheckedProperty],
Copy link

Copilot AI Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line uses a two-way binding syntax ([!!Property]) but appears to be replacing the previous explicit two-way binding setup that was removed. Verify that this syntax provides the same two-way binding behavior as the removed InstancedBinding.TwoWay call.

Suggested change
[!!MenuItem.IsCheckedProperty] = nativeItem[!!NativeMenuItem.IsCheckedProperty],
[!MenuItem.IsCheckedProperty] = new Binding
{
Path = nameof(NativeMenuItem.IsChecked),
Source = nativeItem,
Mode = BindingMode.TwoWay
},

Copilot uses AI. Check for mistakes.

- Move `BindingBase` and `MultiBinding` into Avalonia.Base
- `BindingBase` becomes a true base class for all bindings, and contains only the `Instance` method
- Properties common between reflection and compiled bindings are moved into `StandardBindingBase`
- `Binding` is moved to Avalonia.Base and renamed to `ReflectionBinding`
- A compatibility shim for `Binding` remains in Avalonia.Markup
- Remove `IBinding` and `IBinding2`
- Remove `ITreeDataTemplate's usage of `InstancedBinding`
- Remove `NativeMenuBarPresenter`s usage of `InstancedBinding`
- Remove `InstancedBinding` as it is now unused

This required an update to the DataGrid submodule: cell data validation has been temporarily removed as this used `InstancedBinding`.
@grokys grokys force-pushed the refactor/ibinding-to-bindingbase branch from a23ade8 to d3c0ff2 Compare September 3, 2025 14:04
IDescription,
ISetterValue,
IDisposable
public partial class TemplateBinding : BindingBase
Copy link
Member Author

@grokys grokys Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TemplateBinding can no longer be both a binding and a binding expression now as we can't have multiple base classes. This will lead to increased memory usage: need to measure how much.

@grokys
Copy link
Member Author

grokys commented Sep 3, 2025

API diff between 12.0.999-cibuild0058697-alpha and 12.0.999

Avalonia.Base (net6.0, net8.0, netstandard2.0)

  namespace Avalonia
  {
      public class AvaloniaObject
      {
-         public Avalonia.Data.BindingExpressionBase Bind(Avalonia.AvaloniaProperty property, Avalonia.Data.IBinding binding);
-         public Avalonia.Data.IBinding this[Avalonia.Data.IndexerDescriptor binding] {
+         public Avalonia.Data.BindingBase this[Avalonia.Data.IndexerDescriptor binding] {
+         public Avalonia.Data.BindingExpressionBase Bind(Avalonia.AvaloniaProperty property, Avalonia.Data.BindingBase binding);
      }
      public static class AvaloniaObjectExtensions
      {
-         public static System.IDisposable Bind(this Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty property, Avalonia.Data.IBinding binding, object? anchor = null);
-         public static Avalonia.Data.IBinding ToBinding<T>(this System.IObservable<T> source);
+         public static Avalonia.Data.BindingBase ToBinding<T>(this System.IObservable<T> source);
+         public static System.IDisposable Bind(this Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty property, Avalonia.Data.BindingBase binding, object? anchor = null);
      }
      public static class StyledElementExtensions
      {
-         public static System.IDisposable BindClass(this Avalonia.StyledElement target, string className, Avalonia.Data.IBinding source, object anchor);
+         public static System.IDisposable BindClass(this Avalonia.StyledElement target, string className, Avalonia.Data.BindingBase source, object anchor);
      }
  }
  namespace Avalonia.Data
  {
      public static class BindingOperations
      {
-         public static System.IDisposable Apply(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty property, Avalonia.Data.InstancedBinding binding, object? anchor);
-         public static System.IDisposable Apply(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty property, Avalonia.Data.InstancedBinding binding);
      }
-     public interface IBinding
-     {
-         Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
-     }
-     public sealed class InstancedBinding
-     {
-         public static Avalonia.Data.InstancedBinding OneTime(System.IObservable<object?> observable, Avalonia.Data.BindingPriority priority = 0);
-         public static Avalonia.Data.InstancedBinding OneTime(object value, Avalonia.Data.BindingPriority priority = 0);
-         public static Avalonia.Data.InstancedBinding OneWay(System.IObservable<object?> observable, Avalonia.Data.BindingPriority priority = 0);
-         public static Avalonia.Data.InstancedBinding OneWayToSource(System.IObserver<object?> observer, Avalonia.Data.BindingPriority priority = 0);
-         public static Avalonia.Data.InstancedBinding TwoWay(System.IObservable<object?> observable, System.IObserver<object?> observer, Avalonia.Data.BindingPriority priority = 0);
-         public Avalonia.Data.InstancedBinding WithPriority(Avalonia.Data.BindingPriority priority);
- public Avalonia.Data.BindingMode Mode { get; }
-         public System.IObservable<object?> Observable { get; }
-         public Avalonia.Data.BindingPriority Priority { get; }
-         public System.IObservable<object?> Source { get; }
-     }
      public class TemplateBinding : Avalonia.Data.BindingBase
      {
-         public TemplateBinding() : base(default(Avalonia.Data.BindingPriority?), default(Avalonia.AvaloniaProperty?), default(bool?));
+         public TemplateBinding();
-         public TemplateBinding(Avalonia.AvaloniaProperty property) : base(default(Avalonia.Data.BindingPriority?), default(Avalonia.AvaloniaProperty?), default(bool?));
+         public TemplateBinding(Avalonia.AvaloniaProperty property);
-         public Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
-         public Avalonia.Data.IBinding ProvideValue();
+         public Avalonia.Data.BindingBase ProvideValue();
-         protected override void StartCore();
-         protected override void StopCore();
-         public System.IDisposable Subscribe(System.IObserver<object?> observer);
-         public override string Description { get; }
      }
+     public abstract class BindingBase
+     {
+         protected BindingBase();
+     }
+     public class MultiBinding : Avalonia.Data.BindingBase
+     {
+         public MultiBinding();
+         public System.Collections.Generic.IList<Avalonia.Data.BindingBase> Bindings { get; set; }
+         public Avalonia.Data.Converters.IMultiValueConverter? Converter { get; set; }
+         public System.Globalization.CultureInfo? ConverterCulture { get; set; }
+         public object? ConverterParameter { get; set; }
+         public object FallbackValue { get; set; }
+         public Avalonia.Data.BindingMode? Mode { get; set; }
+         public Avalonia.Data.BindingPriority? Priority { get; set; }
+         public Avalonia.Data.RelativeSource? RelativeSource { get; set; }
+         public string? StringFormat { get; set; }
+         public object TargetNullValue { get; set; }
+     }
+     public class ReflectionBinding : Avalonia.Data.StandardBindingBase
+     {
+         public ReflectionBinding();
+         public ReflectionBinding(string path, Avalonia.Data.BindingMode mode);
+         public ReflectionBinding(string path);
+         public string? ElementName { get; set; }
+         public string Path { get; set; }
+         public Avalonia.Data.RelativeSource? RelativeSource { get; set; }
+         public object? Source { get; set; }
+         public System.Func<string?, string, System.Type>? TypeResolver { get; set; }
+     }
+     public class RelativeSource
+     {
+         public RelativeSource();
+         public RelativeSource(Avalonia.Data.RelativeSourceMode? mode);
+         public int? AncestorLevel { get; set; }
+         public System.Type? AncestorType { get; set; }
+         public Avalonia.Data.RelativeSourceMode? Mode { get; set; }
+         public Avalonia.Data.TreeType? Tree { get; set; }
+     }
+     public sealed class RelativeSourceMode
+     {
+ public const Avalonia.Data.RelativeSourceMode DataContext = 0;
+         public const Avalonia.Data.RelativeSourceMode FindAncestor = 3;
+         public const Avalonia.Data.RelativeSourceMode Self = 2;
+         public const Avalonia.Data.RelativeSourceMode TemplatedParent = 1;
+         public int value__;
+     }
+     public abstract class StandardBindingBase : Avalonia.Data.BindingBase
+     {
+         protected StandardBindingBase();
+         public Avalonia.Data.Converters.IValueConverter? Converter { get; set; }
+         public System.Globalization.CultureInfo? ConverterCulture { get; set; }
+         public object? ConverterParameter { get; set; }
+         public int? Delay { get; set; }
+         public object? FallbackValue { get; set; }
+         public Avalonia.Data.BindingMode? Mode { get; set; }
+         public Avalonia.Data.BindingPriority? Priority { get; set; }
+         public string? StringFormat { get; set; }
+         public object? TargetNullValue { get; set; }
+         public Avalonia.Data.UpdateSourceTrigger? UpdateSourceTrigger { get; set; }
+     }
+     public sealed class TreeType
+     {
+ public const Avalonia.Data.TreeType Logical = 1;
+         public int value__;
+         public const Avalonia.Data.TreeType Visual = 0;
+     }
  }

Avalonia.Controls (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Controls
  {
      public class AutoCompleteBox : Avalonia.Controls.Primitives.TemplatedControl
      {
-         public Avalonia.Data.IBinding? ValueMemberBinding { get; set; }
+         public Avalonia.Data.BindingBase? ValueMemberBinding { get; set; }
          public class BindingEvaluator<T> : Avalonia.Controls.Control
          {
-             public BindingEvaluator(Avalonia.Data.IBinding? binding);
-             public Avalonia.Data.IBinding? ValueBinding { get; set; }
+             public Avalonia.Data.BindingBase? ValueBinding { get; set; }
+             public BindingEvaluator(Avalonia.Data.BindingBase? binding);
          }
      }
      public class ItemsControl : Avalonia.Controls.Primitives.TemplatedControl, Avalonia.LogicalTree.IChildIndexProvider
      {
-         public static readonly Avalonia.StyledProperty<Avalonia.Data.IBinding?> DisplayMemberBindingProperty;
+         public static readonly Avalonia.StyledProperty<Avalonia.Data.BindingBase?> DisplayMemberBindingProperty;
-         public Avalonia.Data.IBinding? DisplayMemberBinding { get; set; }
+         public Avalonia.Data.BindingBase? DisplayMemberBinding { get; set; }
      }
  }
  namespace Avalonia.Controls.Primitives
  {
      public class SelectingItemsControl : Avalonia.Controls.ItemsControl
      {
-         public static readonly Avalonia.StyledProperty<Avalonia.Data.IBinding?> SelectedValueBindingProperty;
+         public static readonly Avalonia.StyledProperty<Avalonia.Data.BindingBase?> SelectedValueBindingProperty;
-         public Avalonia.Data.IBinding? SelectedValueBinding { get; set; }
+         public Avalonia.Data.BindingBase? SelectedValueBinding { get; set; }
      }
      public static class TextSearch
      {
-         public static readonly Avalonia.AttachedProperty<Avalonia.Data.IBinding?> TextBindingProperty;
+         public static readonly Avalonia.AttachedProperty<Avalonia.Data.BindingBase?> TextBindingProperty;
-         public static Avalonia.Data.IBinding? GetTextBinding(Avalonia.Interactivity.Interactive interactive);
+         public static Avalonia.Data.BindingBase? GetTextBinding(Avalonia.Interactivity.Interactive interactive);
-         public static void SetTextBinding(Avalonia.Interactivity.Interactive interactive, Avalonia.Data.IBinding? value);
+         public static void SetTextBinding(Avalonia.Interactivity.Interactive interactive, Avalonia.Data.BindingBase? value);
      }
  }
  namespace Avalonia.Controls.Templates
  {
      public class FuncTreeDataTemplate : Avalonia.Controls.Templates.FuncDataTemplate, Avalonia.Controls.Templates.ITreeDataTemplate, Avalonia.Controls.Templates.IDataTemplate, Avalonia.Controls.Templates.ITemplate<object?, Avalonia.Controls.Control?>
      {
-         public Avalonia.Data.InstancedBinding ItemsSelector(object item);
+         public System.IDisposable BindChildren(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty targetProperty, object item);
      }
      public interface ITreeDataTemplate : Avalonia.Controls.Templates.IDataTemplate, Avalonia.Controls.Templates.ITemplate<object?, Avalonia.Controls.Control?>
      {
-         Avalonia.Data.InstancedBinding? ItemsSelector(object item);
+         System.IDisposable BindChildren(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty targetProperty, object item);
      }
  }

Avalonia.Markup (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Data
  {
      public class Binding : Avalonia.Data.ReflectionBinding
      {
-         public Binding(string path, Avalonia.Data.BindingMode mode = 0);
+         public Binding(string path, Avalonia.Data.BindingMode mode);
-         public override Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
-         public string? ElementName { get; set; }
-         public string Path { get; set; }
-         public Avalonia.Data.RelativeSource? RelativeSource { get; set; }
-         public object? Source { get; set; }
-         public System.Func<string?, string, System.Type>? TypeResolver { get; set; }
+         public Binding(string path);
      }
-     public abstract class BindingBase : Avalonia.Data.IBinding
-     {
-         public BindingBase();
-         public BindingBase(Avalonia.Data.BindingMode? mode = 0);
-         public abstract Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
-         public Avalonia.Data.Converters.IValueConverter? Converter { get; set; }
-         public System.Globalization.CultureInfo? ConverterCulture { get; set; }
-         public object? ConverterParameter { get; set; }
-         public System.WeakReference? DefaultAnchor { get; set; }
-         public int? Delay { get; set; }
-         public object? FallbackValue { get; set; }
-         public Avalonia.Data.BindingMode? Mode { get; set; }
-         public System.WeakReference<Avalonia.Controls.INameScope?>? NameScope { get; set; }
-         public Avalonia.Data.BindingPriority? Priority { get; set; }
-         public string? StringFormat { get; set; }
-         public object? TargetNullValue { get; set; }
-         public Avalonia.Data.UpdateSourceTrigger? UpdateSourceTrigger { get; set; }
-     }
-     public class MultiBinding : Avalonia.Data.IBinding
-     {
-         public MultiBinding();
-         public Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
-         public System.Collections.Generic.IList<Avalonia.Data.IBinding> Bindings { get; set; }
-         public Avalonia.Data.Converters.IMultiValueConverter? Converter { get; set; }
-         public System.Globalization.CultureInfo? ConverterCulture { get; set; }
-         public object? ConverterParameter { get; set; }
-         public object FallbackValue { get; set; }
-         public Avalonia.Data.BindingMode? Mode { get; set; }
-         public Avalonia.Data.BindingPriority? Priority { get; set; }
-         public Avalonia.Data.RelativeSource? RelativeSource { get; set; }
-         public string? StringFormat { get; set; }
-         public object TargetNullValue { get; set; }
-     }
-     public class RelativeSource
-     {
-         public RelativeSource();
-         public RelativeSource(Avalonia.Data.RelativeSourceMode? mode);
-         public int? AncestorLevel { get; set; }
-         public System.Type? AncestorType { get; set; }
-         public Avalonia.Data.RelativeSourceMode? Mode { get; set; }
-         public Avalonia.Data.TreeType? Tree { get; set; }
-     }
-     public sealed class RelativeSourceMode
-     {
- public const Avalonia.Data.RelativeSourceMode DataContext = 0;
-         public const Avalonia.Data.RelativeSourceMode FindAncestor = 3;
-         public const Avalonia.Data.RelativeSourceMode Self = 2;
-         public const Avalonia.Data.RelativeSourceMode TemplatedParent = 1;
-         public int value__;
-     }
-     public sealed class TreeType
-     {
- public const Avalonia.Data.TreeType Logical = 1;
-         public int value__;
-         public const Avalonia.Data.TreeType Visual = 0;
-     }
  }

Avalonia.Markup.Xaml (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Markup.Xaml.MarkupExtensions
  {
      public class CompiledBindingExtension : Avalonia.Data.StandardBindingBase
      {
-         public override Avalonia.Data.InstancedBinding? Initiate(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty? targetProperty, object? anchor = null, bool? enableDataValidation = false);
      }
      public class DynamicResourceExtension : Avalonia.Data.BindingBase
      {
-         public Avalonia.Data.IBinding ProvideValue(System.IServiceProvider serviceProvider);
+         public Avalonia.Data.BindingBase ProvideValue(System.IServiceProvider serviceProvider);
      }
      public class ReflectionBindingExtension : Avalonia.Data.ReflectionBinding
      {
+         public Avalonia.Data.ReflectionBinding ProvideValue(System.IServiceProvider serviceProvider);
-         public Avalonia.Data.Binding ProvideValue(System.IServiceProvider serviceProvider);
-         public Avalonia.Data.Converters.IValueConverter? Converter { get; set; }
-         public System.Globalization.CultureInfo? ConverterCulture { get; set; }
-         public object? ConverterParameter { get; set; }
-         public int? Delay { get; set; }
-         public string? ElementName { get; set; }
-         public object? FallbackValue { get; set; }
-         public Avalonia.Data.BindingMode? Mode { get; set; }
-         public string Path { get; set; }
-         public Avalonia.Data.BindingPriority? Priority { get; set; }
-         public Avalonia.Data.RelativeSource? RelativeSource { get; set; }
-         public object? Source { get; set; }
-         public string? StringFormat { get; set; }
-         public object? TargetNullValue { get; set; }
-         public Avalonia.Data.UpdateSourceTrigger? UpdateSourceTrigger { get; set; }
+         public ReflectionBindingExtension(string path, Avalonia.Data.BindingMode mode);
      }
  }
  namespace Avalonia.Markup.Xaml.Templates
  {
      public class TreeDataTemplate : Avalonia.Controls.Templates.ITreeDataTemplate, Avalonia.Controls.Templates.IDataTemplate, Avalonia.Controls.Templates.ITemplate<object?, Avalonia.Controls.Control?>, Avalonia.Controls.Templates.ITypedDataTemplate
      {
-         public Avalonia.Data.InstancedBinding? ItemsSelector(object item);
+         public System.IDisposable BindChildren(Avalonia.AvaloniaObject target, Avalonia.AvaloniaProperty targetProperty, object item);
      }
  }

@MrJul MrJul added enhancement area-bindings backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch breaking-change and removed backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch labels Sep 3, 2025
@maxkatz6
Copy link
Member

maxkatz6 commented Sep 4, 2025

I am not sure if ReflectionBinding should be in the Avalonia.Base. For a type that needs be phased out in favor of typed Binding.Create(lambda) or XAML compiled bindings, it's way too integrated in the current and new API.

I understand idea behind keeping it for compatibility with older apps, but it's really not ideal when Binding type (one that would be the first choice for users who don't know better options) is synonym for ReflectionBinding.

// Avalonia.Base

// 1. Use "abstract Binding" instead of `StandardBindingBase`.
- public abstract class StandardBindingBase : BindingBase;
- public class Binding : ReflectionBinding
+ public abstract class Binding : BindingBase

// 2. Make ReflectionBinding internal and hidden (not a breaking change anyway)
-public class ReflectionBinding : StandardBindingBase;
+internal sealed class ReflectionBinding : Binding;

// 3. Add static methods to the Binding class:
public abstract class Binding : BindingBase
{
+     public static Binding Create(Func<>) - outside of this PR, but planned for future
+     [RequiresUnreferencedCode]
+     public static Binding Create(string) - returns ReflectionBinding
|
// Avalonia.Markup.Xaml
// 4. Just adjusting extensions.

-public class ReflectionBindingExtension : ReflectionBinding;
+public class ReflectionBindingExtension : Binding; // ProvideValue returns internal ReflectionBinding

-public class CompiledBindingExtension : StandardBindingBase;
+public class CompiledBindingExtension : Binding;

@maxkatz6
Copy link
Member

maxkatz6 commented Sep 4, 2025

Another option, less breaking, (though, not sure how it can be implemented), would be following API:

-public class Binding : ReflectionBinding
-public class ReflectionBinding;
+public class Binding : StandardBindingBase
{
+     [Obsolete("Use Binding Create(Func<>)")]
      public Binding(string)
+     public static Binding Create(Func<>);
}

Keeping new Binding(string) to use reflection binding under the hood, but without making whole class a ReflectionBinding.

@grokys
Copy link
Member Author

grokys commented Sep 5, 2025

I am not sure if ReflectionBinding should be in the Avalonia.Base. For a type that needs be phased out in favor of typed Binding.Create(lambda) or XAML compiled bindings, it's way too integrated in the current and new API.

What would you suggest? Moving all reflection features to e.g. Avalonia.Base.Reflection?

I understand idea behind keeping it for compatibility with older apps, but it's really not ideal when Binding type (one that would be the first choice for users who don't know better options) is synonym for ReflectionBinding.

Yep, very good point - and one I went back and forth on. Originally I didn't want to include any compatibility APIs in this PR but the issue is we use new Binding(string) all over our tests (there are literally hundreds of references to it) and changing all of those to read new ReflectionBinding(string) would have made this PR really hard to read!

I can definitely mark these constructors as [Obsolete] too (at the expense of flooding the build output with warnings).

@maxkatz6
Copy link
Member

maxkatz6 commented Sep 5, 2025

What would you suggest? Moving all reflection features to e.g. Avalonia.Base.Reflection?

If it's possible to build programs without reflection APIs being accessed at all (i.e. not being a base class for most common binding type) - then these reflection API can be in any assembly, we prefer.
So yeah, I don't have issues with reflection internals being part of Avalonia.Base, if it's not explicitly used anywhere, unless user needs it (not accidentally using, but needing it).

{
return InstancedBinding.OneTime(_itemsSelector(item));
target.SetCurrentValue(targetProperty, _itemsSelector(item));
return Disposable.Empty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that correct? The documentation states that the returned IDisposable can be disposed to remove the binding -- this isn't the case here.

The use of "Instance" as a verb is quite unusual apparently ;)
Simply duplicate the members in reflection and compiled binding classes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants