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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
f98b3be
Update ncrunch config.
grokys Sep 22, 2023
ac23404
WIP: Benchmarks
grokys Sep 22, 2023
fb7f8a6
Initial refactor of binding infrastructure.
grokys Sep 10, 2023
a7c1440
Make default binding Source = UnsetProperty.
grokys Sep 15, 2023
036048c
Move logging to BindingExpression.
grokys Sep 21, 2023
523a2bc
Add compatibility hack for older compiled bindings.
grokys Oct 17, 2023
6bb9f92
Log errors from property accessors.
grokys Oct 18, 2023
2f0e075
Don't log errors for named control bindings...
grokys Oct 20, 2023
00a619f
Log errors for failed conversions.
grokys Oct 20, 2023
3e2d0a5
Use consistent wording for binding warnings.
grokys Oct 20, 2023
c4b6520
Log warnings for converter exceptions.
grokys Oct 20, 2023
44a6340
Don't convert new TargetTypeConverters each time.
grokys Oct 21, 2023
15dc9af
Added failing test for implicit conversion.
grokys Oct 21, 2023
5cdd8b1
Support cast operators in compiled bindings.
grokys Oct 22, 2023
b82d7b4
This shouldn't be a public API.
grokys Nov 2, 2023
6ea751f
Make enum/int conversion work.
grokys Nov 2, 2023
3308403
Check for SetValue equality after conversion.
grokys Nov 2, 2023
4899ff6
Added ConverterCulture back to bindings.
grokys Nov 3, 2023
02426b3
Merge branch 'master' into refactor/bindings
grokys Nov 3, 2023
55a4526
Fix merge error.
grokys Nov 8, 2023
15ab324
Use BindingExpression directly in ValueStoe.
grokys Nov 3, 2023
dbfde2d
Introduce BindingExpressionBase.
grokys Nov 9, 2023
6571c7c
Make TemplateBinding a BindingExpression.
grokys Nov 9, 2023
629160a
Make DynamicResource use a BindingExpression.
grokys Nov 11, 2023
9d793c1
WIP: Start exposing a BindingExpression API.
grokys Nov 24, 2023
08c8497
Finish exposing a BindingExpression API.
grokys Nov 24, 2023
6e31e8e
Fix OneTimeBinding.
grokys Nov 24, 2023
d6fc56f
Remove unneeded classes/methods.
grokys Nov 24, 2023
5ea703e
Don't call obsolete API.
grokys Nov 24, 2023
18c844e
Make BindingExpressionBase the public API.
grokys Nov 24, 2023
58db340
Added BindingExpressionBase.UpdateTarget.
grokys Nov 24, 2023
bee98ad
Initial implementation of UpdateSourceTrigger.
grokys Nov 24, 2023
9025e1d
Don't use weak references for values.
grokys Nov 25, 2023
b75f848
No need for virtual/generic methods here now.
grokys Nov 26, 2023
f982f19
Reintroduce support for binding anchors.
grokys Nov 26, 2023
5a23669
Include new property in clone.
grokys Nov 26, 2023
de52fca
Merge branch 'master' into refactor/bindings
grokys Nov 27, 2023
0cb33db
Merge branch 'master' into refactor/bindings
grokys Dec 7, 2023
5a40a9d
Fix merge error.
grokys Dec 7, 2023
7b1cb09
Updated BindingExpression tests.
grokys Dec 10, 2023
17f4ee8
Fix compiled binding indexer tests.
grokys Dec 11, 2023
9dc84da
Use data validation plugins in PropertyAccessorNode.
grokys Dec 11, 2023
dbcd899
Don't separate plugins by reflection.
grokys Dec 13, 2023
770e4e8
Remove unneeded methods.
grokys Dec 14, 2023
28ad2da
Make reflection binding tests use a string.
grokys Dec 14, 2023
99d3e7a
Merge branch 'master' into refactor/bindings
grokys Dec 15, 2023
8ef5092
Added TODO12 plan for IBinding2.
grokys Jan 2, 2024
4797d92
Use more specific exception.
grokys Jan 2, 2024
82c2245
Fix nits from code review.
grokys Jan 2, 2024
b52d52a
Make expression nodes sealed where possible.
grokys Jan 2, 2024
9751a33
Merge branch 'master' into refactor/bindings
grokys Jan 2, 2024
b6cb62e
Unsubscribe on Stop, don't re-subscribe.
grokys Jan 4, 2024
731b414
Tweak ExpressionNode lists.
grokys Jan 4, 2024
d98c1f5
Add a pooled option in BindingExpressionGrammar.
grokys Jan 5, 2024
073682e
Avoid allocations when enumerating binding plugins.
grokys Jan 5, 2024
92638a9
Add IBinding2 support to observable bind overloads.
grokys Jan 5, 2024
d7298e5
Remove disposed binding from ImmediateBindingFrame.
grokys Jan 5, 2024
e7d236e
Added TemplateBinding benchmarks.
grokys Jan 5, 2024
b691626
Remove duplicate items.
grokys Jan 12, 2024
1427090
Fix exception when closing color picker.
grokys Jan 12, 2024
dd63c8b
Don't skip converter when binding to self.
grokys Jan 15, 2024
f6d848c
Don't pass UnsetValue to converters.
grokys Jan 15, 2024
cf12949
Log element name if present.
grokys Jan 16, 2024
a72765d
Respect binding priority.
grokys Jan 16, 2024
090054b
Throw on mismatched binding priorities.
grokys Jan 22, 2024
6779b35
Convert to target type in TemplateBinding.
grokys Jan 22, 2024
a1955da
Short-circuit target type conversion for same types.
grokys Jan 22, 2024
e583a07
Merge branch 'master' into refactor/bindings
grokys Jan 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .ncrunch/Avalonia.Tizen.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions .ncrunch/ControlCatalog.Tizen.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions .ncrunch/ControlCatalog.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions .ncrunch/PInvoke.net6.0.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions .ncrunch/PInvoke.netstandard2.0.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
1 change: 1 addition & 0 deletions samples/BindingDemo/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox Watermark="Two Way (LostFocus)" UseFloatingWatermark="True" Text="{Binding Path=StringValue, UpdateSourceTrigger=LostFocus}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<!-- Removed due to #2983: reinstate when that's fixed.
Expand Down
4 changes: 2 additions & 2 deletions src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)

if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding, targetControl);
return Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
return Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
}
}

Expand Down
84 changes: 64 additions & 20 deletions src/Avalonia.Base/AvaloniaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;

namespace Avalonia
Expand Down Expand Up @@ -406,6 +408,19 @@ public void SetCurrentValue<T>(StyledProperty<T> property, T value)
}
}

/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an <see cref="IBinding"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="binding">The binding.</param>
/// <returns>
/// The binding expression which represents the binding instance on this object.
/// </returns>
public BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding)
{
return Bind(property, binding, null);
}

/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
Expand Down Expand Up @@ -440,7 +455,21 @@ public IDisposable Bind<T>(
VerifyAccess();
ValidatePriority(priority);

return _values.AddBinding(property, source, priority);
if (source is IBinding2 b)
{
if (b.Instance(this, property, null) is not UntypedBindingExpressionBase expression)
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
if (priority != expression.Priority)
throw new NotSupportedException(
$"The binding priority passed to AvaloniaObject.Bind ('{priority}') " +
"conflicts with the binding priority of the provided binding expression " +
$" ({expression.Priority}').");
return GetValueStore().AddBinding(property, expression);
}
else
{
return _values.AddBinding(property, source, priority);
}
}

/// <summary>
Expand Down Expand Up @@ -512,7 +541,16 @@ public IDisposable Bind<T>(
throw new ArgumentException($"The property {property.Name} is readonly.");
}

return _values.AddBinding(property, source);
if (source is IBinding2 b)
{
if (b.Instance(this, property, null) is not UntypedBindingExpressionBase expression)
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
return GetValueStore().AddBinding(property, expression);
}
else
{
return _values.AddBinding(property, source);
}
}

/// <summary>
Expand Down Expand Up @@ -573,6 +611,30 @@ public IDisposable Bind<T>(
/// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);

/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an <see cref="IBinding"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="binding">The binding.</param>
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provide this context.
/// </param>
/// <returns>
/// The binding expression which represents the binding instance on this object.
/// </returns>
internal BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding, object? anchor)
{
if (binding is not IBinding2 b)
throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'.");
if (b.Instance(this, property, anchor) is not UntypedBindingExpressionBase expression)
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");

return GetValueStore().AddBinding(property, expression);
}

internal void AddInheritanceChild(AvaloniaObject child)
{
_inheritanceChildren ??= new List<AvaloniaObject>();
Expand Down Expand Up @@ -608,22 +670,6 @@ internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property)
internal ValueStore GetValueStore() => _values;
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;

/// <summary>
/// Gets a logger to which a binding warning may be written.
/// </summary>
/// <param name="property">The property that the error occurred on.</param>
/// <param name="e">The binding exception, if any.</param>
/// <remarks>
/// This is overridden in <see cref="Visual"/> to prevent logging binding errors when a
/// control is not attached to the visual tree.
/// </remarks>
internal virtual ParametrizedLogger? GetBindingWarningLogger(
AvaloniaProperty property,
Exception? e)
{
return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding);
}

/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
Expand Down Expand Up @@ -757,8 +803,6 @@ internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, T value
/// <param name="value">The value.</param>
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
{
LoggingUtils.LogIfNecessary(this, property, value);

switch (value.Type)
{
case BindingValueType.UnsetValue:
Expand Down
29 changes: 12 additions & 17 deletions src/Avalonia.Base/AvaloniaObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Reactive;

namespace Avalonia
Expand Down Expand Up @@ -233,9 +234,10 @@ public static IDisposable Bind<T>(
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provice this context.
/// can be used to provide this context.
/// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete("Use AvaloniaObject.Bind(AvaloniaProperty, IBinding")]
public static IDisposable Bind(
this AvaloniaObject target,
AvaloniaProperty property,
Expand All @@ -246,20 +248,7 @@ public static IDisposable Bind(
property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding));

var result = binding.Initiate(
target,
property,
anchor,
property.GetMetadata(target.GetType()).EnableDataValidation ?? false);

if (result != null)
{
return BindingOperations.Apply(target, property, result, anchor);
}
else
{
return Disposable.Empty;
}
return target.Bind(property, binding);
}

/// <summary>
Expand Down Expand Up @@ -367,7 +356,7 @@ public static IDisposable AddClassHandler<TTarget, TValue>(
return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action));
}

private class BindingAdaptor : IBinding
private class BindingAdaptor : IBinding2
{
private readonly IObservable<object?> _source;

Expand All @@ -382,7 +371,13 @@ public BindingAdaptor(IObservable<object?> source)
object? anchor = null,
bool enableDataValidation = false)
{
return InstancedBinding.OneWay(_source);
var expression = new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue);
}

BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{
return new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/Avalonia.Base/AvaloniaProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,14 @@ public override string ToString()
/// <param name="value">The value.</param>
internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value);

/// <summary>
/// Routes an untyped SetDirectValueUnchecked call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
/// <param name="value">The value.</param>
internal virtual void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value) =>
throw new NotSupportedException();

/// <summary>
/// Routes an untyped Bind call to a typed call.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/ClassBindingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static IDisposable Bind(StyledElement target, string className, IBinding
{
if (!s_RegisteredProperties.TryGetValue(className, out var prop))
s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
return target.Bind(prop, source, anchor);
return target.Bind(prop, source);
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice",
Expand Down
12 changes: 6 additions & 6 deletions src/Avalonia.Base/Data/BindingChainException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ namespace Avalonia.Data
{
/// <summary>
/// An exception returned through <see cref="BindingNotification"/> signaling that a
/// requested binding expression could not be evaluated because of a null in one of the links
/// of the binding chain.
/// requested binding expression could not be evaluated because of an error in one of
/// the links of the binding chain.
/// </summary>
public class BindingChainException : Exception
{
Expand Down Expand Up @@ -60,15 +60,15 @@ public override string Message
{
if (Expression != null && ExpressionErrorPoint != null)
{
return $"{_message} in expression '{Expression}' at '{ExpressionErrorPoint}'.";
return $"An error occured binding to '{Expression}' at '{ExpressionErrorPoint}': '{_message}'";
}
else if (ExpressionErrorPoint != null)
else if (Expression != null)
{
return $"{_message} in expression '{ExpressionErrorPoint}'.";
return $"An error occured binding to '{Expression}': '{_message}'";
}
else
{
return $"{_message} in expression.";
return $"An error occured in a binding: '{_message}'";
}
}
}
Expand Down
55 changes: 55 additions & 0 deletions src/Avalonia.Base/Data/BindingExpressionBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using Avalonia.PropertyStore;
using Avalonia.Styling;

namespace Avalonia.Data;

public abstract class BindingExpressionBase : IDisposable, ISetterInstance
{
private protected BindingExpressionBase()
{
}

internal BindingMode Mode { get; private protected set; }

public virtual void Dispose()
{
GC.SuppressFinalize(this);
}

/// <summary>
/// Sends the current binding target value to the binding source property in
/// <see cref="BindingMode.TwoWay"/> or <see cref="BindingMode.OneWayToSource"/> bindings.
/// </summary>
/// <remarks>
/// This method does nothing when the Mode of the binding is not
/// <see cref="BindingMode.TwoWay"/> or <see cref="BindingMode.OneWayToSource"/>.
///
/// If the UpdateSourceTrigger value of your binding is set to
/// <see cref="UpdateSourceTrigger.Explicit"/>, you must call the
/// <see cref="UpdateSource"/> method or the changes will not propagate back to the
/// source.
/// </remarks>
public virtual void UpdateSource() { }

/// <summary>
/// Forces a data transfer from the binding source to the binding target.
/// </summary>
public virtual void UpdateTarget() { }

/// <summary>
/// When overridden in a derived class, attaches the binding expression to a value store but
/// does not start it.
/// </summary>
/// <param name="valueStore">The value store to attach to.</param>
/// <param name="frame">The immediate value frame to attach to, if any.</param>
/// <param name="target">The target object.</param>
/// <param name="targetProperty">The target property.</param>
/// <param name="priority">The priority of the binding.</param>
internal abstract void Attach(
ValueStore valueStore,
ImmediateValueFrame? frame,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority);
}
Loading