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

Binding System refactor #13970

Merged
merged 68 commits into from
Jan 29, 2024
Merged

Binding System refactor #13970

merged 68 commits into from
Jan 29, 2024

Conversation

grokys
Copy link
Member

@grokys grokys commented Dec 15, 2023

What does the pull request do?

A major refactor of the binding system:

  • Simplified the BindingExpression code: no more separate BindingObserver and BindingExpression - everything is now implemented in BindingExpression itself
  • Added a new BindingExpressionBase that acts as a base-class for the various types of binding expressions
  • Added a new Bind method on AvaloniaObject that returns the created BindingExpressionBase
  • BindingExpressions can now be put directly into the ValueStore meaning that no intermediate wrapper objects are required, reducing memory usage
  • Because BindingExpression now has more information about what's being bound, we can log less extraneous warnings (Error logged to output window when binding to Run.Text #9422, Binding errors are being logged even before DataContext is set #5762)

This allows us to add a new feature in this PR and more new features in future:

Benchmarks

master

Method Mean Error StdDev Gen0 Allocated
Setup_DataContext_Property_Binding_OneWay 127.5 us 1.07 us 0.95 us 12.9395 212.63 KB
Setup_DataContext_Property_Binding_TwoWay 167.9 us 1.12 us 1.05 us 17.0898 280.59 KB
Method Mean Error StdDev Gen0 Allocated
Produce_DataContext_Property_Binding_Value_OneWay 1.454 us 0.0167 us 0.0140 us 0.1450 2.38 KB
Produce_DataContext_Property_Binding_Value_TwoWay 75.302 us 0.3877 us 0.3627 us 2.0752 35.87 KB
Method Mean Error StdDev Gen0 Allocated
Setup_TemplateBinding_OneWay 45.46 us 0.292 us 0.274 us 4.8828 80.47 KB
Setup_TemplateBinding_TwoWay 84.09 us 0.315 us 0.279 us 7.8125 128.13 KB
Method Mean Error StdDev Gen0 Allocated
Produce_TemplateBinding_Value_OneWay 24.76 us 0.085 us 0.075 us 1.1292 18.77 KB
Produce_TemplateBinding_Value_TwoWay 65.14 us 0.259 us 0.242 us 2.1973 37.22 KB

refactor/bindingexpressions-in-valuestore

Method Mean Error StdDev Gen0 Allocated
Setup_DataContext_Property_Binding_OneWay 122.8 us 0.70 us 0.62 us 5.6152 95.45 KB
Setup_DataContext_Property_Binding_TwoWay 130.3 us 0.92 us 0.81 us 7.0801 118.1 KB
Method Mean Error StdDev Gen0 Allocated
Produce_DataContext_Property_Binding_Value_OneWay 1.457 us 0.0073 us 0.0069 us 0.0725 1.21 KB
Produce_DataContext_Property_Binding_Value_TwoWay 30.309 us 0.1168 us 0.1036 us 1.0681 17.84 KB
Method Mean Error StdDev Gen0 Allocated
Setup_TemplateBinding_OneWay 49.83 us 0.266 us 0.248 us 4.2725 70.45 KB
Setup_TemplateBinding_TwoWay 48.76 us 0.216 us 0.202 us 4.2725 70.45 KB
Method Mean Error StdDev Gen0 Allocated
Produce_TemplateBinding_Value_OneWay 22.19 us 0.198 us 0.185 us 1.4648 24.27 KB
Produce_TemplateBinding_Value_TwoWay 55.34 us 0.169 us 0.158 us 2.8687 47.71 KB

As can be seen, almost all of the benchmarks have significantly better memory usage and most have better or equal performance. There are a couple of exceptions though: Setup_TemplateBinding_OneWay and Produce_TemplateBinding_Value_OneWay.

What's happening here is that we're now storing the current value from the binding as a boxed value in TemplateBinding whereas before an IValueEntry<T> was being created for each template binding, meaning that the value didn't need to be boxed. Setup_TemplateBinding_OneWay being slightly slower is a result of this: a little more GC is taking place. This is offset by the reduced memory cost for setting up a TemplateBinding after the refactor.

Breaking changes

  • Binding.Source default value is now Unsetvalue - setting it to null now means "bind to null source" instead of "bind to data context" (as in WPF)
  • FallbackValue and TargetNullValue conversion errors are no longer passed to the binding target as BindingNotifications - just logs an error
  • TemplateBinding now throws if Mode is set to OneTime or OneWayToSource - previously it silently reverted to OneWay

Obsoletions / Deprecations

Fixed issues

Fixes #3754
Fixes #5762
Fixes #9422
Fixes #10469
Fixes #10793
Fixes #13533
Fixes #13734

- `ExpressionObserver` has been removed and its functionality merged with `BindingExpression`
- `BindingExpression` handles all types of `BindingMode` itself; doesn't require `BindingOperations.Apply` to set up a separate observable for `TwoWay/`OneWayToSource` bindings
  - This allows us to fix some long-standing issues with `OneWayToSource` bindings
- Expression nodes have been refactored
  - No longer split between `Avalonia.Base` and `Avalonia.Markup`
  - Categorize them according to whether they use reflection or not

A few tests are failing around binding warnings: this is because the next step here is to fix binding warnings.
Null is a theoretically valid value for `Source`; setting it to null shouldn't mean "use the data context".
As `BindingExpression` now has enough information to decide when it's appropriate to log an error/warning or not.

Fixes #5762
Fixes #9422
Previously, `CompiledBindingPathBuilder` didn't have a `TemplatedParent` method and instead the XAML compiler rewrite templated parent bindings to be a `$self.TemplateParent` property binding. resulting in extraneous logs.

Add a constructor with an  `apiVersion`  to `CompiledBindingPathBuilder` which will be used by newer versions of the XAML compiler, and if a usage is detected using an `apiVersion` of 0, then upgrade `$self.TemplatedParent` to use a `TemplatedParentPathElement`.
...on elements which aren't yet rooted.
"Could not convert" instead of "Cannot convert".
A bit of a hack as we'd ideally not be using reflection when using compiled bindings.
Should only be used for tests.
And also use "identity equals" where value types and strings use `object.Equals` and reference types use `object.ReferenceEquals`.
Removed deleted files from csproj that were re-added due to indentation changes.
And `UntypedBindingExpressionBase`.
grokys added a commit that referenced this pull request Feb 29, 2024
Certain conversions rely on type converters, which were disabled in compiled bindings since #13970 due to warnings that type converters are not trimming friendly.

Ideally we'd be generating the type conversion logic in the XAML compiler, but in reality the problem with type converters and trimming is limited to type converters with generics, which is an edge case.

For the moment re-enable the usage of type converters in compiled bindings until we implement generating the conversion code in the XAML compiler.
github-merge-queue bot pushed a commit that referenced this pull request Feb 29, 2024
* Update ncrunch config.

* Add tests for converting strings to brushes.

* Make complied bindings use TypeConverters.

Certain conversions rely on type converters, which were disabled in compiled bindings since #13970 due to warnings that type converters are not trimming friendly.

Ideally we'd be generating the type conversion logic in the XAML compiler, but in reality the problem with type converters and trimming is limited to type converters with generics, which is an edge case.

For the moment re-enable the usage of type converters in compiled bindings until we implement generating the conversion code in the XAML compiler.
grokys added a commit that referenced this pull request May 14, 2024
Usually it is not necessary to provide the target property when creating a `BindingExpression` because the property will be assigned when the binding expression is attached to the target in `BindingExpressionBase.Attach`.

This is however one case where `Attach` is not called: when the obsolete `binding.Initiate` method is called and then an observable is read from the `InstancedBinding` without the binding actually being attached to the target object. In this case, prior to the binding refactor in #13970 the value produced by the observable was still converted to the target type. After #13970, because the target property (and hence the target type) is not yet set, the conversion is to the target type is no longer done.

`DataGrid` uses this obsolete method when editing cells, causing #15081. Ideally we'd fix that in `DataGrid` but I'm not happy making this change so close to 11.1, so instead fix this use-case to behave as before.

Fixes #15081
github-merge-queue bot pushed a commit that referenced this pull request May 14, 2024
* Added failing tests for #15081.

* Provide target property in BindingExpression ctor.

Usually it is not necessary to provide the target property when creating a `BindingExpression` because the property will be assigned when the binding expression is attached to the target in `BindingExpressionBase.Attach`.

This is however one case where `Attach` is not called: when the obsolete `binding.Initiate` method is called and then an observable is read from the `InstancedBinding` without the binding actually being attached to the target object. In this case, prior to the binding refactor in #13970 the value produced by the observable was still converted to the target type. After #13970, because the target property (and hence the target type) is not yet set, the conversion is to the target type is no longer done.

`DataGrid` uses this obsolete method when editing cells, causing #15081. Ideally we'd fix that in `DataGrid` but I'm not happy making this change so close to 11.1, so instead fix this use-case to behave as before.

Fixes #15081
grokys added a commit that referenced this pull request May 14, 2024
When #13970 was written, [a check](https://github.com/AvaloniaUI/Avalonia/pull/13970/files#diff-cfb25a491b9452e1815aa2c0d71465aaf81e99792a88a04a1a2ed572fd1930ffR60) was added to ensure that nested `BindingNotification`s didn't happen, and the refactor was written with the assumption that they wouldn't happen.

The problem is that they _do_ happen: when a source object implements both `INotifyDataErrorInfo` and had data annotations, then the nested data validation plugins would each wrap the value coming from the previous plugin in a new `BindingNotification`, resulting in nested `BindingNotifications`.

This adds support for nested binding notifications back in - even though IMO nesting binding notifications is a bug, if we're doing it and we previously supported it then we should continue to support it.

Fixes #15201
github-merge-queue bot pushed a commit that referenced this pull request May 14, 2024
* Added failing test for #15201.

* Handle nested BindingNotifications.

When #13970 was written, [a check](https://github.com/AvaloniaUI/Avalonia/pull/13970/files#diff-cfb25a491b9452e1815aa2c0d71465aaf81e99792a88a04a1a2ed572fd1930ffR60) was added to ensure that nested `BindingNotification`s didn't happen, and the refactor was written with the assumption that they wouldn't happen.

The problem is that they _do_ happen: when a source object implements both `INotifyDataErrorInfo` and had data annotations, then the nested data validation plugins would each wrap the value coming from the previous plugin in a new `BindingNotification`, resulting in nested `BindingNotifications`.

This adds support for nested binding notifications back in - even though IMO nesting binding notifications is a bug, if we're doing it and we previously supported it then we should continue to support it.

Fixes #15201
grokys added a commit that referenced this pull request Jun 3, 2024
* Added failing tests for #15081.

* Provide target property in BindingExpression ctor.

Usually it is not necessary to provide the target property when creating a `BindingExpression` because the property will be assigned when the binding expression is attached to the target in `BindingExpressionBase.Attach`.

This is however one case where `Attach` is not called: when the obsolete `binding.Initiate` method is called and then an observable is read from the `InstancedBinding` without the binding actually being attached to the target object. In this case, prior to the binding refactor in #13970 the value produced by the observable was still converted to the target type. After #13970, because the target property (and hence the target type) is not yet set, the conversion is to the target type is no longer done.

`DataGrid` uses this obsolete method when editing cells, causing #15081. Ideally we'd fix that in `DataGrid` but I'm not happy making this change so close to 11.1, so instead fix this use-case to behave as before.

Fixes #15081
grokys added a commit that referenced this pull request Jun 3, 2024
* Added failing test for #15201.

* Handle nested BindingNotifications.

When #13970 was written, [a check](https://github.com/AvaloniaUI/Avalonia/pull/13970/files#diff-cfb25a491b9452e1815aa2c0d71465aaf81e99792a88a04a1a2ed572fd1930ffR60) was added to ensure that nested `BindingNotification`s didn't happen, and the refactor was written with the assumption that they wouldn't happen.

The problem is that they _do_ happen: when a source object implements both `INotifyDataErrorInfo` and had data annotations, then the nested data validation plugins would each wrap the value coming from the previous plugin in a new `BindingNotification`, resulting in nested `BindingNotifications`.

This adds support for nested binding notifications back in - even though IMO nesting binding notifications is a bug, if we're doing it and we previously supported it then we should continue to support it.

Fixes #15201
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment