Browse Source

Binding System refactor (#13970)

* Update ncrunch config.

* WIP: Benchmarks

* Initial refactor of binding infrastructure.

- `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.

* Make default binding Source = UnsetProperty.

Null is a theoretically valid value for `Source`; setting it to null shouldn't mean "use the data context".

* Move logging to BindingExpression.

As `BindingExpression` now has enough information to decide when it's appropriate to log an error/warning or not.

Fixes #5762
Fixes #9422

* Add compatibility hack for older compiled bindings.

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`.

* Log errors from property accessors.

* Don't log errors for named control bindings...

...on elements which aren't yet rooted.

* Log errors for failed conversions.

* Use consistent wording for binding warnings.

"Could not convert" instead of "Cannot convert".

* Log warnings for converter exceptions.

* Don't convert new TargetTypeConverters each time.

* Added failing test for implicit conversion.

* Support cast operators in compiled bindings.

A bit of a hack as we'd ideally not be using reflection when using compiled bindings.

* This shouldn't be a public API.

Should only be used for tests.

* Make enum/int conversion work.

* Check for SetValue equality after conversion.

And also use "identity equals" where value types and strings use `object.Equals` and reference types use `object.ReferenceEquals`.

* Added ConverterCulture back to bindings.

* Fix merge error.

Removed deleted files from csproj that were re-added due to indentation changes.

* Use BindingExpression directly in ValueStoe.

* Introduce BindingExpressionBase.

And `UntypedBindingExpressionBase`.

* Make TemplateBinding a BindingExpression.

* Make DynamicResource use a BindingExpression.

* WIP: Start exposing a BindingExpression API.

* Finish exposing a BindingExpression API.

* Fix OneTimeBinding.

* Remove unneeded classes/methods.

* Don't call obsolete API.

* Make BindingExpressionBase the public API.

This matches WPF's API.

* Added BindingExpressionBase.UpdateTarget.

* Initial implementation of UpdateSourceTrigger.

* Don't use weak references for values.

If they're boxed values, they can get collected.

* No need for virtual/generic methods here now.

* Reintroduce support for binding anchors.

Turns out these were needed by animations, just our animation system has no unit tests so I missed that fact earlier. Add a basic animation unit test that fails without anchor support, and add binding anchors back in. Currently a private API as I suspect this feature shouldn't be needed outside the framework.

* Include new property in clone.

And add real-life example of `UpdateSourceTrigger=LostFocus` to BindingDemo.

* Fix merge error.

* Updated BindingExpression tests.

- Make them run for both compiled and reflection bindings (found a bunch of tests that fail with compiled bindings)
- Make them not depend on converting the `BindingExpression` to an observable and instead test the end result of the binding on an `AvaloniaObject`

* Fix compiled binding indexer tests.

* Use data validation plugins in PropertyAccessorNode.

Added a warning suppression for now: we may need a separate `DataValidators` list for AOT-friendly plugins.

* Don't separate plugins by reflection.

`DataAnnotationsValidationPlugin` is public and so it can't be moved. No point in moving the others if this one will be in the wrong place.

* Remove unneeded methods.

* Make reflection binding tests use a string.

Convert the `System.Linq.Expression` to a string and then use this, as reflection bindings will always be instanced with a string path.

* Added TODO12 plan for IBinding2.

* Use more specific exception.

* Fix nits from code review.

* Make expression nodes sealed where possible.

* Unsubscribe on Stop, don't re-subscribe.

D'oh.

* Tweak ExpressionNode lists.

Saves a few K in benchmarks and it's a cleaner API.

* Add a pooled option in BindingExpressionGrammar.

Micro-optimization.

* Avoid allocations when enumerating binding plugins.

* Add IBinding2 support to observable bind overloads.

In the case of `TemplateBinding`, the `IObservable<object?>` bind overload is selected by C#. Add an explicit check for an `IBinding2` here to use the more performant code-path.

* Remove disposed binding from ImmediateBindingFrame.

* Added TemplateBinding benchmarks.

* Remove duplicate items.

Seems to have been caused by a merge error.

* Fix exception when closing color picker.

And add tests.

* Don't skip converter when binding to self.

* Don't pass UnsetValue to converters.

This follows WPF behavior.

* Log element name if present.

More useful than just logging the control hash code.

* Respect binding priority.

* Throw on mismatched binding priorities.

We don't want to respect the binding priority in this case as it breaks `TemplateBindings` when the default `LocalValue` priority is passed. Instead make sure that the priority parameter matches that of the expression.

This reverts commit a72765d705.

* Convert to target type in TemplateBinding.

* Short-circuit target type conversion for same types.
pull/14399/head
Steven Kirk 2 years ago
committed by GitHub
parent
commit
3b1eb338e6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      .ncrunch/Avalonia.Tizen.v3.ncrunchproject
  2. 5
      .ncrunch/ControlCatalog.Tizen.v3.ncrunchproject
  3. 5
      .ncrunch/ControlCatalog.v3.ncrunchproject
  4. 5
      .ncrunch/PInvoke.net6.0.v3.ncrunchproject
  5. 5
      .ncrunch/PInvoke.netstandard2.0.v3.ncrunchproject
  6. 1
      samples/BindingDemo/MainWindow.xaml
  7. 4
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  8. 80
      src/Avalonia.Base/AvaloniaObject.cs
  9. 29
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  10. 8
      src/Avalonia.Base/AvaloniaProperty.cs
  11. 2
      src/Avalonia.Base/ClassBindingManager.cs
  12. 12
      src/Avalonia.Base/Data/BindingChainException.cs
  13. 55
      src/Avalonia.Base/Data/BindingExpressionBase.cs
  14. 41
      src/Avalonia.Base/Data/BindingNotification.cs
  15. 34
      src/Avalonia.Base/Data/BindingOperations.cs
  16. 39
      src/Avalonia.Base/Data/BindingValue.cs
  17. 57
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  18. 18
      src/Avalonia.Base/Data/Core/BindingError.cs
  19. 688
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  20. 7
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  21. 168
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  22. 54
      src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs
  23. 58
      src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs
  24. 81
      src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs
  25. 31
      src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs
  26. 19
      src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNodeBase.cs
  27. 253
      src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs
  28. 28
      src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs
  29. 13
      src/Avalonia.Base/Data/Core/ExpressionNodes/IPropertyAccessorNode.cs
  30. 23
      src/Avalonia.Base/Data/Core/ExpressionNodes/ISettableNode.cs
  31. 85
      src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs
  32. 69
      src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs
  33. 107
      src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs
  34. 46
      src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs
  35. 67
      src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs
  36. 85
      src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs
  37. 95
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs
  38. 53
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs
  39. 81
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs
  40. 159
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs
  41. 29
      src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs
  42. 26
      src/Avalonia.Base/Data/Core/ExpressionNodes/SourceNode.cs
  43. 43
      src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs
  44. 49
      src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs
  45. 86
      src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs
  46. 316
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  47. 20
      src/Avalonia.Base/Data/Core/IBinding2.cs
  48. 34
      src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs
  49. 7
      src/Avalonia.Base/Data/Core/ITransformNode.cs
  50. 68
      src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs
  51. 76
      src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs
  52. 102
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  53. 91
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  54. 43
      src/Avalonia.Base/Data/Core/MarkupBindingChainException.cs
  55. 232
      src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
  56. 26
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  57. 219
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  58. 4
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  59. 28
      src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs
  60. 3
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  61. 2
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  62. 5
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  63. 6
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs
  64. 3
      src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs
  65. 3
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  66. 15
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  67. 107
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  68. 5
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  69. 27
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin`1.cs
  70. 27
      src/Avalonia.Base/Data/Core/Plugins/PropertyInfoAccessorPlugin.cs
  71. 141
      src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs
  72. 3
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  73. 49
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin`1.cs
  74. 103
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  75. 53
      src/Avalonia.Base/Data/Core/SettableNode.cs
  76. 56
      src/Avalonia.Base/Data/Core/StreamNode.cs
  77. 127
      src/Avalonia.Base/Data/Core/TargetTypeConverter.cs
  78. 34
      src/Avalonia.Base/Data/Core/TypeCastNode.cs
  79. 600
      src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs
  80. 34
      src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs
  81. 0
      src/Avalonia.Base/Data/CultureInfoIetfLanguageTagConverter.cs
  82. 3
      src/Avalonia.Base/Data/IBinding.cs
  83. 18
      src/Avalonia.Base/Data/IndexerBinding.cs
  84. 33
      src/Avalonia.Base/Data/InstancedBinding.cs
  85. 21
      src/Avalonia.Base/Data/TemplateBinding.Observable.cs
  86. 260
      src/Avalonia.Base/Data/TemplateBinding.cs
  87. 29
      src/Avalonia.Base/Data/UpdateSourceTrigger.cs
  88. 12
      src/Avalonia.Base/Diagnostics/ObsoletionMessages.cs
  89. 7
      src/Avalonia.Base/DirectProperty.cs
  90. 7
      src/Avalonia.Base/DirectPropertyBase.cs
  91. 6
      src/Avalonia.Base/IDirectPropertyAccessor.cs
  92. 29
      src/Avalonia.Base/Logging/TraceLogSink.cs
  93. 2
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  94. 2
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  95. 12
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  96. 18
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  97. 7
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  98. 2
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  99. 82
      src/Avalonia.Base/PropertyStore/LoggingUtils.cs
  100. 39
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

5
.ncrunch/Avalonia.Tizen.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/ControlCatalog.Tizen.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/ControlCatalog.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/PInvoke.net6.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/PInvoke.netstandard2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

1
samples/BindingDemo/MainWindow.xaml

@ -23,6 +23,7 @@
<StackPanel Margin="18" Spacing="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/> <TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/> <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 Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/> <TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<!-- Removed due to #2983: reinstate when that's fixed. <!-- Removed due to #2983: reinstate when that's fixed.

4
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -47,11 +47,11 @@ namespace Avalonia.Animation
if (value is IBinding binding) if (value is IBinding binding)
{ {
return this.Bind(ValueProperty, binding, targetControl); return Bind(ValueProperty, binding, targetControl);
} }
else else
{ {
return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl); return Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
} }
} }

80
src/Avalonia.Base/AvaloniaObject.cs

@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia namespace Avalonia
@ -406,6 +408,19 @@ namespace Avalonia
} }
} }
/// <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> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
@ -440,8 +455,22 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
ValidatePriority(priority); ValidatePriority(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); return _values.AddBinding(property, source, priority);
} }
}
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
@ -512,8 +541,17 @@ namespace Avalonia
throw new ArgumentException($"The property {property.Name} is readonly."); throw new ArgumentException($"The property {property.Name} is readonly.");
} }
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); return _values.AddBinding(property, source);
} }
}
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
@ -573,6 +611,30 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); 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) internal void AddInheritanceChild(AvaloniaObject child)
{ {
_inheritanceChildren ??= new List<AvaloniaObject>(); _inheritanceChildren ??= new List<AvaloniaObject>();
@ -608,22 +670,6 @@ namespace Avalonia
internal ValueStore GetValueStore() => _values; internal ValueStore GetValueStore() => _values;
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren; 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> /// <summary>
/// Called to update the validation state for properties for which data validation is /// Called to update the validation state for properties for which data validation is
/// enabled. /// enabled.
@ -757,8 +803,6 @@ namespace Avalonia
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value) internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
{ {
LoggingUtils.LogIfNecessary(this, property, value);
switch (value.Type) switch (value.Type)
{ {
case BindingValueType.UnsetValue: case BindingValueType.UnsetValue:

29
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Reactive; using Avalonia.Reactive;
namespace Avalonia namespace Avalonia
@ -233,9 +234,10 @@ namespace Avalonia
/// An optional anchor from which to locate required context. When binding to objects that /// 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 /// 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 /// 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> /// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns> /// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete("Use AvaloniaObject.Bind(AvaloniaProperty, IBinding")]
public static IDisposable Bind( public static IDisposable Bind(
this AvaloniaObject target, this AvaloniaObject target,
AvaloniaProperty property, AvaloniaProperty property,
@ -246,20 +248,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding)); binding = binding ?? throw new ArgumentNullException(nameof(binding));
var result = binding.Initiate( return target.Bind(property, binding);
target,
property,
anchor,
property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
if (result != null)
{
return BindingOperations.Apply(target, property, result, anchor);
}
else
{
return Disposable.Empty;
}
} }
/// <summary> /// <summary>
@ -367,7 +356,7 @@ namespace Avalonia
return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action)); return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action));
} }
private class BindingAdaptor : IBinding private class BindingAdaptor : IBinding2
{ {
private readonly IObservable<object?> _source; private readonly IObservable<object?> _source;
@ -382,7 +371,13 @@ namespace Avalonia
object? anchor = null, object? anchor = null,
bool enableDataValidation = false) 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);
} }
} }

8
src/Avalonia.Base/AvaloniaProperty.cs

@ -542,6 +542,14 @@ namespace Avalonia
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); 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> /// <summary>
/// Routes an untyped Bind call to a typed call. /// Routes an untyped Bind call to a typed call.
/// </summary> /// </summary>

2
src/Avalonia.Base/ClassBindingManager.cs

@ -14,7 +14,7 @@ namespace Avalonia
{ {
if (!s_RegisteredProperties.TryGetValue(className, out var prop)) if (!s_RegisteredProperties.TryGetValue(className, out var prop))
s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className); 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", [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice",

12
src/Avalonia.Base/Data/BindingChainException.cs

@ -4,8 +4,8 @@ namespace Avalonia.Data
{ {
/// <summary> /// <summary>
/// An exception returned through <see cref="BindingNotification"/> signaling that a /// 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 /// requested binding expression could not be evaluated because of an error in one of
/// of the binding chain. /// the links of the binding chain.
/// </summary> /// </summary>
public class BindingChainException : Exception public class BindingChainException : Exception
{ {
@ -60,15 +60,15 @@ namespace Avalonia.Data
{ {
if (Expression != null && ExpressionErrorPoint != null) 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 else
{ {
return $"{_message} in expression."; return $"An error occured in a binding: '{_message}'";
} }
} }
} }

55
src/Avalonia.Base/Data/BindingExpressionBase.cs

@ -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);
}

41
src/Avalonia.Base/Data/BindingNotification.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Data namespace Avalonia.Data
{ {
@ -55,6 +57,7 @@ namespace Avalonia.Data
/// <param name="value">The binding value.</param> /// <param name="value">The binding value.</param>
public BindingNotification(object? value) public BindingNotification(object? value)
{ {
Debug.Assert(value is not BindingNotification);
_value = value; _value = value;
} }
@ -162,6 +165,31 @@ namespace Avalonia.Data
return notification is not null ? notification.Value : o; return notification is not null ? notification.Value : o;
} }
/// <summary>
/// Updates the value of an object that may be a <see cref="BindingNotification"/>.
/// </summary>
/// <param name="o">The object that may be a binding notification.</param>
/// <param name="value">The new value.</param>
/// <returns>
/// The updated binding notification if <paramref name="o"/> is a binding notification;
/// otherwise <paramref name="value"/>.
/// </returns>
/// <remarks>
/// If <paramref name="o"/> is a <see cref="BindingNotification"/> then sets its value
/// to <paramref name="value"/>. If <paramref name="value"/> is a
/// <see cref="BindingNotification"/> then the value will first be extracted.
/// </remarks>
public static object? UpdateValue(object o, object value)
{
if (o is BindingNotification n)
{
n.SetValue(ExtractValue(value));
return n;
}
return value;
}
/// <summary> /// <summary>
/// Gets an exception from an object that may be a <see cref="BindingNotification"/>. /// Gets an exception from an object that may be a <see cref="BindingNotification"/>.
/// </summary> /// </summary>
@ -261,4 +289,17 @@ namespace Avalonia.Data
a?.Message == b?.Message; a?.Message == b?.Message;
} }
} }
internal static class BindingErrorTypeExtensions
{
public static BindingValueType ToBindingValueType(this BindingErrorType type)
{
return type switch
{
BindingErrorType.Error => BindingValueType.BindingError,
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
_ => BindingValueType.Value,
};
}
}
} }

34
src/Avalonia.Base/Data/BindingOperations.cs

@ -1,4 +1,5 @@
using System; using System;
using Avalonia.Diagnostics;
using Avalonia.Reactive; using Avalonia.Reactive;
namespace Avalonia.Data namespace Avalonia.Data
@ -13,23 +14,22 @@ namespace Avalonia.Data
/// <param name="target">The target object.</param> /// <param name="target">The target object.</param>
/// <param name="property">The property to bind.</param> /// <param name="property">The property to bind.</param>
/// <param name="binding">The instanced binding.</param> /// <param name="binding">The instanced 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>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns> /// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public static IDisposable Apply( public static IDisposable Apply(
AvaloniaObject target, AvaloniaObject target,
AvaloniaProperty property, AvaloniaProperty property,
InstancedBinding binding, InstancedBinding binding)
object? anchor)
{ {
_ = target ?? throw new ArgumentNullException(nameof(target)); _ = target ?? throw new ArgumentNullException(nameof(target));
_ = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
_ = binding ?? throw new ArgumentNullException(nameof(binding)); _ = binding ?? throw new ArgumentNullException(nameof(binding));
if (binding.Expression is { } expression)
{
return target.GetValueStore().AddBinding(property, expression);
}
var mode = binding.Mode; var mode = binding.Mode;
if (mode == BindingMode.Default) if (mode == BindingMode.Default)
@ -83,6 +83,24 @@ namespace Avalonia.Data
} }
} }
/// <summary>
/// Applies an <see cref="InstancedBinding"/> a property on an <see cref="AvaloniaObject"/>.
/// </summary>
/// <param name="target">The target object.</param>
/// <param name="property">The property to bind.</param>
/// <param name="binding">The instanced binding.</param>
/// <param name="anchor">Obsolete, unused.</param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete("Use the Apply(AvaloniaObject, AvaloniaProperty, InstancedBinding) overload.")]
public static IDisposable Apply(
AvaloniaObject target,
AvaloniaProperty property,
InstancedBinding binding,
object? anchor)
{
return Apply(target, property, binding);
}
private sealed class TwoWayBindingDisposable : IDisposable private sealed class TwoWayBindingDisposable : IDisposable
{ {
private readonly IDisposable _toTargetSubscription; private readonly IDisposable _toTargetSubscription;

39
src/Avalonia.Base/Data/BindingValue.cs

@ -415,6 +415,45 @@ namespace Avalonia.Data
e); e);
} }
/// <summary>
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
/// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
/// <see cref="BindingNotification"/> without type conversion.
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
internal static BindingValue<T> FromUntypedStrict(object? value)
{
if (value == AvaloniaProperty.UnsetValue)
return Unset;
else if (value == BindingOperations.DoNothing)
return DoNothing;
var type = BindingValueType.Value;
T? v = default;
Exception? error = null;
if (value is BindingNotification n)
{
error = n.Error;
type = n.ErrorType switch
{
BindingErrorType.Error => BindingValueType.BindingError,
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
_ => BindingValueType.Value,
};
if (n.HasValue)
type |= BindingValueType.HasValue;
value = n.Value;
}
if ((type & BindingValueType.HasValue) != 0)
v = (T)value!;
return new BindingValue<T>(type, v, error);
}
[Conditional("DEBUG")] [Conditional("DEBUG")]
private static void ValidateValue(T value) private static void ValidateValue(T value)
{ {

57
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -1,57 +0,0 @@
using System;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
internal class AvaloniaPropertyAccessorNode : SettableNode
{
private IDisposable? _subscription;
private readonly bool _enableValidation;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation)
{
_property = property;
_enableValidation = enableValidation;
}
public override string? Description => PropertyName;
public string? PropertyName { get; }
public override Type PropertyType => _property.PropertyType;
protected override bool SetTargetValueCore(object? value, BindingPriority priority)
{
try
{
if (Target.TryGetTarget(out var target) && target is AvaloniaObject obj)
{
obj.SetValue(_property, value, priority);
return true;
}
return false;
}
catch
{
return false;
}
}
protected override void StartListeningCore(WeakReference<object?> reference)
{
if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj)
{
_subscription = new AvaloniaPropertyObservable<object?,object?>(obj, _property).Subscribe(ValueChanged);
}
else
{
_subscription = null;
}
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
}
}

18
src/Avalonia.Base/Data/Core/BindingError.cs

@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
namespace Avalonia.Data.Core;
internal class BindingError
{
public BindingError(Exception exception, BindingErrorType errorType)
{
Debug.Assert(errorType != BindingErrorType.None);
Exception = exception;
ErrorType = errorType;
}
public Exception Exception { get; }
public BindingErrorType ErrorType { get; }
}

688
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -1,389 +1,565 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Reactive; using System.Linq.Expressions;
using System.Text;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core;
{
/// <summary>
/// Binds to an expression on an object using a type value converter to convert the values
/// that are sent and received.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object? _fallbackValue;
private readonly object? _targetNullValue;
private readonly BindingPriority _priority;
InnerListener? _innerListener;
WeakReference<object>? _value;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
public BindingExpression(ExpressionObserver inner, Type targetType)
: this(inner, targetType, DefaultValueConverter.Instance, CultureInfo.InvariantCulture)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// A binding expression which accepts and produces (possibly boxed) object values.
/// </summary> /// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param> /// <remarks>
/// <param name="targetType">The type to convert the value to.</param> /// A <see cref="BindingExpression"/> represents a untyped binding which has been
/// <param name="converter">The value converter to use.</param> /// instantiated on an object.
/// <param name="converterCulture">The converter culture to use.</param> /// </remarks>
/// <param name="converterParameter"> internal partial class BindingExpression : UntypedBindingExpressionBase, IDescription, IDisposable
/// A parameter to pass to <paramref name="converter"/>. {
/// </param> private static readonly List<ExpressionNode> s_emptyExpressionNodes = new();
/// <param name="priority">The binding priority.</param> private readonly WeakReference<object?>? _source;
public BindingExpression( private readonly BindingMode _mode;
ExpressionObserver inner, private readonly List<ExpressionNode> _nodes;
Type targetType, private readonly TargetTypeConverter? _targetTypeConverter;
IValueConverter converter, private readonly UncommonFields? _uncommon;
CultureInfo converterCulture,
object? converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(
inner,
targetType,
AvaloniaProperty.UnsetValue,
AvaloniaProperty.UnsetValue,
converter,
converterCulture,
converterParameter, priority)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class. /// Initializes a new instance of the <see cref="BindingExpression"/> class.
/// </summary> /// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param> /// <param name="source">The source from which the value will be read.</param>
/// <param name="targetType">The type to convert the value to.</param> /// <param name="nodes">The nodes representing the binding path.</param>
/// <param name="fallbackValue"> /// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value. /// The fallback value. Pass <see cref="AvaloniaProperty.UnsetValue"/> for no fallback.
/// </param>
/// <param name="targetNullValue">
/// The value to use when the binding result is null.
/// </param> /// </param>
/// <param name="converter">The value converter to use.</param> /// <param name="converter">The converter to use.</param>
/// <param name="converterCulture">The converter culture to use.</param> /// <param name="converterCulture">The converter culture to use.</param>
/// <param name="converterParameter"> /// <param name="converterParameter">The converter parameter.</param>
/// A parameter to pass to <paramref name="converter"/>. /// <param name="enableDataValidation">
/// Whether data validation should be enabled for the binding.
/// </param> /// </param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param> /// <param name="priority">The binding priority.</param>
/// <param name="stringFormat">The format string to use.</param>
/// <param name="targetNullValue">The null target value.</param>
/// <param name="targetTypeConverter">
/// A final type converter to be run on the produced value.
/// </param>
/// <param name="updateSourceTrigger">The trigger for updating the source value.</param>
public BindingExpression( public BindingExpression(
ExpressionObserver inner, object? source,
Type targetType, List<ExpressionNode>? nodes,
object? fallbackValue, object? fallbackValue,
object? targetNullValue, IValueConverter? converter = null,
IValueConverter converter, CultureInfo? converterCulture = null,
CultureInfo converterCulture,
object? converterParameter = null, object? converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue) bool enableDataValidation = false,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue,
string? stringFormat = null,
object? targetNullValue = null,
TargetTypeConverter? targetTypeConverter = null,
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
: base(priority, enableDataValidation)
{ {
_ = inner ?? throw new ArgumentNullException(nameof(inner)); if (mode == BindingMode.Default)
_ = targetType ?? throw new ArgumentNullException(nameof(targetType)); throw new ArgumentException("Binding mode cannot be Default.", nameof(mode));
_ = converter ?? throw new ArgumentNullException(nameof(converter)); if (updateSourceTrigger == UpdateSourceTrigger.Default)
throw new ArgumentException("UpdateSourceTrigger cannot be Default.", nameof(updateSourceTrigger));
_inner = inner; if (source == AvaloniaProperty.UnsetValue)
_targetType = targetType; source = null;
Converter = converter;
ConverterCulture = converterCulture;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_targetNullValue = targetNullValue;
_priority = priority;
}
/// <summary> _source = new(source);
/// Gets the converter to use on the expression. _mode = mode;
/// </summary> _nodes = nodes ?? s_emptyExpressionNodes;
public IValueConverter Converter { get; } _targetTypeConverter = targetTypeConverter;
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
public CultureInfo ConverterCulture { get; set; }
/// <summary> if (converter is not null ||
/// Gets a parameter to pass to <see cref="Converter"/>. converterCulture is not null ||
/// </summary> converterParameter is not null ||
public object? ConverterParameter { get; } fallbackValue != AvaloniaProperty.UnsetValue ||
!string.IsNullOrWhiteSpace(stringFormat) ||
(targetNullValue is not null && targetNullValue != AvaloniaProperty.UnsetValue) ||
updateSourceTrigger is not UpdateSourceTrigger.PropertyChanged)
{
_uncommon = new()
{
_converter = converter,
_converterCulture = converterCulture,
_converterParameter = converterParameter,
_fallbackValue = fallbackValue,
_stringFormat = stringFormat switch
{
string s when string.IsNullOrWhiteSpace(s) => null,
string s when !s.Contains('{') => $"{{0:{stringFormat}}}",
_ => stringFormat,
},
_targetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
_updateSourceTrigger = updateSourceTrigger,
};
}
/// <inheritdoc/> IPropertyAccessorNode? leafAccessor = null;
string? IDescription.Description => _inner.Expression;
/// <inheritdoc/> if (nodes is not null)
public void OnCompleted() {
for (var i = 0; i < nodes.Count; ++i)
{ {
var node = nodes[i];
node.SetOwner(this, i);
if (node is IPropertyAccessorNode n)
leafAccessor = n;
}
} }
/// <inheritdoc/> if (enableDataValidation)
public void OnError(Exception error) leafAccessor?.EnableDataValidation();
{
} }
/// <inheritdoc/> public override string Description
public void OnNext(object? value)
{ {
if (value == BindingOperations.DoNothing) get
{ {
return; var b = new StringBuilder();
LeafNode.BuildString(b, _nodes);
return b.ToString();
}
} }
using (_inner.Subscribe(_ => { })) public Type? SourceType => (LeafNode as ISettableNode)?.ValueType;
{ public IValueConverter? Converter => _uncommon?._converter;
var type = _inner.ResultType; public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
public object? ConverterParameter => _uncommon?._converterParameter;
public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue;
public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
public string? StringFormat => _uncommon?._stringFormat;
public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
public UpdateSourceTrigger UpdateSourceTrigger => _uncommon?._updateSourceTrigger ?? UpdateSourceTrigger.PropertyChanged;
if (type != null) public override void UpdateSource()
{ {
var converted = Converter.ConvertBack( if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource)
value, WriteTargetValueToSource();
type, }
ConverterParameter,
ConverterCulture);
if (converted == BindingOperations.DoNothing) public override void UpdateTarget()
{ {
if (_nodes.Count == 0)
return; return;
}
if (converted == AvaloniaProperty.UnsetValue) var source = _nodes[0].Source;
{
converted = TypeUtilities.Default(type); for (var i = 0; i < _nodes.Count; ++i)
_inner.SetValue(converted, _priority); _nodes[i].SetSource(null, null);
_nodes[0].SetSource(source, null);
} }
else if (converted is BindingNotification notification)
{ /// <summary>
if (notification.ErrorType == BindingErrorType.None) /// Creates an <see cref="BindingExpression"/> from an expression tree.
{ /// </summary>
throw new AvaloniaInternalException( /// <typeparam name="TIn">The input type of the binding expression.</typeparam>
"IValueConverter should not return non-errored BindingNotification."); /// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="source">The source from which the binding value will be read.</param>
/// <param name="expression">The expression representing the binding path.</param>
/// <param name="converter">The converter to use.</param>
/// <param name="converterCulture">The converter culture to use.</param>
/// <param name="converterParameter">The converter parameter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled for the binding.</param>
/// <param name="fallbackValue">The fallback value.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="targetNullValue">The null target value.</param>
/// <param name="allowReflection">Whether to allow reflection for target type conversion.</param>
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal static BindingExpression Create<TIn, TOut>(
TIn source,
Expression<Func<TIn, TOut>> expression,
IValueConverter? converter = null,
CultureInfo? converterCulture = null,
object? converterParameter = null,
bool enableDataValidation = false,
Optional<object?> fallbackValue = default,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue,
object? targetNullValue = null,
bool allowReflection = true)
where TIn : class?
{
var nodes = BindingExpressionVisitor<TIn>.BuildNodes(expression, enableDataValidation);
var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
return new BindingExpression(
source,
nodes,
fallback,
converter: converter,
converterCulture: converterCulture,
converterParameter: converterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: priority,
targetNullValue: targetNullValue,
targetTypeConverter: allowReflection ?
TargetTypeConverter.GetReflectionConverter() :
TargetTypeConverter.GetDefaultConverter());
} }
PublishNext(notification); /// <summary>
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when its
/// <see cref="ExpressionNode.Value"/> changes.
/// </summary>
/// <param name="nodeIndex">The <see cref="ExpressionNode.Index"/>.</param>
/// <param name="value">The <see cref="ExpressionNode.Value"/>.</param>
/// <param name="dataValidationError">
/// The data validation error associated with the current value, if any.
/// </param>
internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataValidationError)
{
Debug.Assert(value is not BindingNotification);
Debug.Assert(nodeIndex >= 0 && nodeIndex < _nodes.Count);
if (_fallbackValue != AvaloniaProperty.UnsetValue) if (nodeIndex == _nodes.Count - 1)
{ {
if (TypeUtilities.TryConvert( // The leaf node has changed. If the binding mode is not OneWayToSource, publish the
type, // value to the target.
_fallbackValue, if (_mode != BindingMode.OneWayToSource)
ConverterCulture,
out converted))
{ {
_inner.SetValue(converted, _priority); var error = dataValidationError is not null ?
new BindingError(dataValidationError, BindingErrorType.DataValidationError) :
null;
ConvertAndPublishValue(value, error);
} }
else
{ // If the binding mode is OneTime, then stop the binding if a valid value was published.
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( if (_mode == BindingMode.OneTime && GetValue() != AvaloniaProperty.UnsetValue)
this, Stop();
"Could not convert FallbackValue {FallbackValue} to {Type}",
_fallbackValue,
type);
} }
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
{
// When the binding mode is OneWayToSource, we need to write the value to the source
// when the object holding the source property changes; this is node before the leaf
// node. First update the leaf node's source, then write the value to its property.
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
WriteTargetValueToSource();
} }
else if (value is null)
{
OnNodeError(nodeIndex, "Value is null.");
} }
else else
{ {
_inner.SetValue(converted, _priority); _nodes[nodeIndex + 1].SetSource(value, dataValidationError);
}
} }
} }
}
protected override void Initialize() => _innerListener = new InnerListener(this);
protected override void Deinitialize() => _innerListener?.Dispose();
protected override void Subscribed(IObserver<object> observer, bool first) /// <summary>
{ /// Called by an <see cref="ExpressionNode"/> belonging to this binding when an error occurs
if (!first && _value != null && _value.TryGetTarget(out var val)) /// reading its value.
/// </summary>
/// <param name="nodeIndex">
/// The <see cref="ExpressionNode.Index"/> or -1 if the source is null.
/// </param>
/// <param name="error">The error message.</param>
internal void OnNodeError(int nodeIndex, string error)
{ {
observer.OnNext(val); // Set the source of all nodes after the one that errored to null. This needs to be done
} // for each node individually because setting the source to null will not result in
} // OnNodeValueChanged or OnNodeError being called.
for (var i = nodeIndex + 1; i < _nodes.Count; ++i)
_nodes[i].SetSource(null, null);
/// <inheritdoc/> if (_mode == BindingMode.OneWayToSource)
private object? ConvertValue(object? value) return;
{
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) var errorPoint = CalculateErrorPoint(nodeIndex);
{
return _targetNullValue; if (ShouldLogError(out var target))
Log(target, error, errorPoint);
// Clear the current value and publish the error.
var bindingError = new BindingError(
new BindingChainException(error, Description, errorPoint.ToString()),
BindingErrorType.Error);
ConvertAndPublishValue(AvaloniaProperty.UnsetValue, bindingError);
} }
if (value == BindingOperations.DoNothing) internal void OnDataValidationError(Exception error)
{ {
return value; var bindingError = new BindingError(error, BindingErrorType.DataValidationError);
PublishValue(UnchangedValue, bindingError);
} }
var notification = value as BindingNotification; internal override bool WriteValueToSource(object? value)
if (notification == null)
{ {
var converted = Converter.Convert( if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type)
value, return false;
_targetType,
ConverterParameter,
ConverterCulture);
if (converted == BindingOperations.DoNothing) if (Converter is { } converter &&
value != AvaloniaProperty.UnsetValue &&
value != BindingOperations.DoNothing)
{ {
return converted; value = ConvertBack(converter, ConverterCulture, ConverterParameter, value, type);
} }
notification = converted as BindingNotification; if (value == BindingOperations.DoNothing)
return true;
if (notification?.ErrorType == BindingErrorType.None) // Use the target type converter to convert the value to the target type if necessary.
if (_targetTypeConverter is not null)
{
if (_targetTypeConverter.TryConvert(value, type, ConverterCulture, out var converted))
{ {
converted = notification.Value; value = converted;
} }
else if (FallbackValue != AvaloniaProperty.UnsetValue)
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
{ {
var fallback = ConvertFallback(); value = FallbackValue;
converted = Merge(converted, fallback);
} }
else if (IsDataValidationEnabled)
return converted; {
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var ex = new InvalidCastException(
$"Could not convert '{valueString}' ({valueTypeName}) to {type}.");
OnDataValidationError(ex);
return false;
} }
else else
{ {
return ConvertValue(notification); return false;
} }
} }
private BindingNotification ConvertValue(BindingNotification notification) // Don't set the value if it's unchanged.
{ if (TypeUtilities.IdentityEquals(LeafNode.Value, value, type))
if (notification.HasValue) return true;
try
{ {
var converted = ConvertValue(notification.Value); return setter.WriteValueToSource(value, _nodes);
notification = Merge(notification, converted);
} }
else if (_fallbackValue != AvaloniaProperty.UnsetValue) catch
{ {
var fallback = ConvertFallback(); return false;
notification = Merge(notification, fallback);
} }
return notification;
} }
private BindingNotification ConvertFallback() protected override bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target)
{ {
object? converted; if (!TryGetTarget(out target))
return false;
if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode)
return sourceNode.ShouldLogErrors(target);
return true;
}
if (_fallbackValue == AvaloniaProperty.UnsetValue) protected override void StartCore()
{ {
throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value"); if (_source?.TryGetTarget(out var source) == true)
} {
if (_nodes.Count > 0)
_nodes[0].SetSource(source, null);
else
ConvertAndPublishValue(source, null);
if (TypeUtilities.TryConvert( if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
_targetType, TryGetTarget(out var target) &&
_fallbackValue, TargetProperty is not null)
ConverterCulture,
out converted))
{ {
return new BindingNotification(converted); if (_mode is BindingMode.OneWayToSource)
PublishValue(target.GetValue(TargetProperty));
var trigger = UpdateSourceTrigger;
if (trigger is UpdateSourceTrigger.PropertyChanged)
target.PropertyChanged += OnTargetPropertyChanged;
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
ie.LostFocus += OnTargetLostFocus;
}
} }
else else
{ {
return new BindingNotification( OnNodeError(-1, "Binding Source is null.");
new InvalidCastException(
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
BindingErrorType.Error);
} }
} }
private static BindingNotification Merge(object a, BindingNotification b) protected override void StopCore()
{ {
var an = a as BindingNotification; foreach (var node in _nodes)
node.Reset();
if (an != null) if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
{ TryGetTarget(out var target))
Merge(an, b);
return an;
}
else
{ {
return b; var trigger = UpdateSourceTrigger;
if (trigger is UpdateSourceTrigger.PropertyChanged)
target.PropertyChanged -= OnTargetPropertyChanged;
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
ie.LostFocus -= OnTargetLostFocus;
} }
} }
private static BindingNotification Merge(BindingNotification a, object? b) private string CalculateErrorPoint(int nodeIndex)
{ {
var bn = b as BindingNotification; // Build a string describing the binding chain up to the node that errored.
var result = new StringBuilder();
if (bn != null) if (nodeIndex >= 0)
{ _nodes[nodeIndex].BuildString(result);
Merge(a, bn);
}
else else
{ result.Append("(source)");
a.SetValue(b);
return result.ToString();
} }
return a; private void Log(AvaloniaObject target, string error, string errorPoint, LogEventLevel level = LogEventLevel.Warning)
{
if (!Logger.TryGet(level, LogArea.Binding, out var log))
return;
log.Log(
target,
"An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}",
(object?)TargetProperty ?? "(unknown)",
Description,
errorPoint,
error);
} }
private static BindingNotification Merge(BindingNotification a, BindingNotification b) private void ConvertAndPublishValue(object? value, BindingError? error)
{ {
if (b.HasValue) var isTargetNullValue = false;
// All values other than UnsetValue and DoNothing should be passed to the converter.
if (Converter is { } converter &&
value != AvaloniaProperty.UnsetValue &&
value != BindingOperations.DoNothing)
{ {
a.SetValue(b.Value); value = Convert(converter, ConverterCulture, ConverterParameter, value, TargetType, ref error);
} }
else
// Check this here as the converter may return DoNothing.
if (value == BindingOperations.DoNothing)
return;
// TargetNullValue only applies when the value is null: UnsetValue indicates that there
// was a binding error so we don't want to use TargetNullValue in that case.
if (value is null && TargetNullValue != AvaloniaProperty.UnsetValue)
{ {
a.ClearValue(); value = ConvertFallback(TargetNullValue, nameof(TargetNullValue));
isTargetNullValue = true;
} }
if (b.Error != null) // If we have a value, try to convert it to the target type.
if (value != AvaloniaProperty.UnsetValue)
{
if (StringFormat is { } stringFormat &&
(TargetType == typeof(object) || TargetType == typeof(string)) &&
!isTargetNullValue)
{
// The string format applies if we're targeting a type that can accept a string
// and the value isn't the TargetNullValue.
value = string.Format(ConverterCulture, stringFormat, value);
}
else if (_targetTypeConverter is not null)
{ {
a.AddError(b.Error, b.ErrorType); // Otherwise, if we have a target type converter, convert the value to the target type.
value = ConvertFrom(_targetTypeConverter, value, ref error);
}
} }
return a; // FallbackValue applies if the result from the binding, converter or target type converter
// is UnsetValue.
if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue)
value = ConvertFallback(FallbackValue, nameof(FallbackValue));
// Publish the value.
PublishValue(value, error);
} }
public class InnerListener : IObserver<object?>, IDisposable private void WriteTargetValueToSource()
{ {
private readonly BindingExpression _owner; Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
private readonly IDisposable _dispose;
public InnerListener(BindingExpression owner) if (TryGetTarget(out var target) &&
TargetProperty is not null &&
target.GetValue(TargetProperty) is var value &&
!TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType))
{ {
_owner = owner; WriteValueToSource(value);
_dispose = owner._inner.Subscribe(this); }
} }
public void Dispose() => _dispose.Dispose(); private void OnTargetLostFocus(object? sender, RoutedEventArgs e)
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object? value)
{ {
if (value == BindingOperations.DoNothing) Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.LostFocus);
{
return;
}
var converted = _owner.ConvertValue(value); WriteTargetValueToSource();
}
if (converted == BindingOperations.DoNothing) private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
return; Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged);
if (e.Property == TargetProperty)
WriteValueToSource(e.NewValue);
} }
_owner._value = converted is not null ? new WeakReference<object>(converted) : null; private object? ConvertFallback(object? fallback, string fallbackName)
_owner.PublishNext(converted); {
if (_targetTypeConverter is null || TargetType == typeof(object) || fallback == AvaloniaProperty.UnsetValue)
return fallback;
if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result))
return result;
if (TryGetTarget(out var target))
Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error);
return AvaloniaProperty.UnsetValue;
} }
private object? ConvertFrom(TargetTypeConverter? converter, object? value, ref BindingError? error)
{
if (converter is null)
return value;
if (converter.TryConvert(value, TargetType, ConverterCulture, out var result))
return result;
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'.";
if (ShouldLogError(out var target))
Log(target, message, LogEventLevel.Warning);
error = new(new InvalidCastException(message), BindingErrorType.Error);
return AvaloniaProperty.UnsetValue;
} }
/// <summary>
/// Uncommonly used fields are separated out to reduce memory usage.
/// </summary>
private class UncommonFields
{
public IValueConverter? _converter;
public object? _converterParameter;
public CultureInfo? _converterCulture;
public object? _fallbackValue;
public string? _stringFormat;
public object? _targetNullValue;
public UpdateSourceTrigger _updateSourceTrigger;
} }
} }

7
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -1,7 +0,0 @@
namespace Avalonia.Data.Core
{
internal class EmptyExpressionNode : ExpressionNode
{
public override string Description => ".";
}
}

168
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -1,168 +0,0 @@
using System;
namespace Avalonia.Data.Core
{
internal abstract class ExpressionNode
{
protected static readonly WeakReference<object?> UnsetReference =
new WeakReference<object?>(AvaloniaProperty.UnsetValue);
protected static readonly WeakReference<object?> NullReference =
new WeakReference<object?>(null);
private WeakReference<object?> _target = UnsetReference;
private Action<object?>? _subscriber;
private bool _listening;
protected WeakReference<object?>? LastValue { get; private set; }
public abstract string? Description { get; }
public ExpressionNode? Next { get; set; }
public WeakReference<object?> Target
{
get { return _target; }
set
{
_ = value ?? throw new ArgumentNullException(nameof(value));
_target.TryGetTarget(out var oldTarget);
value.TryGetTarget(out var newTarget);
if (!ReferenceEquals(oldTarget, newTarget))
{
if (_listening)
{
StopListening();
}
_target = value;
if (_subscriber != null)
{
StartListening();
}
}
}
}
public void Subscribe(Action<object?> subscriber)
{
if (_subscriber != null)
{
throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
}
_subscriber = subscriber;
Next?.Subscribe(NextValueChanged);
StartListening();
}
public void Unsubscribe()
{
Next?.Unsubscribe();
if (_listening)
{
StopListening();
}
LastValue = null;
_subscriber = null;
}
protected virtual void StartListeningCore(WeakReference<object?> reference)
{
reference.TryGetTarget(out var target);
ValueChanged(target);
}
protected virtual void StopListeningCore()
{
}
protected virtual void NextValueChanged(object? value)
{
if (_subscriber is null)
return;
var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
bindingBroken?.AddNode(Description ?? "{empty}");
_subscriber(value);
}
protected void ValueChanged(object? value) => ValueChanged(value, true);
private void ValueChanged(object? value, bool notify)
{
if (_subscriber is { } subscriber)
{
var notification = value as BindingNotification;
var next = Next;
if (notification == null)
{
LastValue = value != null ? new WeakReference<object?>(value) : NullReference;
if (next != null)
{
next.Target = LastValue;
}
else if (notify)
{
subscriber(value);
}
}
else
{
LastValue = notification.Value != null ? new WeakReference<object?>(notification.Value) : NullReference;
if (next != null)
{
next.Target = LastValue;
}
if (next == null || notification.Error != null)
{
subscriber(value);
}
}
}
}
private void StartListening()
{
_target.TryGetTarget(out var target);
if (target == null)
{
ValueChanged(TargetNullNotification());
_listening = false;
}
else if (target != AvaloniaProperty.UnsetValue)
{
_listening = true;
StartListeningCore(_target!);
}
else
{
ValueChanged(AvaloniaProperty.UnsetValue, notify:false);
_listening = false;
}
}
private void StopListening()
{
StopListeningCore();
_listening = false;
}
private static BindingNotification TargetNullNotification()
{
return new BindingNotification(
new MarkupBindingChainException("Null value"),
BindingErrorType.Error,
AvaloniaProperty.UnsetValue);
}
}
}

54
src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which accesses an array with integer
/// indexers.
/// </summary>
internal sealed class ArrayIndexerNode : ExpressionNode, ISettableNode
{
private readonly int[] _indexes;
public ArrayIndexerNode(int[] indexes)
{
_indexes = indexes;
}
public Type? ValueType => Source?.GetType().GetElementType();
public override void BuildString(StringBuilder builder)
{
builder.Append('[');
for (var i = 0; i < _indexes.Length; i++)
{
builder.Append(_indexes[i]);
if (i != _indexes.Length - 1)
builder.Append(',');
}
builder.Append(']');
}
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (Source is Array array)
{
array.SetValue(value, _indexes);
return true;
}
return false;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is Array array)
SetValue(array.GetValue(_indexes));
else
ClearValue();
}
}

58
src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode
{
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _onValueChanged;
public AvaloniaPropertyAccessorNode(AvaloniaProperty property)
{
Property = property;
_onValueChanged = OnValueChanged;
}
public AvaloniaProperty Property { get; }
public Type? ValueType => Property.PropertyType;
override public void BuildString(StringBuilder builder)
{
if (builder.Length > 0 && builder[builder.Length - 1] != '!')
builder.Append('.');
builder.Append(Property.Name);
}
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (Source is AvaloniaObject o)
{
o.SetValue(Property, value);
return true;
}
return false;
}
protected override void OnSourceChanged(object? source, Exception? dataValidationError)
{
if (source is AvaloniaObject newObject)
{
newObject.PropertyChanged += _onValueChanged;
SetValue(newObject.GetValue(Property));
}
}
protected override void Unsubscribe(object oldSource)
{
if (oldSource is AvaloniaObject oldObject)
oldObject.PropertyChanged -= _onValueChanged;
}
private void OnValueChanged(object? source, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Property && source is AvaloniaObject o)
SetValue(o.GetValue(Property));
}
}

81
src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs

@ -0,0 +1,81 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using Avalonia.Utilities;
namespace Avalonia.Data.Core.ExpressionNodes;
internal abstract class CollectionNodeBase : ExpressionNode,
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>,
IWeakEventSubscriber<PropertyChangedEventArgs>
{
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
if (ShouldUpdate(sender, e))
UpdateValueOrSetError(sender);
}
void IWeakEventSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
{
if (ShouldUpdate(sender, e))
UpdateValueOrSetError(sender);
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
Subscribe(source);
UpdateValue(source);
}
protected override void Unsubscribe(object source)
{
if (source is INotifyCollectionChanged incc)
WeakEvents.CollectionChanged.Unsubscribe(incc, this);
if (source is INotifyPropertyChanged inpc)
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
protected abstract bool ShouldUpdate(object? sender, PropertyChangedEventArgs e);
protected abstract int? TryGetFirstArgumentAsInt();
protected abstract void UpdateValue(object? source);
private bool ShouldUpdate(object? sender, NotifyCollectionChangedEventArgs e)
{
if (sender != Source)
return false;
if (sender is IList && TryGetFirstArgumentAsInt() is int index)
{
return e.Action switch
{
NotifyCollectionChangedAction.Add => index >= e.NewStartingIndex,
NotifyCollectionChangedAction.Remove => index >= e.OldStartingIndex,
NotifyCollectionChangedAction.Replace =>
index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems!.Count,
NotifyCollectionChangedAction.Move =>
index >= e.NewStartingIndex && index < e.NewStartingIndex + e.NewItems!.Count ||
index >= e.OldStartingIndex && index < e.OldStartingIndex + e.OldItems!.Count,
_ => true,
};
}
// Implementation defined meaning for the index, so just try to update anyway
return true;
}
private void Subscribe(object? source)
{
if (source is INotifyCollectionChanged incc)
WeakEvents.CollectionChanged.Subscribe(incc, this);
if (source is INotifyPropertyChanged inpc)
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
private void UpdateValueOrSetError(object? source)
{
try { UpdateValue(source); }
catch (Exception e) { SetError(e); }
}
}

31
src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs

@ -0,0 +1,31 @@
using System;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class DataContextNode : DataContextNodeBase
{
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is IDataContextProvider && source is AvaloniaObject ao)
{
ao.PropertyChanged += OnPropertyChanged;
SetValue(ao.GetValue(StyledElement.DataContextProperty));
}
else
{
SetError($"Unable to read DataContext from '{source.GetType()}'.");
}
}
protected override void Unsubscribe(object oldSource)
{
if (oldSource is StyledElement oldElement)
oldElement.PropertyChanged -= OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender == Source && e.Property == StyledElement.DataContextProperty)
SetValue(e.NewValue);
}
}

19
src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNodeBase.cs

@ -0,0 +1,19 @@
using System;
using Avalonia.LogicalTree;
namespace Avalonia.Data.Core.ExpressionNodes;
internal abstract class DataContextNodeBase : SourceNode
{
public override object? SelectSource(object? source, object target, object? anchor)
{
if (source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException(
"DataContextNode is invalid in conjunction with a binding source.");
if (target is IDataContextProvider and AvaloniaObject)
return target;
if (anchor is IDataContextProvider and AvaloniaObject)
return anchor;
throw new InvalidOperationException("Cannot find a DataContext to bind to.");
}
}

253
src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs

@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in the binding path of an <see cref="BindingExpression"/>.
/// </summary>
internal abstract class ExpressionNode
{
private WeakReference<object?>? _source;
private object? _value = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets the index of the node in the binding path.
/// </summary>
public int Index { get; private set; }
/// <summary>
/// Gets the owning <see cref="BindingExpression"/>.
/// </summary>
public BindingExpression? Owner { get; private set; }
/// <summary>
/// Gets the source object from which the node will read its value.
/// </summary>
public object? Source
{
get
{
if (_source?.TryGetTarget(out var source) == true)
return source;
return null;
}
}
/// <summary>
/// Gets the current value of the node.
/// </summary>
public object? Value => _value;
/// <summary>
/// Appends a string representation of the expression node to a string builder.
/// </summary>
/// <param name="builder">The string builder.</param>
public virtual void BuildString(StringBuilder builder) { }
/// <summary>
/// Builds a string representation of a binding expression.
/// </summary>
/// <param name="builder">The string builder.</param>
/// <param name="nodes">The nodes in the binding expression.</param>
public virtual void BuildString(StringBuilder builder, IReadOnlyList<ExpressionNode> nodes)
{
if (Index > 0)
nodes[Index - 1].BuildString(builder, nodes);
BuildString(builder);
}
/// <summary>
/// Resets the node to its uninitialized state when the <see cref="Owner"/> is unsubscribed.
/// </summary>
public void Reset()
{
SetSource(null, null);
_source = null;
_value = AvaloniaProperty.UnsetValue;
}
/// <summary>
/// Sets the owner binding.
/// </summary>
/// <param name="owner">The owner binding.</param>
/// <param name="index">The index of the node in the binding path.</param>
/// <exception cref="InvalidOperationException">
/// The node already has an owner.
/// </exception>
public void SetOwner(BindingExpression owner, int index)
{
if (Owner is not null)
throw new InvalidOperationException($"{this} already has an owner.");
Owner = owner;
Index = index;
}
/// <summary>
/// Sets the <see cref="Source"/> from which the node will read its value and updates
/// the current <see cref="Value"/>, notifying the <see cref="Owner"/> if the value
/// changes.
/// </summary>
/// <param name="source">
/// The new source from which the node will read its value. May be
/// <see cref="AvaloniaProperty.UnsetValue"/> in which case the source will be considered
/// to be null.
/// </param>
/// <param name="dataValidationError">
/// Any data validation error reported by the previous expression node.
/// </param>
public void SetSource(object? source, Exception? dataValidationError)
{
var oldSource = Source;
if (source == AvaloniaProperty.UnsetValue)
source = null;
if (source == oldSource)
return;
if (oldSource is not null)
Unsubscribe(oldSource);
_source = new(source);
if (source is null)
{
// If the source is null then the value is null. We explicitly do not want to call
// OnSourceChanged as we don't want to raise errors for subsequent nodes in the
// binding change.
_value = AvaloniaProperty.UnsetValue;
}
else
{
try { OnSourceChanged(source, dataValidationError); }
catch (Exception e) { SetError(e); }
}
}
/// <summary>
/// Sets the current value to <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
protected void ClearValue() => SetValue(AvaloniaProperty.UnsetValue);
/// <summary>
/// Notifies the <see cref="Owner"/> of a data validation error.
/// </summary>
/// <param name="error">The error.</param>
protected void SetDataValidationError(Exception error)
{
if (error is TargetInvocationException tie)
error = tie.InnerException!;
Owner?.OnDataValidationError(error);
}
/// <summary>
/// Sets the current value to <see cref="AvaloniaProperty.UnsetValue"/> and notifies the
/// <see cref="Owner"/> of the error.
/// </summary>
/// <param name="message">The error message.</param>
protected void SetError(string message)
{
_value = AvaloniaProperty.UnsetValue;
Owner?.OnNodeError(Index, message);
}
/// <summary>
/// Sets the current value to <see cref="AvaloniaProperty.UnsetValue"/> and notifies the
/// <see cref="Owner"/> of the error.
/// </summary>
/// <param name="e">The error.</param>
protected void SetError(Exception e)
{
if (e is TargetInvocationException tie)
e = tie.InnerException!;
if (e is AggregateException ae && ae.InnerExceptions.Count == 1)
e = e.InnerException!;
SetError(e.Message);
}
/// <summary>
/// Sets the current <see cref="Value"/>, notifying the <see cref="Owner"/> if the value
/// has changed.
/// </summary>
/// <param name="valueOrNotification">
/// The new value. May be a <see cref="BindingNotification"/>.
/// </param>
protected void SetValue(object? valueOrNotification)
{
if (valueOrNotification is BindingNotification notification)
{
if (notification.ErrorType == BindingErrorType.Error)
{
SetError(notification.Error!);
}
else if (notification.ErrorType == BindingErrorType.DataValidationError)
{
if (notification.HasValue)
SetValue(notification.Value, notification.Error);
else
SetDataValidationError(notification.Error!);
}
else
{
SetValue(notification.Value, null);
}
}
else
{
SetValue(valueOrNotification, null);
}
}
/// <summary>
/// Sets the current <see cref="Value"/>, notifying the <see cref="Owner"/> if the value
/// has changed.
/// </summary>
/// <param name="value">
/// The new value. May not be a <see cref="BindingNotification"/>.
/// </param>
/// <param name="dataValidationError">
/// The data validation error associated with the new value, if any.
/// </param>
protected void SetValue(object? value, Exception? dataValidationError = null)
{
Debug.Assert(value is not BindingNotification);
if (Owner is null)
return;
// We raise a change notification if:
//
// - This is the initial value (_value is null)
// - There is a data validation error
// - There is no data validation error, but the owner has one
// - The new value is different to the old value
if (_value is null ||
dataValidationError is not null ||
(dataValidationError is null && Owner.ErrorType == BindingErrorType.DataValidationError) ||
!Equals(value, _value))
{
_value = value;
Owner.OnNodeValueChanged(Index, value, dataValidationError);
}
}
/// <summary>
/// When implemented in a derived class, subscribes to the new source, and updates the current
/// <see cref="Value"/>.
/// </summary>
/// <param name="source">The new source.</param>
/// <param name="dataValidationError">
/// Any data validation error reported by the previous expression node.
/// </param>
protected abstract void OnSourceChanged(object source, Exception? dataValidationError);
/// <summary>
/// When implemented in a derived class, unsubscribes from the previous source.
/// </summary>
/// <param name="oldSource">The old source.</param>
protected virtual void Unsubscribe(object oldSource) { }
}

28
src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs

@ -0,0 +1,28 @@
using System;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which uses a function to transform its
/// value.
/// </summary>
internal sealed class FuncTransformNode : ExpressionNode
{
private readonly Func<object?, object?> _transform;
public FuncTransformNode(Func<object?, object?> transform)
{
_transform = transform;
}
public override void BuildString(StringBuilder builder)
{
// We don't have enough information to add anything here.
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
SetValue(_transform(source));
}
}

13
src/Avalonia.Base/Data/Core/ExpressionNodes/IPropertyAccessorNode.cs

@ -0,0 +1,13 @@
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// Indicates that a <see cref="ExpressionNode"/> accesses a property on an object.
/// </summary>
internal interface IPropertyAccessorNode
{
string PropertyName { get; }
IPropertyAccessor? Accessor { get; }
void EnableDataValidation();
}

23
src/Avalonia.Base/Data/Core/ExpressionNodes/ISettableNode.cs

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// An <see cref="ExpressionNode"/> that can write a value to its source.
/// </summary>
internal interface ISettableNode
{
/// <summary>
/// Gets the type of the value accepted by the node, or null if the node is not settable.
/// </summary>
Type? ValueType { get; }
/// <summary>
/// Tries to write the specified value to the source.
/// </summary>
/// <param name="value">The value to write.</param>
/// <param name="nodes">The expression nodes in the binding.</param>
/// <returns>True if the value was written sucessfully; otherwise false.</returns>
bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes);
}

85
src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs

@ -0,0 +1,85 @@
using System;
using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class LogicalAncestorElementNode : SourceNode
{
private readonly Type? _ancestorType;
private readonly int _ancestorLevel;
private IDisposable? _subscription;
public LogicalAncestorElementNode(Type? ancestorType, int ancestorLevel)
{
_ancestorType = ancestorType;
_ancestorLevel = ancestorLevel;
}
public override void BuildString(StringBuilder builder)
{
builder.Append("$parent");
if (_ancestorLevel > 0 || _ancestorType is not null)
{
builder.Append('[');
if (_ancestorType is not null)
{
builder.Append(_ancestorType.Name);
if (_ancestorLevel > 0)
builder.Append(',');
}
if (_ancestorLevel > 0)
builder.Append(_ancestorLevel);
builder.Append(']');
}
}
public override object? SelectSource(object? source, object target, object? anchor)
{
if (source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException(
"LogicalAncestorNode is invalid in conjunction with a binding source.");
if (target is ILogical)
return target;
if (anchor is ILogical)
return anchor;
throw new InvalidOperationException("Cannot find an ILogical to get a logical ancestor.");
}
public override bool ShouldLogErrors(object target)
{
return target is ILogical logical && logical.IsAttachedToLogicalTree;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is ILogical logical)
{
var locator = ControlLocator.Track(logical, _ancestorLevel, _ancestorType);
_subscription = locator.Subscribe(TrackedControlChanged);
}
}
protected override void Unsubscribe(object oldSource)
{
_subscription?.Dispose();
_subscription = null;
}
private void TrackedControlChanged(ILogical? control)
{
if (control is not null)
{
SetValue(control);
}
else
{
SetError("Ancestor not found.");
}
}
}

69
src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class LogicalNotNode : ExpressionNode, ISettableNode
{
public override void BuildString(StringBuilder builder)
{
builder.Append("!");
}
public override void BuildString(StringBuilder builder, IReadOnlyList<ExpressionNode> nodes)
{
builder.Append("!");
if (Index > 0)
nodes[Index - 1].BuildString(builder, nodes);
}
public Type ValueType => typeof(bool);
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (Index > 0 && nodes[Index - 1] is ISettableNode previousNode && TryConvert(value, out var boolValue))
return previousNode.WriteValueToSource(!boolValue, nodes);
return false;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var v = BindingNotification.ExtractValue(source);
if (TryConvert(v, out var value))
{
SetValue(BindingNotification.UpdateValue(source, !value), dataValidationError);
}
else
SetError($"Unable to convert '{source}' to bool.");
}
private static bool TryConvert(object? value, out bool result)
{
if (value is bool b)
{
result = b;
return true;
}
if (value is string s)
{
// Special case string for performance.
if (bool.TryParse(s, out result))
return true;
}
else
{
try
{
result = Convert.ToBoolean(value, CultureInfo.InvariantCulture);
return true;
}
catch { }
}
result = false;
return false;
}
}

107
src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Windows.Input;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which converts methods to an
/// <see cref="ICommand"/>.
/// </summary>
internal sealed class MethodCommandNode : ExpressionNode
{
private readonly string _methodName;
private readonly Action<object, object?> _execute;
private readonly Func<object, object?, bool>? _canExecute;
private readonly ISet<string> _dependsOnProperties;
private Command? _command;
public MethodCommandNode(
string methodName,
Action<object, object?> execute,
Func<object, object?, bool>? canExecute,
ISet<string> dependsOnProperties)
{
_methodName = methodName;
_execute = execute;
_canExecute = canExecute;
_dependsOnProperties = dependsOnProperties;
}
public override void BuildString(StringBuilder builder)
{
if (builder.Length > 0 && builder[builder.Length - 1] != '!')
builder.Append('.');
builder.Append(_methodName);
builder.Append("()");
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is INotifyPropertyChanged newInpc)
newInpc.PropertyChanged += OnPropertyChanged;
_command = new Command(source, _execute, _canExecute);
SetValue(_command);
}
protected override void Unsubscribe(object oldSource)
{
if (oldSource is INotifyPropertyChanged oldInpc)
oldInpc.PropertyChanged -= OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrEmpty(e.PropertyName) || _dependsOnProperties.Contains(e.PropertyName))
{
_command?.RaiseCanExecuteChanged();
}
}
private sealed class Command : ICommand
{
private readonly WeakReference<object?> _target;
private readonly Action<object, object?> _execute;
private readonly Func<object, object?, bool>? _canExecute;
public event EventHandler? CanExecuteChanged;
public Command(object? target, Action<object, object?> execute, Func<object, object?, bool>? canExecute)
{
_target = new(target);
_execute = execute;
_canExecute = canExecute;
}
public void RaiseCanExecuteChanged()
{
Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)
, Threading.DispatcherPriority.Input);
}
public bool CanExecute(object? parameter)
{
if (_target.TryGetTarget(out var target))
{
if (_canExecute == null)
{
return true;
}
return _canExecute(target, parameter);
}
return false;
}
public void Execute(object? parameter)
{
if (_target.TryGetTarget(out var target))
{
_execute(target, parameter);
}
}
}
}

46
src/Avalonia.Base/Data/Core/ExpressionNodes/NamedElementNode.cs

@ -0,0 +1,46 @@
using System;
using System.Text;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class NamedElementNode : SourceNode
{
private readonly WeakReference<INameScope?> _nameScope;
private readonly string _name;
private IDisposable? _subscription;
public NamedElementNode(INameScope? nameScope, string name)
{
_nameScope = new(nameScope);
_name = name;
}
public override void BuildString(StringBuilder builder)
{
builder.Append('#');
builder.Append(_name);
}
public override bool ShouldLogErrors(object target)
{
// We don't log errors when the target element isn't rooted.
return target is not ILogical logical || logical.IsAttachedToLogicalTree;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_nameScope.TryGetTarget(out var scope))
_subscription = NameScopeLocator.Track(scope, _name).Subscribe(SetValue);
else
SetError("NameScope not found.");
}
protected override void Unsubscribe(object oldSource)
{
_subscription?.Dispose();
_subscription = null;
}
}

67
src/Avalonia.Base/Data/Core/ExpressionNodes/ParentDataContextNode.cs

@ -0,0 +1,67 @@
using System;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which selects the value of the visual
/// parent's DataContext.
/// </summary>
internal sealed class ParentDataContextNode : DataContextNodeBase
{
private static readonly AvaloniaObject s_unset = new();
private AvaloniaObject? _parent = s_unset;
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is AvaloniaObject newElement)
newElement.PropertyChanged += OnPropertyChanged;
if (source is Visual v)
SetParent(v.GetValue(Visual.VisualParentProperty));
else
SetParent(null);
}
protected override void Unsubscribe(object oldSource)
{
if (oldSource is AvaloniaObject oldElement)
oldElement.PropertyChanged -= OnPropertyChanged;
}
private void SetParent(AvaloniaObject? parent)
{
if (parent == _parent)
return;
Unsubscribe();
_parent = parent;
if (_parent is IDataContextProvider)
{
_parent.PropertyChanged += OnParentPropertyChanged;
SetValue(_parent.GetValue(StyledElement.DataContextProperty));
}
else
{
SetValue(null);
}
}
private void Unsubscribe()
{
if (_parent is not null)
_parent.PropertyChanged -= OnParentPropertyChanged;
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Visual.VisualParentProperty)
SetParent(e.NewValue as AvaloniaObject);
}
private void OnParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == StyledElement.DataContextProperty)
SetValue(e.NewValue);
}
}

85
src/Avalonia.Base/Data/Core/ExpressionNodes/PropertyAccessorNode.cs

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in the binding path of an <see cref="BindingExpression"/> that reads a property
/// via a predefined <see cref="IPropertyAccessorPlugin"/>.
/// </summary>
internal sealed class PropertyAccessorNode : ExpressionNode, IPropertyAccessorNode, ISettableNode
{
private readonly Action<object?> _onValueChanged;
private readonly IPropertyAccessorPlugin _plugin;
private IPropertyAccessor? _accessor;
private bool _enableDataValidation;
public PropertyAccessorNode(string propertyName, IPropertyAccessorPlugin plugin)
{
_plugin = plugin;
_onValueChanged = OnValueChanged;
PropertyName = propertyName;
}
public IPropertyAccessor? Accessor => _accessor;
public string PropertyName { get; }
public Type? ValueType => _accessor?.PropertyType;
public override void BuildString(StringBuilder builder)
{
if (builder.Length > 0 && builder[builder.Length - 1] != '!')
builder.Append('.');
builder.Append(PropertyName);
}
public void EnableDataValidation() => _enableDataValidation = true;
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (_accessor?.PropertyType is not null)
{
return _accessor.SetValue(value, BindingPriority.LocalValue);
}
return false;
}
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var reference = new WeakReference<object?>(source);
if (_plugin.Start(reference, PropertyName) is { } accessor)
{
if (_enableDataValidation)
{
foreach (var validator in BindingPlugins.s_dataValidators)
{
if (validator.Match(reference, PropertyName))
accessor = validator.Start(reference, PropertyName, accessor);
}
}
_accessor = accessor;
_accessor.Subscribe(_onValueChanged);
}
else
{
ClearValue();
}
}
protected override void Unsubscribe(object oldSource)
{
_accessor?.Dispose();
_accessor = null;
}
private void OnValueChanged(object? newValue)
{
SetValue(newValue);
}
}

95
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
/// <summary>
/// A node in the binding path of an <see cref="BindingExpression"/> that reads a property
/// via an <see cref="IPropertyAccessorPlugin"/> selected at runtime from the registered
/// <see cref="BindingPlugins.PropertyAccessors"/>.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal sealed class DynamicPluginPropertyAccessorNode : ExpressionNode, IPropertyAccessorNode, ISettableNode
{
private readonly Action<object?> _onValueChanged;
private IPropertyAccessor? _accessor;
private bool _enableDataValidation;
public DynamicPluginPropertyAccessorNode(string propertyName)
{
_onValueChanged = OnValueChanged;
PropertyName = propertyName;
}
public IPropertyAccessor? Accessor => _accessor;
public string PropertyName { get; }
public Type? ValueType => _accessor?.PropertyType;
override public void BuildString(StringBuilder builder)
{
if (builder.Length > 0 && builder[builder.Length - 1] != '!')
builder.Append('.');
builder.Append(PropertyName);
}
public void EnableDataValidation() => _enableDataValidation = true;
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
return _accessor?.SetValue(value, BindingPriority.LocalValue) ?? false;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var reference = new WeakReference<object?>(source);
if (GetPlugin(source) is { } plugin &&
plugin.Start(reference, PropertyName) is { } accessor)
{
if (_enableDataValidation)
{
foreach (var validator in BindingPlugins.s_dataValidators)
{
if (validator.Match(reference, PropertyName))
accessor = validator.Start(reference, PropertyName, accessor);
}
}
_accessor = accessor;
_accessor.Subscribe(_onValueChanged);
}
else
{
SetError(
$"Could not find a matching property accessor for '{PropertyName}' on '{source.GetType()}'.");
}
}
protected override void Unsubscribe(object oldSource)
{
_accessor?.Dispose();
_accessor = null;
}
private void OnValueChanged(object? newValue)
{
SetValue(newValue);
}
private IPropertyAccessorPlugin? GetPlugin(object? source)
{
if (source is null)
return null;
foreach (var plugin in BindingPlugins.s_propertyAccessors)
{
if (plugin.Match(source, PropertyName))
return plugin;
}
return null;
}
}

53
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs

@ -0,0 +1,53 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal sealed class DynamicPluginStreamNode : ExpressionNode
{
private IDisposable? _subscription;
override public void BuildString(StringBuilder builder)
{
builder.Append('^');
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
var reference = new WeakReference<object?>(source);
if (GetPlugin(reference) is { } plugin &&
plugin.Start(reference) is { } accessor)
{
_subscription = accessor.Subscribe(SetValue);
}
else
{
SetValue(null);
}
}
protected override void Unsubscribe(object oldSource)
{
_subscription?.Dispose();
_subscription = null;
}
private static IStreamPlugin? GetPlugin(WeakReference<object?> source)
{
if (source is null)
return null;
foreach (var plugin in BindingPlugins.s_streamHandlers)
{
if (plugin.Match(source))
return plugin;
}
return null;
}
}

81
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ExpressionTreeIndexerNode.cs

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal sealed class ExpressionTreeIndexerNode : CollectionNodeBase, ISettableNode
{
private readonly ParameterExpression _parameter;
private readonly IndexExpression _expression;
private readonly Delegate _setDelegate;
private readonly Delegate _getDelegate;
private readonly Delegate _firstArgumentDelegate;
public ExpressionTreeIndexerNode(IndexExpression expression)
{
var valueParameter = Expression.Parameter(expression.Type);
_parameter = Expression.Parameter(expression.Object!.Type);
_expression = expression.Update(_parameter, expression.Arguments);
_getDelegate = Expression.Lambda(_expression, _parameter).Compile();
_setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile();
_firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile();
}
public Type? ValueType => _expression.Type;
public override void BuildString(StringBuilder builder)
{
builder.Append('[');
for (var i = 1; i < _expression.Arguments.Count; ++i)
{
if (i > 1)
builder.Append(", ");
builder.Append(_expression.Arguments[i]);
}
builder.Append(_expression.Arguments[0]);
builder.Append(']');
}
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (Source is null)
return false;
_setDelegate.DynamicInvoke(Source, value);
return true;
}
protected override bool ShouldUpdate(object? sender, PropertyChangedEventArgs e)
{
return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt()
{
var source = Source;
if (source is null)
return null;
return _firstArgumentDelegate.DynamicInvoke(source) as int?;
}
protected override void UpdateValue(object? source)
{
try
{
if (source is not null)
SetValue(_getDelegate.DynamicInvoke(source));
else
SetValue(null);
}
catch (Exception e)
{
SetError(e);
}
}
}

159
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs

@ -0,0 +1,159 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using Avalonia.Input;
using Avalonia.Utilities;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)]
internal sealed class ReflectionIndexerNode : CollectionNodeBase, ISettableNode
{
private static readonly BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
private MethodInfo? _getter;
private MethodInfo? _setter;
private object?[]? _indexes;
public ReflectionIndexerNode(IList arguments)
{
Arguments = arguments;
}
public IList Arguments { get; }
public Type? ValueType => _getter?.ReturnType;
public override void BuildString(StringBuilder builder)
{
builder.Append('[');
for (var i = 0; i < Arguments.Count; i++)
{
builder.Append(Arguments[i]);
if (i != Arguments.Count - 1)
builder.Append(',');
}
builder.Append(']');
}
public bool WriteValueToSource(object? value, IReadOnlyList<ExpressionNode> nodes)
{
if (Source is null || _setter is null)
return false;
var args = new object?[_indexes!.Length + 1];
_indexes.CopyTo(args, 0);
args[_indexes.Length] = value;
_setter.Invoke(Source, args);
return true;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
_indexes = null;
if (GetIndexer(source.GetType(), out _getter, out _setter))
{
var parameters = _getter.GetParameters();
if (parameters.Length != Arguments.Count)
{
SetError($"Wrong number of arguments for indexer: expected {parameters.Length}, got {Arguments.Count}.");
return;
}
_indexes = ConvertIndexes(parameters, Arguments);
base.OnSourceChanged(source, dataValidationError);
}
else
{
SetError($"Type '{source.GetType()}' does not have an indexer.");
}
}
protected override bool ShouldUpdate(object? sender, PropertyChangedEventArgs e)
{
if (sender is null || e.PropertyName is null)
return false;
var typeInfo = sender.GetType().GetTypeInfo();
return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
}
protected override int? TryGetFirstArgumentAsInt()
{
if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value))
return (int?)value;
return null;
}
protected override void UpdateValue(object? source)
{
if (_getter is not null && _indexes is not null)
SetValue(_getter.Invoke(source, _indexes));
else
ClearValue();
}
private static object?[] ConvertIndexes(ParameterInfo[] indexParameters, IList arguments)
{
var result = new List<object?>();
for (var i = 0; i < indexParameters.Length; i++)
{
var type = indexParameters[i].ParameterType;
var argument = arguments[i];
if (TypeUtilities.TryConvert(type, argument, CultureInfo.InvariantCulture, out var value))
result.Add(value);
else
throw new InvalidCastException(
$"Could not convert list index '{i}' of type '{argument}' to '{type}'.");
}
return result.ToArray();
}
private static bool GetIndexer(Type? type, [NotNullWhen(true)] out MethodInfo? getter, out MethodInfo? setter)
{
getter = setter = null;
if (type is null)
return false;
if (type.IsArray)
{
getter = type.GetMethod("Get");
setter = type.GetMethod("Set");
return getter is not null;
}
for (; type != null; type = type.BaseType)
{
// check for the default indexer name first to make this faster.
// this will only be false when a class in vb has a custom indexer name.
if (type.GetProperty(CommonPropertyNames.IndexerName, InstanceFlags) is { } indexer)
{
getter = indexer.GetMethod;
setter = indexer.SetMethod;
return getter is not null;
}
foreach (var property in type.GetProperties(InstanceFlags))
{
if (property.GetIndexParameters().Length > 0)
{
getter = property.GetMethod;
setter = property.SetMethod;
return getter is not null;
}
}
}
return false;
}
}

29
src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs

@ -0,0 +1,29 @@
using System;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes.Reflection;
/// <summary>
/// A node in an <see cref="BindingExpression"/> which casts a value using reflection.
/// </summary>
internal sealed class ReflectionTypeCastNode : ExpressionNode
{
private readonly Type _targetType;
public ReflectionTypeCastNode(Type targetType) => _targetType = targetType;
public override void BuildString(StringBuilder builder)
{
builder.Append('(');
builder.Append(_targetType.Name);
builder.Append(')');
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_targetType.IsInstanceOfType(source))
SetValue(source);
else
ClearValue();
}
}

26
src/Avalonia.Base/Data/Core/ExpressionNodes/SourceNode.cs

@ -0,0 +1,26 @@
namespace Avalonia.Data.Core.ExpressionNodes;
/// <summary>
/// A node in a binding expression which represents the source of the binding, e.g. DataContext,
/// logical ancestor.
/// </summary>
internal abstract class SourceNode : ExpressionNode
{
/// <summary>
/// Selects the source for the binding expression based on the binding source, target and
/// anchor.
/// </summary>
/// <param name="source">The binding source.</param>
/// <param name="target">The binding target.</param>
/// <param name="anchor">The anchor.</param>
/// <returns>The source for the binding expression.</returns>
public virtual object? SelectSource(object? source, object target, object? anchor)
{
return source != AvaloniaProperty.UnsetValue ? source : target;
}
public virtual bool ShouldLogErrors(object target)
{
return Value is not null;
}
}

43
src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs

@ -0,0 +1,43 @@
using System;
using System.Text;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class StreamNode : ExpressionNode, IObserver<object?>
{
private IStreamPlugin _plugin;
private IDisposable? _subscription;
public StreamNode(IStreamPlugin plugin)
{
_plugin = plugin;
}
public override void BuildString(StringBuilder builder)
{
builder.Append('^');
}
void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => SetValue(value);
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (_plugin.Start(new(source)) is { } accessor)
{
_subscription = accessor.Subscribe(this);
}
else
{
ClearValue();
}
}
protected override void Unsubscribe(object oldSource)
{
_subscription?.Dispose();
_subscription = null;
}
}

49
src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs

@ -0,0 +1,49 @@
using System;
using System.Text;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class TemplatedParentNode : SourceNode
{
public override void BuildString(StringBuilder builder)
{
builder.Append("$templatedParent");
}
public override object? SelectSource(object? source, object target, object? anchor)
{
if (source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException(
"TemplatedParentNode is invalid in conjunction with a binding source.");
if (target is StyledElement)
return target;
if (anchor is StyledElement)
return anchor;
throw new InvalidOperationException("Cannot find a StyledElement to get a TemplatedParent.");
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is StyledElement newElement)
{
newElement.PropertyChanged += OnPropertyChanged;
SetValue(newElement.TemplatedParent);
}
else
{
SetError($"Unable to read TemplatedParent from '{source.GetType()}'.");
}
}
protected override void Unsubscribe(object oldSource)
{
if (oldSource is StyledElement oldElement)
oldElement.PropertyChanged -= OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender == Source && e.Property == StyledElement.TemplatedParentProperty)
SetValue(e.NewValue);
}
}

86
src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs

@ -0,0 +1,86 @@
using System;
using System.Text;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Data.Core.ExpressionNodes;
internal sealed class VisualAncestorElementNode : SourceNode
{
private readonly Type? _ancestorType;
private readonly int _ancestorLevel;
private IDisposable? _subscription;
public VisualAncestorElementNode(Type? ancestorType, int ancestorLevel)
{
_ancestorType = ancestorType;
_ancestorLevel = ancestorLevel;
}
public override void BuildString(StringBuilder builder)
{
builder.Append("$visualParent");
if (_ancestorLevel > 0 || _ancestorType is not null)
{
builder.Append('[');
if (_ancestorType is not null)
{
builder.Append(_ancestorType.Name);
if (_ancestorLevel > 0)
builder.Append(',');
}
if (_ancestorLevel > 0)
builder.Append(_ancestorLevel);
builder.Append(']');
}
}
public override object? SelectSource(object? source, object target, object? anchor)
{
if (source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException(
"VisualAncestorNode is invalid in conjunction with a binding source.");
if (target is Visual)
return target;
if (anchor is Visual)
return anchor;
throw new InvalidOperationException("Cannot find an ILogical to get a visual ancestor.");
}
public override bool ShouldLogErrors(object target)
{
return target is Visual visual && visual.IsAttachedToVisualTree;
}
protected override void OnSourceChanged(object source, Exception? dataValidationError)
{
if (source is Visual visual)
{
var locator = VisualLocator.Track(visual, _ancestorLevel, _ancestorType);
_subscription = locator.Subscribe(TrackedControlChanged);
}
}
protected override void Unsubscribe(object oldSource)
{
_subscription?.Dispose();
_subscription = null;
}
private void TrackedControlChanged(Visual? control)
{
if (control is not null)
{
SetValue(control);
}
else
{
SetError("Ancestor not found.");
}
}
}

316
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -1,316 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
/// <summary>
/// Observes and sets the value of an expression on an object.
/// </summary>
internal class ExpressionObserver : LightweightObservableBase<object?>, IDescription
{
/// <summary>
/// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type.
/// </summary>
public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
new List<IPropertyAccessorPlugin>
{
new AvaloniaPropertyAccessorPlugin(),
new MethodAccessorPlugin(),
new InpcPropertyAccessorPlugin(),
};
/// <summary>
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static readonly List<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new DataAnnotationsValidationPlugin(),
new IndeiValidationPlugin(),
new ExceptionValidationPlugin(),
};
/// <summary>
/// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator.
/// </summary>
public static readonly List<IStreamPlugin> StreamHandlers =
new List<IStreamPlugin>
{
new TaskStreamPlugin(),
new ObservableStreamPlugin(),
};
private readonly ExpressionNode _node;
private readonly object? _root;
private readonly Func<object?>? _rootGetter;
private IDisposable? _rootSubscription;
private WeakReference<object?>? _value;
private IReadOnlyList<ITransformNode>? _transformNodes;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="node">The expression.</param>
/// <param name="description">
/// A description of the expression.
/// </param>
public ExpressionObserver(
object? root,
ExpressionNode node,
string? description = null)
{
_node = node;
Description = description;
_root = new WeakReference<object?>(root == AvaloniaProperty.UnsetValue ? null : root);
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="node">The expression.</param>
/// <param name="description">
/// A description of the expression.
/// </param>
public ExpressionObserver(
IObservable<object?> rootObservable,
ExpressionNode node,
string? description)
{
_ = rootObservable ??throw new ArgumentNullException(nameof(rootObservable));
_node = node;
Description = description;
_root = rootObservable;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="node">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter. Generic argument value is not used.</param>
/// <param name="description">
/// A description of the expression.
/// </param>
public ExpressionObserver(
Func<object?> rootGetter,
ExpressionNode node,
IObservable<ValueTuple> update,
string? description)
{
Description = description;
_rootGetter = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
_node = node ?? throw new ArgumentNullException(nameof(node));
_root = update.Select(x => rootGetter());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
T? root,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string? description = null)
{
return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
bool enableDataValidation = false,
string? description = null)
{
_ = rootObservable ?? throw new ArgumentNullException(nameof(rootObservable));
return new ExpressionObserver(
rootObservable.Select(o => (object?)o),
Parse(expression, enableDataValidation),
description ?? expression.ToString());
}
/// <summary>
/// Creates a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter. Generic argument value is not used.</param>
/// <param name="enableDataValidation">Whether or not to track data validation</param>
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
IObservable<ValueTuple> update,
bool enableDataValidation = false,
string? description = null)
{
_ = rootGetter ?? throw new ArgumentNullException(nameof(rootGetter));
return new ExpressionObserver(
() => rootGetter(),
Parse(expression, enableDataValidation),
update,
description ?? expression.ToString());
}
private IReadOnlyList<ITransformNode> GetTransformNodesFromChain()
{
LinkedList<ITransformNode> transforms = new LinkedList<ITransformNode>();
var node = _node;
while (node != null)
{
if (node is ITransformNode transform)
{
transforms.AddFirst(transform);
}
node = node.Next;
}
return new List<ITransformNode>(transforms);
}
private IReadOnlyList<ITransformNode> TransformNodes => (_transformNodes ?? (_transformNodes = GetTransformNodesFromChain()));
/// <summary>
/// Attempts to set the value of a property expression.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="priority">The binding priority to use.</param>
/// <returns>
/// True if the value could be set; false if the expression does not evaluate to a
/// property. Note that the <see cref="ExpressionObserver"/> must be subscribed to
/// before setting the target value can work, as setting the value requires the
/// expression to be evaluated.
/// </returns>
public bool SetValue(object? value, BindingPriority priority = BindingPriority.LocalValue)
{
if (Leaf is SettableNode settable)
{
foreach (var transform in TransformNodes)
{
value = transform.Transform(value);
if (value is BindingNotification)
{
return false;
}
}
return settable.SetTargetValue(value, priority);
}
return false;
}
/// <summary>
/// Gets a description of the expression being observed.
/// </summary>
public string? Description { get; }
/// <summary>
/// Gets the expression being observed.
/// </summary>
public string? Expression { get; }
/// <summary>
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
/// </summary>
public Type? ResultType => (Leaf as SettableNode)?.PropertyType;
/// <summary>
/// Gets the leaf node.
/// </summary>
private ExpressionNode Leaf
{
get
{
var node = _node;
while (node.Next != null) node = node.Next;
return node;
}
}
protected override void Initialize()
{
_value = null;
if (_rootGetter is not null)
_node.Target = new WeakReference<object?>(_rootGetter());
_node.Subscribe(ValueChanged);
StartRoot();
}
protected override void Deinitialize()
{
_rootSubscription?.Dispose();
_rootSubscription = null;
_node.Unsubscribe();
}
protected override void Subscribed(IObserver<object?> observer, bool first)
{
if (!first && _value != null && _value.TryGetTarget(out var value))
{
observer.OnNext(value);
}
}
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{
return ExpressionTreeParser.Parse(expression, enableDataValidation);
}
private void StartRoot()
{
if (_root is IObservable<object> observable)
{
_rootSubscription = observable.Subscribe(
new AnonymousObserver<object>(
x => _node.Target = new WeakReference<object?>(x != AvaloniaProperty.UnsetValue ? x : null),
x => PublishCompleted(),
PublishCompleted));
}
else
{
_node.Target = (WeakReference<object?>)_root!;
}
}
private void ValueChanged(object? value)
{
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description ?? "{empty}");
_value = new WeakReference<object?>(value);
PublishNext(value);
}
}
}

20
src/Avalonia.Base/Data/Core/IBinding2.cs

@ -0,0 +1,20 @@
namespace Avalonia.Data.Core;
/// <summary>
/// Internal interface for instancing bindings on an <see cref="AvaloniaObject"/>.
/// </summary>
/// <remarks>
/// TODO12: The presence of this interface is a hack needed because we can't break our API until
/// 12.0. The Instance method would ideally be located as an internal method on a BindingBase
/// class, but we already have a BindingBase in 11.x which is not suitable for this as it contains
/// extra members that are not needed on all of the binding types. The current BindingBase should
/// be renamed to something like BindingMarkupExtensionBase and a new BindingBase created with the
/// Instance method from this interface. This interface should then be removed.
/// </remarks>
internal interface IBinding2 : IBinding
{
BindingExpressionBase Instance(
AvaloniaObject target,
AvaloniaProperty targetProperty,
object? anchor);
}

34
src/Avalonia.Base/Data/Core/IBindingExpressionSink.cs

@ -0,0 +1,34 @@
namespace Avalonia.Data.Core;
internal interface IBindingExpressionSink
{
/// <summary>
/// Called when an <see cref="UntypedBindingExpressionBase"/>'s value or error state
/// changes.
/// </summary>
/// <param name="instance">The binding expression.</param>
/// <param name="hasValueChanged">
/// Indicates whether <paramref name="value"/> represents a new value produced by the binding.
/// </param>
/// <param name="hasErrorChanged">
/// Indicates whether <paramref name="error"/> represents a new error produced by the binding.
/// </param>
/// <param name="value">
/// The new binding value; if <paramref name="hasValueChanged"/> is true.
/// </param>
/// <param name="error">
/// The new binding error; if <paramref name="hasErrorChanged"/> is true.
/// </param>
void OnChanged(
UntypedBindingExpressionBase instance,
bool hasValueChanged,
bool hasErrorChanged,
object? value,
BindingError? error);
/// <summary>
/// Called when an <see cref="UntypedBindingExpressionBase"/> completes.
/// </summary>
/// <param name="instance">The binding expression.</param>
void OnCompleted(UntypedBindingExpressionBase instance);
}

7
src/Avalonia.Base/Data/Core/ITransformNode.cs

@ -1,7 +0,0 @@
namespace Avalonia.Data.Core
{
interface ITransformNode
{
object? Transform(object? value);
}
}

68
src/Avalonia.Base/Data/Core/IndexerBindingExpression.cs

@ -0,0 +1,68 @@
using System;
namespace Avalonia.Data.Core;
internal class IndexerBindingExpression : UntypedBindingExpressionBase
{
private readonly AvaloniaObject _source;
private readonly AvaloniaProperty _sourceProperty;
private readonly AvaloniaObject _target;
private readonly AvaloniaProperty? _targetProperty;
private readonly BindingMode _mode;
public IndexerBindingExpression(
AvaloniaObject source,
AvaloniaProperty sourceProperty,
AvaloniaObject target,
AvaloniaProperty? targetProperty,
BindingMode mode)
: base(BindingPriority.LocalValue)
{
_source = source;
_sourceProperty = sourceProperty;
_target = target;
_targetProperty = targetProperty;
_mode = mode;
}
public override string Description => $"IndexerBinding {_sourceProperty})";
internal override bool WriteValueToSource(object? value)
{
_source.SetValue(_sourceProperty, value);
return true;
}
protected override void StartCore()
{
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && _targetProperty is not null)
_target.PropertyChanged += OnTargetPropertyChanged;
if (_mode is not BindingMode.OneWayToSource)
{
_source.PropertyChanged += OnSourcePropertyChanged;
PublishValue(_source.GetValue(_sourceProperty));
}
if (_mode is BindingMode.OneTime)
Stop();
}
protected override void StopCore()
{
_source.PropertyChanged -= OnSourcePropertyChanged;
_target.PropertyChanged -= OnTargetPropertyChanged;
}
private void OnSourcePropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _sourceProperty)
PublishValue(_source.GetValue(_sourceProperty));
}
private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _targetProperty)
WriteValueToSource(e.NewValue);
}
}

76
src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs

@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
namespace Avalonia.Data.Core
{
class IndexerExpressionNode : IndexerNodeBase
{
private readonly ParameterExpression _parameter;
private readonly IndexExpression _expression;
private readonly Delegate _setDelegate;
private readonly Delegate _getDelegate;
private readonly Delegate _firstArgumentDelegate;
public IndexerExpressionNode(IndexExpression expression)
{
_parameter = Expression.Parameter(expression.Object!.Type);
_expression = expression.Update(_parameter, expression.Arguments);
_getDelegate = Expression.Lambda(_expression, _parameter).Compile();
var valueParameter = Expression.Parameter(expression.Type);
_setDelegate = Expression.Lambda(Expression.Assign(_expression, valueParameter), _parameter, valueParameter).Compile();
_firstArgumentDelegate = Expression.Lambda(_expression.Arguments[0], _parameter).Compile();
}
public override Type PropertyType => _expression.Type;
public override string Description => _expression.ToString();
protected override bool SetTargetValueCore(object? value, BindingPriority priority)
{
try
{
Target.TryGetTarget(out var target);
_setDelegate.DynamicInvoke(target, value);
return true;
}
catch (Exception)
{
return false;
}
}
protected override object? GetValue(object? target)
{
try
{
return _getDelegate.DynamicInvoke(target);
}
catch (TargetInvocationException e) when (e.InnerException is ArgumentOutOfRangeException
|| e.InnerException is IndexOutOfRangeException
|| e.InnerException is KeyNotFoundException)
{
return AvaloniaProperty.UnsetValue;
}
}
protected override bool ShouldUpdate(object? sender, PropertyChangedEventArgs e)
{
return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName;
}
protected override int? TryGetFirstArgumentAsInt()
{
Target.TryGetTarget(out var target);
return _firstArgumentDelegate.DynamicInvoke(target) as int?;
}
}
}

102
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -1,102 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
internal abstract class IndexerNodeBase : SettableNode,
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>,
IWeakEventSubscriber<PropertyChangedEventArgs>
{
protected override void StartListeningCore(WeakReference<object?> reference)
{
reference.TryGetTarget(out var target);
if (target is INotifyCollectionChanged incc)
{
WeakEvents.CollectionChanged.Subscribe(incc, this);
}
if (target is INotifyPropertyChanged inpc)
{
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
ValueChanged(GetValue(target));
}
protected override void StopListeningCore()
{
if (Target.TryGetTarget(out var target))
{
if (target is INotifyCollectionChanged incc)
{
WeakEvents.CollectionChanged.Unsubscribe(incc, this);
}
if (target is INotifyPropertyChanged inpc)
{
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
}
}
protected abstract object? GetValue(object? target);
protected abstract int? TryGetFirstArgumentAsInt();
private bool ShouldUpdate(object? sender, NotifyCollectionChangedEventArgs e)
{
if (sender is IList)
{
var index = TryGetFirstArgumentAsInt();
if (index == null)
{
return false;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
return index >= e.NewStartingIndex;
case NotifyCollectionChangedAction.Remove:
return index >= e.OldStartingIndex;
case NotifyCollectionChangedAction.Replace:
return index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems!.Count;
case NotifyCollectionChangedAction.Move:
return (index >= e.NewStartingIndex &&
index < e.NewStartingIndex + e.NewItems!.Count) ||
(index >= e.OldStartingIndex &&
index < e.OldStartingIndex + e.OldItems!.Count);
case NotifyCollectionChangedAction.Reset:
return true;
}
}
return true; // Implementation defined meaning for the index, so just try to update anyway
}
protected abstract bool ShouldUpdate(object? sender, PropertyChangedEventArgs e);
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
if (ShouldUpdate(sender, e))
{
ValueChanged(GetValue(sender));
}
}
void IWeakEventSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
{
if (ShouldUpdate(sender, e))
{
ValueChanged(GetValue(sender));
}
}
}
}

91
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

@ -1,91 +0,0 @@
using System;
using System.Globalization;
namespace Avalonia.Data.Core
{
internal class LogicalNotNode : ExpressionNode, ITransformNode
{
public override string Description => "!";
protected override void NextValueChanged(object? value)
{
base.NextValueChanged(Negate(value));
}
private static object Negate(object? value)
{
var notification = value as BindingNotification;
var v = BindingNotification.ExtractValue(value);
BindingNotification GenerateError(Exception e)
{
notification ??= new BindingNotification(AvaloniaProperty.UnsetValue);
notification.AddError(e, BindingErrorType.Error);
notification.ClearValue();
return notification;
}
if (v != AvaloniaProperty.UnsetValue)
{
var s = v as string;
if (s != null)
{
bool result;
if (bool.TryParse(s, out result))
{
return !result;
}
else
{
return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool."));
}
}
else
{
try
{
var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture);
if (notification is object)
{
notification.SetValue(!boolean);
return notification;
}
else
{
return !boolean;
}
}
catch (InvalidCastException)
{
// The error message here is "Unable to cast object of type 'System.Object'
// to type 'System.IConvertible'" which is kinda useless so provide our own.
return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool."));
}
catch (Exception e)
{
return GenerateError(e);
}
}
}
return notification ?? AvaloniaProperty.UnsetValue;
}
public object? Transform(object? value)
{
if (value is null)
return null;
var originalType = value.GetType();
var negated = Negate(value);
if (negated is BindingNotification)
{
return negated;
}
return Convert.ChangeType(negated, originalType);
}
}
}

43
src/Avalonia.Base/Data/Core/MarkupBindingChainException.cs

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Data.Core
{
internal class MarkupBindingChainException : BindingChainException
{
private IList<string>? _nodes = new List<string>();
public MarkupBindingChainException(string message)
: base(message)
{
}
public MarkupBindingChainException(string message, string node)
: base(message)
{
AddNode(node);
}
public MarkupBindingChainException(string message, string expression, string expressionNullPoint)
: base(message, expression, expressionNullPoint)
{
_nodes = null;
}
public bool HasNodes => _nodes?.Count > 0;
public void AddNode(string node) => _nodes?.Add(node);
public void Commit(string expression)
{
Expression = expression;
ExpressionErrorPoint = _nodes != null ?
string.Join(".", _nodes.Reverse())
.Replace(".!", "!")
.Replace(".[", "[")
.Replace(".^", "^") :
string.Empty;
_nodes = null;
}
}
}

232
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs

@ -0,0 +1,232 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.ExpressionNodes.Reflection;
namespace Avalonia.Data.Core.Parsers;
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
{
private static readonly PropertyInfo AvaloniaObjectIndexer;
private static readonly MethodInfo CreateDelegateMethod;
private static readonly string IndexerGetterName = "get_Item";
private const string MultiDimensionalArrayGetterMethodName = "Get";
private readonly bool _enableDataValidation;
private readonly LambdaExpression _rootExpression;
private readonly List<ExpressionNode> _nodes = new();
private Expression? _head;
public BindingExpressionVisitor(LambdaExpression expression, bool enableDataValidation)
{
_rootExpression = expression;
_enableDataValidation = enableDataValidation;
}
static BindingExpressionVisitor()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) })!;
}
public static List<ExpressionNode> BuildNodes<TOut>(Expression<Func<TIn, TOut>> expression, bool enableDataValidation)
{
var visitor = new BindingExpressionVisitor<TIn>(expression, enableDataValidation);
visitor.Visit(expression);
return visitor._nodes;
}
protected override Expression VisitBinary(BinaryExpression node)
{
// Indexers require more work since the compiler doesn't generate IndexExpressions:
// they weren't in System.Linq.Expressions v1 and so must be generated manually.
if (node.NodeType == ExpressionType.ArrayIndex)
return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitIndex(IndexExpression node)
{
if (node.Indexer == AvaloniaObjectIndexer)
{
var property = GetValue<AvaloniaProperty>(node.Arguments[0]);
return Add(node.Object, node, new AvaloniaPropertyAccessorNode(property));
}
else
{
return Add(node.Object, node, new ExpressionTreeIndexerNode(node));
}
}
protected override Expression VisitMember(MemberExpression node)
{
switch (node.Member.MemberType)
{
case MemberTypes.Property:
return Add(node.Expression, node, new DynamicPluginPropertyAccessorNode(node.Member.Name));
default:
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
var method = node.Method;
if (method.Name == IndexerGetterName && node.Object is not null)
{
var property = TryGetPropertyFromMethod(method);
return Visit(Expression.MakeIndex(node.Object, property, node.Arguments));
}
else if (method.Name == MultiDimensionalArrayGetterMethodName &&
node.Object is not null)
{
var expression = Expression.MakeIndex(node.Object, null, node.Arguments);
return Add(node.Object, node, new ExpressionTreeIndexerNode(expression));
}
else if (method.Name.StartsWith(StreamBindingExtensions.StreamBindingName) &&
method.DeclaringType == typeof(StreamBindingExtensions))
{
var instance = node.Method.IsStatic ? node.Arguments[0] : node.Object;
Add(instance, node, new DynamicPluginStreamNode());
return node;
}
else if (method == CreateDelegateMethod)
{
var accessor = new DynamicPluginPropertyAccessorNode(GetValue<MethodInfo>(node.Object!).Name);
return Add(node.Arguments[1], node, accessor);
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType}.{node.Method.Name}'.");
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == _rootExpression.Parameters[0] && _head is null)
_head = node;
return base.VisitParameter(node);
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
return Add(node.Operand, node, new LogicalNotNode());
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand.Type.IsAssignableFrom(node.Type))
{
// Ignore inheritance casts
return _head = base.VisitUnary(node);
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
// Ignore as operator.
return _head = base.VisitUnary(node);
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitBlock(BlockExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLabel(LabelExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitListInit(ListInitExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLoop(LoopExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
}
protected override Expression VisitSwitch(SwitchExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTry(TryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
private Expression Add(Expression? instance, Expression expression, ExpressionNode node)
{
var visited = Visit(instance);
if (visited != _head)
throw new ExpressionParseException(
0,
$"Unable to parse '{expression}': expected an instance of '{_head}' but got '{visited}'.");
_nodes.Add(node);
return _head = expression;
}
private static T GetValue<T>(Expression expr)
{
if (expr is ConstantExpression constant)
return (T)constant.Value!;
return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
}
private static PropertyInfo? TryGetPropertyFromMethod(MethodInfo method)
{
var type = method.DeclaringType;
return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
}

26
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

@ -1,26 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
namespace Avalonia.Data.Core.Parsers
{
static class ExpressionTreeParser
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public static ExpressionNode Parse(Expression expr, bool enableDataValidation)
{
var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation);
visitor.Visit(expr);
var nodes = visitor.Nodes;
for (int n = 0; n < nodes.Count - 1; ++n)
{
nodes[n].Next = nodes[n + 1];
}
return nodes.FirstOrDefault() ?? new EmptyExpressionNode();
}
}
}

219
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@ -1,219 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Avalonia.Data.Core.Parsers
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
class ExpressionVisitorNodeBuilder : ExpressionVisitor
{
private const string MultiDimensionalArrayGetterMethodName = "Get";
private static readonly PropertyInfo AvaloniaObjectIndexer;
private static readonly MethodInfo CreateDelegateMethod;
private readonly bool _enableDataValidation;
static ExpressionVisitorNodeBuilder()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) })!;
}
public List<ExpressionNode> Nodes { get; }
public ExpressionVisitorNodeBuilder(bool enableDataValidation)
{
_enableDataValidation = enableDataValidation;
Nodes = new List<ExpressionNode>();
}
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
Nodes.Add(new LogicalNotNode());
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand.Type.IsAssignableFrom(node.Type))
{
// Ignore inheritance casts
}
else
{
throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression.");
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
// Ignore as operator.
}
else
{
throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression");
}
return base.VisitUnary(node);
}
protected override Expression VisitMember(MemberExpression node)
{
var visited = base.VisitMember(node);
Nodes.Add(new PropertyAccessorNode(node.Member.Name, _enableDataValidation));
return visited;
}
protected override Expression VisitIndex(IndexExpression node)
{
Visit(node.Object);
if (node.Indexer == AvaloniaObjectIndexer)
{
var property = GetArgumentExpressionValue<AvaloniaProperty>(node.Arguments[0]);
Nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableDataValidation));
}
else
{
Nodes.Add(new IndexerExpressionNode(node));
}
return node;
}
private static T GetArgumentExpressionValue<T>(Expression expr)
{
try
{
return Expression.Lambda<Func<T>>(expr).Compile(preferInterpretation: true)();
}
catch (InvalidOperationException ex)
{
throw new ExpressionParseException(0, "Unable to parse indexer value.", ex);
}
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.ArrayIndex)
{
return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitBlock(BlockExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLabel(LabelExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitListInit(ListInitExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitLoop(LoopExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method == CreateDelegateMethod)
{
var visited = Visit(node.Arguments[1]);
Nodes.Add(new PropertyAccessorNode(GetArgumentExpressionValue<MethodInfo>(node.Object!).Name, _enableDataValidation));
return node;
}
else if (node.Method.Name == StreamBindingExtensions.StreamBindingName || node.Method.Name.StartsWith(StreamBindingExtensions.StreamBindingName + '`'))
{
if (node.Method.IsStatic)
{
Visit(node.Arguments[0]);
}
else
{
Visit(node.Object);
}
Nodes.Add(new StreamNode());
return node;
}
var property = TryGetPropertyFromMethod(node.Method);
if (property != null)
{
return Visit(Expression.MakeIndex(node.Object!, property, node.Arguments));
}
else if (node.Object!.Type.IsArray && node.Method.Name == MultiDimensionalArrayGetterMethodName)
{
return Visit(Expression.MakeIndex(node.Object, null, node.Arguments));
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType!.AssemblyQualifiedName}.{node.Method.Name}'.");
}
private static PropertyInfo? TryGetPropertyFromMethod(MethodInfo method)
{
var type = method.DeclaringType;
return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
protected override Expression VisitSwitch(SwitchExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTry(TryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
}
}

4
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ExceptionServices;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
@ -11,7 +9,6 @@ namespace Avalonia.Data.Core.Plugins
internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{ {
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string propertyName) public bool Match(object obj, string propertyName)
{ {
if (obj is AvaloniaObject o) if (obj is AvaloniaObject o)
@ -31,7 +28,6 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName) public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
{ {
_ = reference ?? throw new ArgumentNullException(nameof(reference)); _ = reference ?? throw new ArgumentNullException(nameof(reference));

28
src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs

@ -1,28 +1,50 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
/// <summary> /// <summary>
/// Holds a registry of plugins used for bindings. /// Holds a registry of plugins used for bindings.
/// </summary> /// </summary>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public static class BindingPlugins public static class BindingPlugins
{ {
internal static readonly List<IPropertyAccessorPlugin> s_propertyAccessors = new()
{
new AvaloniaPropertyAccessorPlugin(),
new ReflectionMethodAccessorPlugin(),
new InpcPropertyAccessorPlugin(),
};
internal static readonly List<IDataValidationPlugin> s_dataValidators = new()
{
new DataAnnotationsValidationPlugin(),
new IndeiValidationPlugin(),
new ExceptionValidationPlugin(),
};
internal static readonly List<IStreamPlugin> s_streamHandlers = new()
{
new TaskStreamPlugin(),
new ObservableStreamPlugin(),
};
/// <summary> /// <summary>
/// An ordered collection of property accessor plugins that can be used to customize /// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type. /// the reading and subscription of property values on a type.
/// </summary> /// </summary>
public static IList<IPropertyAccessorPlugin> PropertyAccessors => ExpressionObserver.PropertyAccessors; public static IList<IPropertyAccessorPlugin> PropertyAccessors => s_propertyAccessors;
/// <summary> /// <summary>
/// An ordered collection of validation checker plugins that can be used to customize /// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data. /// the validation of view model and model data.
/// </summary> /// </summary>
public static IList<IDataValidationPlugin> DataValidators => ExpressionObserver.DataValidators; public static IList<IDataValidationPlugin> DataValidators => s_dataValidators;
/// <summary> /// <summary>
/// An ordered collection of stream plugins that can be used to customize the behavior /// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator. /// of the '^' stream binding operator.
/// </summary> /// </summary>
public static IList<IStreamPlugin> StreamHandlers => ExpressionObserver.StreamHandlers; public static IList<IStreamPlugin> StreamHandlers => s_streamHandlers;
} }
} }

3
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@ -10,10 +10,10 @@ namespace Avalonia.Data.Core.Plugins
/// <summary> /// <summary>
/// Validates properties on that have <see cref="ValidationAttribute"/>s. /// Validates properties on that have <see cref="ValidationAttribute"/>s.
/// </summary> /// </summary>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public class DataAnnotationsValidationPlugin : IDataValidationPlugin public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{ {
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName) public bool Match(WeakReference<object?> reference, string memberName)
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);
@ -26,7 +26,6 @@ namespace Avalonia.Data.Core.Plugins
} }
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner) public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
{ {
return new Accessor(reference, name, inner); return new Accessor(reference, name, inner);

2
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -10,11 +10,9 @@ namespace Avalonia.Data.Core.Plugins
public class ExceptionValidationPlugin : IDataValidationPlugin public class ExceptionValidationPlugin : IDataValidationPlugin
{ {
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName) => true; public bool Match(WeakReference<object?> reference, string memberName) => true;
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner) public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
{ {
return new Validator(reference, name, inner); return new Validator(reference, name, inner);

5
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@ -1,10 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
/// <summary> /// <summary>
/// Defines how data validation is observed by an <see cref="ExpressionObserver"/>. /// Defines how data validation is observed by an <see cref="BindingExpression"/>.
/// </summary> /// </summary>
public interface IDataValidationPlugin public interface IDataValidationPlugin
{ {
@ -14,7 +13,6 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="reference">A weak reference to the object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param> /// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns> /// <returns>True if the plugin can handle the object; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
bool Match(WeakReference<object?> reference, string memberName); bool Match(WeakReference<object?> reference, string memberName);
/// <summary> /// <summary>
@ -27,7 +25,6 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
IPropertyAccessor Start(WeakReference<object?> reference, IPropertyAccessor Start(WeakReference<object?> reference,
string propertyName, string propertyName,
IPropertyAccessor inner); IPropertyAccessor inner);

6
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs

@ -1,11 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
/// <summary> /// <summary>
/// Defines how a member is read, written and observed by an /// Defines how a member is read, written and observed by a binding.
/// <see cref="ExpressionObserver"/>.
/// </summary> /// </summary>
public interface IPropertyAccessorPlugin public interface IPropertyAccessorPlugin
{ {
@ -15,7 +13,6 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="obj">The object.</param> /// <param name="obj">The object.</param>
/// <param name="propertyName">The property name.</param> /// <param name="propertyName">The property name.</param>
/// <returns>True if the plugin can handle the property on the object; otherwise false.</returns> /// <returns>True if the plugin can handle the property on the object; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
bool Match(object obj, string propertyName); bool Match(object obj, string propertyName);
/// <summary> /// <summary>
@ -27,7 +24,6 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
IPropertyAccessor? Start(WeakReference<object?> reference, IPropertyAccessor? Start(WeakReference<object?> reference,
string propertyName); string propertyName);
} }

3
src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -13,7 +12,6 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the value.</param> /// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns> /// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
bool Match(WeakReference<object?> reference); bool Match(WeakReference<object?> reference);
/// <summary> /// <summary>
@ -23,7 +21,6 @@ namespace Avalonia.Data.Core.Plugins
/// <returns> /// <returns>
/// An observable that produces the output for the value. /// An observable that produces the output for the value.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
IObservable<object?> Start(WeakReference<object?> reference); IObservable<object?> Start(WeakReference<object?> reference);
} }
} }

3
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -19,7 +18,6 @@ namespace Avalonia.Data.Core.Plugins
); );
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName) public bool Match(WeakReference<object?> reference, string memberName)
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);
@ -28,7 +26,6 @@ namespace Avalonia.Data.Core.Plugins
} }
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor accessor) public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor accessor)
{ {
return new Validator(reference, name, accessor); return new Validator(reference, name, accessor);

15
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -11,13 +11,13 @@ namespace Avalonia.Data.Core.Plugins
/// Reads a property from a standard C# object that optionally supports the /// Reads a property from a standard C# object that optionally supports the
/// <see cref="INotifyPropertyChanged"/> interface. /// <see cref="INotifyPropertyChanged"/> interface.
/// </summary> /// </summary>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{ {
private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup = private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup =
new Dictionary<(Type, string), PropertyInfo?>(); new Dictionary<(Type, string), PropertyInfo?>();
/// <inheritdoc/> /// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null; public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null;
/// <summary> /// <summary>
@ -29,7 +29,6 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName) public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
{ {
_ = reference ?? throw new ArgumentNullException(nameof(reference)); _ = reference ?? throw new ArgumentNullException(nameof(reference));
@ -55,7 +54,6 @@ namespace Avalonia.Data.Core.Plugins
private const BindingFlags PropertyBindingFlags = private const BindingFlags PropertyBindingFlags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{ {
if (instance is IReflectableType reflectableType && instance is not Type) if (instance is IReflectableType reflectableType && instance is not Type)
@ -80,7 +78,7 @@ namespace Avalonia.Data.Core.Plugins
var properties = type.GetProperties(PropertyBindingFlags); var properties = type.GetProperties(PropertyBindingFlags);
foreach (PropertyInfo propertyInfo in properties) foreach (var propertyInfo in properties)
{ {
if (propertyInfo.Name == propertyName) if (propertyInfo.Name == propertyName)
{ {
@ -95,6 +93,7 @@ namespace Avalonia.Data.Core.Plugins
return found; return found;
} }
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs> private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{ {
private readonly WeakReference<object?> _reference; private readonly WeakReference<object?> _reference;
@ -117,7 +116,7 @@ namespace Avalonia.Data.Core.Plugins
get get
{ {
var o = GetReferenceTarget(); var o = GetReferenceTarget();
return (o != null) ? _property.GetValue(o) : null; return o != null ? _property.GetValue(o) : null;
} }
} }
@ -125,8 +124,12 @@ namespace Avalonia.Data.Core.Plugins
{ {
if (_property.CanWrite) if (_property.CanWrite)
{ {
if (!TypeUtilities.TryConvert(_property.PropertyType, value, null, out var converted))
throw new ArgumentException($"Object of type '{value?.GetType()}' " +
$"cannot be converted to type '{_property.PropertyType}'.");
_eventRaised = false; _eventRaised = false;
_property.SetValue(GetReferenceTarget(), value); _property.SetValue(GetReferenceTarget(), converted);
if (!_eventRaised) if (!_eventRaised)
{ {

107
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -1,113 +1,39 @@
using System; using System;
using System.Collections.Generic; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins;
{
internal class MethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
new Dictionary<(Type, string), MethodInfo?>();
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));
_ = methodName ?? throw new ArgumentNullException(nameof(methodName));
if (!reference.TryGetTarget(out var instance) || instance is null)
return null;
var method = GetFirstMethodWithName(instance.GetType(), methodName); internal class MethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly MethodInfo _method;
private readonly Type _delegateType;
if (method is not null) public MethodAccessorPlugin(MethodInfo method, Type delegateType)
{
return new Accessor(reference, method);
}
else
{ {
var message = $"Could not find CLR method '{methodName}' on '{instance}'"; _method = method;
var exception = new MissingMemberException(message); _delegateType = delegateType;
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
} }
}
private MethodInfo? GetFirstMethodWithName(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
var key = (type, methodName);
if (!_methodLookup.TryGetValue(key, out var methodInfo)) public bool Match(object obj, string propertyName)
{ {
methodInfo = TryFindAndCacheMethod(type, methodName); throw new InvalidOperationException("The MethodAccessorPlugin does not support dynamic matching");
}
return methodInfo;
} }
private MethodInfo? TryFindAndCacheMethod( public IPropertyAccessor Start(WeakReference<object?> reference, string propertyName)
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
MethodInfo? found = null;
const BindingFlags bindingFlags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
var methods = type.GetMethods(bindingFlags);
foreach (var methodInfo in methods)
{
if (methodInfo.Name == methodName)
{ {
var parameters = methodInfo.GetParameters(); Debug.Assert(_method.Name == propertyName);
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object)) return new Accessor(reference, _method, _delegateType);
{
found = methodInfo;
break;
}
else if (parameters.Length == 0)
{
found = methodInfo;
}
}
}
_methodLookup.Add((type, methodName), found);
return found;
} }
private sealed class Accessor : PropertyAccessorBase private sealed class Accessor : PropertyAccessorBase
{ {
public Accessor(WeakReference<object?> reference, MethodInfo method) public Accessor(WeakReference<object?> reference, MethodInfo method, Type delegateType)
{ {
_ = reference ?? throw new ArgumentNullException(nameof(reference)); _ = reference ?? throw new ArgumentNullException(nameof(reference));
_ = method ?? throw new ArgumentNullException(nameof(method)); _ = method ?? throw new ArgumentNullException(nameof(method));
var returnType = method.ReturnType; PropertyType = delegateType;
var parameters = method.GetParameters();
var signatureTypeCount = parameters.Length + 1;
var paramTypes = new Type[signatureTypeCount];
for (var i = 0; i < parameters.Length; i++)
{
ParameterInfo parameter = parameters[i];
paramTypes[i] = parameter.ParameterType;
}
paramTypes[paramTypes.Length - 1] = returnType;
PropertyType = Expression.GetDelegateType(paramTypes);
if (method.IsStatic) if (method.IsStatic)
{ {
@ -138,5 +64,4 @@ namespace Avalonia.Data.Core.Plugins
{ {
} }
} }
}
} }

5
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -1,8 +1,8 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Reactive;
using System.Reflection; using System.Reflection;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator. /// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
/// </summary> /// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class ObservableStreamPlugin : IStreamPlugin internal class ObservableStreamPlugin : IStreamPlugin
{ {
private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableGeneric;
@ -26,7 +27,6 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the value.</param> /// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns> /// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual bool Match(WeakReference<object?> reference) public virtual bool Match(WeakReference<object?> reference)
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);
@ -43,7 +43,6 @@ namespace Avalonia.Data.Core.Plugins
/// <returns> /// <returns>
/// An observable that produces the output for the value. /// An observable that produces the output for the value.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual IObservable<object?> Start(WeakReference<object?> reference) public virtual IObservable<object?> Start(WeakReference<object?> reference)
{ {
if (!reference.TryGetTarget(out var target) || target is null) if (!reference.TryGetTarget(out var target) || target is null)

27
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin`1.cs

@ -0,0 +1,27 @@
using System;
using System.Linq;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.Plugins;
internal class ObservableStreamPlugin<T> : IStreamPlugin
{
public bool Match(WeakReference<object?> reference)
{
return reference.TryGetTarget(out var target) && target is IObservable<T>;
}
public IObservable<object?> Start(WeakReference<object?> reference)
{
if (!(reference.TryGetTarget(out var target) && target is IObservable<T> obs))
{
return Observable.Empty<object?>();
}
else if (target is IObservable<object?> obj)
{
return obj;
}
return obs.Select(x => (object?)x);
}
}

27
src/Avalonia.Base/Data/Core/Plugins/PropertyInfoAccessorPlugin.cs

@ -0,0 +1,27 @@
using System;
using System.Diagnostics;
namespace Avalonia.Data.Core.Plugins;
internal class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin
{
private readonly IPropertyInfo _propertyInfo;
private readonly Func<WeakReference<object?>, IPropertyInfo, IPropertyAccessor> _accessorFactory;
public PropertyInfoAccessorPlugin(IPropertyInfo propertyInfo, Func<WeakReference<object?>, IPropertyInfo, IPropertyAccessor> accessorFactory)
{
_propertyInfo = propertyInfo;
_accessorFactory = accessorFactory;
}
public bool Match(object obj, string propertyName)
{
throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching");
}
public IPropertyAccessor Start(WeakReference<object?> reference, string propertyName)
{
Debug.Assert(_propertyInfo.Name == propertyName);
return _accessorFactory(reference, _propertyInfo);
}
}

141
src/Avalonia.Base/Data/Core/Plugins/ReflectionMethodAccessorPlugin.cs

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
namespace Avalonia.Data.Core.Plugins
{
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
internal class ReflectionMethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
new Dictionary<(Type, string), MethodInfo?>();
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));
_ = methodName ?? throw new ArgumentNullException(nameof(methodName));
if (!reference.TryGetTarget(out var instance) || instance is null)
return null;
var method = GetFirstMethodWithName(instance.GetType(), methodName);
if (method is not null)
{
return new Accessor(reference, method);
}
else
{
var message = $"Could not find CLR method '{methodName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}
private MethodInfo? GetFirstMethodWithName(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
var key = (type, methodName);
if (!_methodLookup.TryGetValue(key, out var methodInfo))
{
methodInfo = TryFindAndCacheMethod(type, methodName);
}
return methodInfo;
}
private MethodInfo? TryFindAndCacheMethod(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
MethodInfo? found = null;
const BindingFlags bindingFlags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
var methods = type.GetMethods(bindingFlags);
foreach (var methodInfo in methods)
{
if (methodInfo.Name == methodName)
{
var parameters = methodInfo.GetParameters();
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object))
{
found = methodInfo;
break;
}
else if (parameters.Length == 0)
{
found = methodInfo;
}
}
}
_methodLookup.Add((type, methodName), found);
return found;
}
private sealed class Accessor : PropertyAccessorBase
{
public Accessor(WeakReference<object?> reference, MethodInfo method)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));
_ = method ?? throw new ArgumentNullException(nameof(method));
var returnType = method.ReturnType;
var parameters = method.GetParameters();
var signatureTypeCount = parameters.Length + 1;
var paramTypes = new Type[signatureTypeCount];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
paramTypes[i] = parameter.ParameterType;
}
paramTypes[paramTypes.Length - 1] = returnType;
PropertyType = Expression.GetDelegateType(paramTypes);
if (method.IsStatic)
{
Value = method.CreateDelegate(PropertyType);
}
else if (reference.TryGetTarget(out var target))
{
Value = method.CreateDelegate(PropertyType, target);
}
}
public override Type? PropertyType { get; }
public override object? Value { get; }
public override bool SetValue(object? value, BindingPriority priority) => false;
protected override void SubscribeCore()
{
try
{
PublishValue(Value);
}
catch { }
}
protected override void UnsubscribeCore()
{
}
}
}
}

3
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator. /// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
/// </summary> /// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
internal class TaskStreamPlugin : IStreamPlugin internal class TaskStreamPlugin : IStreamPlugin
{ {
/// <summary> /// <summary>
@ -17,7 +18,6 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the value.</param> /// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns> /// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual bool Match(WeakReference<object?> reference) public virtual bool Match(WeakReference<object?> reference)
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);
@ -32,7 +32,6 @@ namespace Avalonia.Data.Core.Plugins
/// <returns> /// <returns>
/// An observable that produces the output for the value. /// An observable that produces the output for the value.
/// </returns> /// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual IObservable<object?> Start(WeakReference<object?> reference) public virtual IObservable<object?> Start(WeakReference<object?> reference)
{ {
reference.TryGetTarget(out var target); reference.TryGetTarget(out var target);

49
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin`1.cs

@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
using Avalonia.Reactive;
namespace Avalonia.Data.Core.Plugins;
internal class TaskStreamPlugin<T> : IStreamPlugin
{
public bool Match(WeakReference<object?> reference)
{
return reference.TryGetTarget(out var target) && target is Task<T>;
}
public IObservable<object?> Start(WeakReference<object?> reference)
{
if (!(reference.TryGetTarget(out var target) && target is Task<T> task))
{
return Observable.Empty<object?>();
}
switch (task.Status)
{
case TaskStatus.RanToCompletion:
case TaskStatus.Faulted:
return HandleCompleted(task);
default:
var subject = new LightweightSubject<object?>();
task.ContinueWith(
_ => HandleCompleted(task).Subscribe(subject),
TaskScheduler.FromCurrentSynchronizationContext())
.ConfigureAwait(false);
return subject;
}
}
private static IObservable<object?> HandleCompleted(Task<T> task)
{
switch (task.Status)
{
case TaskStatus.RanToCompletion:
return Observable.Return((object?)task.Result);
case TaskStatus.Faulted:
return Observable.Return(new BindingNotification(task.Exception!, BindingErrorType.Error));
default:
throw new AvaloniaInternalException("HandleCompleted called for non-completed Task.");
}
}
}

103
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -1,103 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class PropertyAccessorNode : SettableNode
{
private readonly bool _enableValidation;
private readonly IPropertyAccessorPlugin? _customPlugin;
private IPropertyAccessor? _accessor;
public PropertyAccessorNode(string propertyName, bool enableValidation)
{
PropertyName = propertyName;
_enableValidation = enableValidation;
}
public PropertyAccessorNode(string propertyName, bool enableValidation, IPropertyAccessorPlugin customPlugin)
{
PropertyName = propertyName;
_enableValidation = enableValidation;
_customPlugin = customPlugin;
}
public override string Description => PropertyName;
public string PropertyName { get; }
public override Type? PropertyType => _accessor?.PropertyType;
protected override bool SetTargetValueCore(object? value, BindingPriority priority)
{
if (_accessor != null)
{
try
{
return _accessor.SetValue(value, priority);
}
catch { }
}
return false;
}
protected override void StartListeningCore(WeakReference<object?> reference)
{
if (!reference.TryGetTarget(out var target) || target is null)
return;
var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(target);
var accessor = plugin?.Start(reference, PropertyName);
// We need to handle accessor fallback before handling validation. Validators do not support null accessors.
if (accessor == null)
{
reference.TryGetTarget(out var instance);
var message = $"Could not find a matching property accessor for '{PropertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
accessor = new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
if (_enableValidation && Next == null)
{
foreach (var validator in ExpressionObserver.DataValidators)
{
if (validator.Match(reference, PropertyName))
{
accessor = validator.Start(reference, PropertyName, accessor);
}
}
}
if (accessor is null)
{
throw new AvaloniaInternalException("Data validators must return non-null accessor.");
}
_accessor = accessor;
accessor.Subscribe(ValueChanged);
}
private IPropertyAccessorPlugin? GetPropertyAccessorPluginForObject(object target)
{
foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
{
if (x.Match(target, PropertyName))
{
return x;
}
}
return null;
}
protected override void StopListeningCore()
{
_accessor?.Dispose();
_accessor = null;
}
}
}

53
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -1,53 +0,0 @@
using System;
namespace Avalonia.Data.Core
{
internal abstract class SettableNode : ExpressionNode
{
public bool SetTargetValue(object? value, BindingPriority priority)
{
if (ShouldNotSet(value))
{
return true;
}
return SetTargetValueCore(value, priority);
}
private bool ShouldNotSet(object? value)
{
var propertyType = PropertyType;
if (propertyType == null)
{
return false;
}
if (LastValue == null)
{
return false;
}
bool isLastValueAlive = LastValue.TryGetTarget(out var lastValue);
if (!isLastValueAlive)
{
if (value == null && LastValue == NullReference)
{
return true;
}
return false;
}
if (propertyType.IsValueType)
{
return Equals(lastValue, value);
}
return ReferenceEquals(lastValue, value);
}
protected abstract bool SetTargetValueCore(object? value, BindingPriority priority);
public abstract Type? PropertyType { get; }
}
}

56
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -1,56 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class StreamNode : ExpressionNode
{
private readonly IStreamPlugin? _customPlugin = null;
private IDisposable? _subscription;
public override string Description => "^";
public StreamNode() { }
public StreamNode(IStreamPlugin customPlugin)
{
_customPlugin = customPlugin;
}
protected override void StartListeningCore(WeakReference<object?> reference)
{
_subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
private IStreamPlugin? GetPlugin(WeakReference<object?> reference)
{
if (_customPlugin != null)
{
return _customPlugin;
}
foreach (var plugin in ExpressionObserver.StreamHandlers)
{
if (plugin.Match(reference))
{
return plugin;
}
}
// TODO: Improve error
ValueChanged(new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", Description),
BindingErrorType.Error));
return null;
}
}
}

127
src/Avalonia.Base/Data/Core/TargetTypeConverter.cs

@ -0,0 +1,127 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Data.Converters;
using System.Windows.Input;
using Avalonia.Utilities;
using static Avalonia.Utilities.TypeUtilities;
namespace Avalonia.Data.Core;
internal abstract class TargetTypeConverter
{
private static TargetTypeConverter? s_default;
private static TargetTypeConverter? s_reflection;
public static TargetTypeConverter GetDefaultConverter() => s_default ??= new DefaultConverter();
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
public static TargetTypeConverter GetReflectionConverter() => s_reflection ??= new ReflectionConverter();
public abstract bool TryConvert(object? value, Type type, CultureInfo culture, out object? result);
private class DefaultConverter : TargetTypeConverter
{
public override bool TryConvert(object? value, Type type, CultureInfo culture, out object? result)
{
if (value?.GetType() == type)
{
result = value;
return true;
}
var t = Nullable.GetUnderlyingType(type) ?? type;
if (value is null)
{
result = null;
return !t.IsValueType || t != type;
}
if (value == AvaloniaProperty.UnsetValue)
{
// Here the behavior is different from the ReflectionConverter: there isn't any way
// to create the default value for a type without using reflection, so we have to report
// that we can't convert the value.
result = null;
return false;
}
if (t.IsAssignableFrom(value.GetType()))
{
result = value;
return true;
}
if (t == typeof(string))
{
result = value.ToString();
return true;
}
if (t.IsEnum && t.GetEnumUnderlyingType() == value.GetType())
{
result = Enum.ToObject(t, value);
return true;
}
if (value is IConvertible convertible)
{
try
{
result = convertible.ToType(t, culture);
return true;
}
catch
{
result = null;
return false;
}
}
// TODO: This requires reflection: we probably need to make compiled bindings emit
// conversion code at compile-time.
if (FindTypeConversionOperatorMethod(
value.GetType(),
t,
OperatorType.Implicit | OperatorType.Explicit) is { } cast)
{
result = cast.Invoke(null, new[] { value });
return true;
}
result = null;
return false;
}
}
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
private class ReflectionConverter : TargetTypeConverter
{
public override bool TryConvert(object? value, Type type, CultureInfo culture, out object? result)
{
if (value?.GetType() == type)
{
result = value;
return true;
}
else if (value == AvaloniaProperty.UnsetValue)
{
result = Activator.CreateInstance(type);
return true;
}
else if (typeof(ICommand).IsAssignableFrom(type) &&
value is Delegate d &&
!d.Method.IsPrivate &&
d.Method.GetParameters().Length <= 1)
{
result = new MethodToCommandConverter(d);
return true;
}
else
{
return TypeUtilities.TryConvert(type, value, culture, out result);
}
}
}
}

34
src/Avalonia.Base/Data/Core/TypeCastNode.cs

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Data.Core
{
internal class TypeCastNode : ExpressionNode
{
public override string Description => $"as {TargetType.FullName}";
public Type TargetType { get; }
public TypeCastNode(Type type)
{
TargetType = type;
}
protected virtual object? Cast(object? value)
{
return TargetType.IsInstanceOfType(value) ? value : null;
}
protected override void StartListeningCore(WeakReference<object?> reference)
{
if (reference.TryGetTarget(out var target))
{
target = Cast(target);
reference = target == null ? NullReference : new WeakReference<object?>(target);
}
base.StartListeningCore(reference);
}
}
}

600
src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs

@ -0,0 +1,600 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Logging;
using Avalonia.Metadata;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Data.Core;
/// <summary>
/// Base class for binding expressions which produce untyped values.
/// </summary>
[PrivateApi]
public abstract class UntypedBindingExpressionBase : BindingExpressionBase,
IDisposable,
IDescription,
IValueEntry
{
protected static readonly object UnchangedValue = new();
private readonly bool _isDataValidationEnabled;
private object? _defaultValue;
private BindingError? _error;
private ImmediateValueFrame? _frame;
private bool _isDefaultValueInitialized;
private bool _isRunning;
private bool _produceValue;
private IBindingExpressionSink? _sink;
private WeakReference<AvaloniaObject?>? _target;
private object? _value = AvaloniaProperty.UnsetValue;
/// <summary>
/// Initializes a new instance of the <see cref="UntypedBindingExpressionBase"/> class.
/// </summary>
/// <param name="defaultPriority">
/// The default binding priority for the expression.
/// </param>
/// <param name="isDataValidationEnabled">Whether data validation is enabled.</param>
public UntypedBindingExpressionBase(
BindingPriority defaultPriority,
bool isDataValidationEnabled = false)
{
Priority = defaultPriority;
_isDataValidationEnabled = isDataValidationEnabled;
}
/// <summary>
/// Gets a description of the binding expression.
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets the current error state of the binding expression.
/// </summary>
public BindingErrorType ErrorType => _error?.ErrorType ?? BindingErrorType.None;
/// <summary>
/// Gets a value indicating whether data validation is enabled for the binding expression.
/// </summary>
public bool IsDataValidationEnabled => _isDataValidationEnabled;
/// <summary>
/// Gets a value indicating whether the binding expression is currently running.
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// Gets the priority of the binding expression.
/// </summary>
/// <remarks>
/// Before being attached to a value store, this property describes the default priority of the
/// binding expression; this may change when the expression is attached to a value store.
/// </remarks>
public BindingPriority Priority { get; private set; }
/// <summary>
/// Gets the <see cref="AvaloniaProperty"/> which the binding expression is targeting.
/// </summary>
public AvaloniaProperty? TargetProperty { get; private set; }
/// <summary>
/// Gets the target type of the binding expression; that is, the type that values produced by
/// the expression should be converted to.
/// </summary>
public Type TargetType { get; private set; } = typeof(object);
AvaloniaProperty IValueEntry.Property => TargetProperty ?? throw new Exception();
/// <summary>
/// Terminates the binding.
/// </summary>
public override void Dispose()
{
if (_sink is null)
return;
Stop();
var sink = _sink;
var frame = _frame;
_sink = null;
_frame = null;
sink.OnCompleted(this);
frame?.OnEntryDisposed(this);
}
/// <summary>
/// Gets the current value of the binding expression.
/// </summary>
/// <returns>
/// The current value or <see cref="AvaloniaProperty.UnsetValue"/> if the binding was unable
/// to read a value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The binding expression has not been started.
/// </exception>
public object? GetValue()
{
if (!IsRunning)
throw new InvalidOperationException("BindingExpression has not been started.");
return _value;
}
/// <summary>
/// Gets the current value of the binding expression or the default value for the target property.
/// </summary>
/// <returns>
/// The current value or the target property default.
/// </returns>
public object? GetValueOrDefault()
{
var result = GetValue();
if (result == AvaloniaProperty.UnsetValue)
result = GetCachedDefaultValue();
return result;
}
/// <summary>
/// Starts the binding expression following a call to
/// <see cref="AttachCore(IBindingExpressionSink, AvaloniaObject, AvaloniaProperty, BindingPriority)"/>.
/// </summary>
public void Start() => Start(produceValue: true);
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
{
if (_error is not null)
{
state = _error.ErrorType switch
{
BindingErrorType.Error => BindingValueType.BindingError,
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
_ => throw new InvalidOperationException("Invalid BindingErrorType."),
};
error = _error.Exception;
}
else
{
state = BindingValueType.Value;
error = null;
}
return IsDataValidationEnabled;
}
bool IValueEntry.HasValue()
{
Start(produceValue: false);
return true;
}
object? IValueEntry.GetValue()
{
Start(produceValue: false);
return GetValueOrDefault();
}
void IValueEntry.Unsubscribe() => Stop();
internal override void Attach(
ValueStore valueStore,
ImmediateValueFrame? frame,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
AttachCore(valueStore, frame, target, targetProperty, priority);
}
/// <summary>
/// Initializes the binding expression with the specified subscriber and target property and
/// starts it.
/// </summary>
/// <param name="subscriber">The subscriber.</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 void AttachAndStart(
IBindingExpressionSink subscriber,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
AttachCore(subscriber, null, target, targetProperty, priority);
Start(produceValue: true);
}
/// <summary>
/// Produces an observable which can be used to observe the value of the binding expression.
/// </summary>
/// <param name="target">The binding target, if known.</param>
/// <returns>An observable subject.</returns>
/// <exception cref="InvalidOperationException">
/// The binding expression is already instantiated on an AvaloniaObject.
/// </exception>
/// <remarks>
/// This method is mostly here for backwards compatibility with <see cref="InstancedBinding"/>
/// and unit testing and we may want to remove it in future. In particular its usefulness in
/// terms of unit testing is limited in that it preserves the semantics of binding expressions
/// as expected by unit tests, not necessarily the semantics that will be used when the
/// expression is used as an <see cref="IValueEntry"/> instantiated in a
/// <see cref="ValueStore"/>. Unit tests should be migrated to not test the behaviour of
/// binding expressions through an observable, and instead test the behaviour of the binding
/// when applied to an <see cref="AvaloniaObject"/>.
///
/// A binding expression may only act as an observable or as a binding expression targeting an
/// AvaloniaObject, not both.
/// </remarks>
internal IAvaloniaSubject<object?> ToObservable(AvaloniaObject? target = null)
{
if (_sink is ObservableSink s)
return s;
if (_sink is not null)
throw new InvalidOperationException(
"Cannot call AsObservable on a to binding expression which is already " +
"instantiated on an AvaloniaObject.");
var o = new ObservableSink(this);
_sink = o;
_target = target is not null ? new(target) : null;
return o;
}
/// <summary>
/// When overridden in a derived class, writes the specified value to the binding source if
/// possible.
/// </summary>
/// <param name="value">The value to write.</param>
/// <returns>
/// True if the value could be written to the binding source; otherwise false.
/// </returns>
internal virtual bool WriteValueToSource(object? value) => false;
private void AttachCore(
IBindingExpressionSink sink,
ImmediateValueFrame? frame,
AvaloniaObject target,
AvaloniaProperty targetProperty,
BindingPriority priority)
{
if (_sink is not null)
throw new InvalidOperationException("BindingExpression was already attached.");
_sink = sink;
_frame = frame;
_target = new(target);
TargetProperty = targetProperty;
TargetType = targetProperty.PropertyType;
Priority = priority;
}
/// <summary>
/// Converts a value using a value converter, logging a warning if necessary.
/// </summary>
/// <param name="converter">The value converter.</param>
/// <param name="converterCulture">The culture to use for the conversion.</param>
/// <param name="converterParameter">The converter parameter.</param>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The target type to convert to.</param>
/// <param name="error">The current error state.</param>
/// <returns>
/// The converted value, or <see cref="AvaloniaProperty.UnsetValue"/> if an error occurred;
/// in which case the error state will logged and updated in <paramref name="error"/>.
/// </returns>
private protected object? Convert(
IValueConverter converter,
CultureInfo? converterCulture,
object? converterParameter,
object? value,
Type targetType,
ref BindingError? error)
{
try
{
return converter.Convert(
value,
targetType,
converterParameter,
converterCulture ?? CultureInfo.CurrentCulture);
}
catch (Exception e)
{
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{targetType}' using '{converter}'";
if (ShouldLogError(out var target))
Log(target, $"{message}: {e.Message}", LogEventLevel.Warning);
error = new(new InvalidCastException(message + '.', e), BindingErrorType.Error);
return AvaloniaProperty.UnsetValue;
}
}
/// <summary>
/// Converts a value using a value converter's ConvertBack method, logging a warning if
/// necessary.
/// </summary>
/// <param name="converter">The value converter.</param>
/// <param name="converterCulture">The culture to use for the conversion.</param>
/// <param name="converterParameter">The converter parameter.</param>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The target type to convert to.</param>
/// <returns>
/// The converted value, or <see cref="AvaloniaProperty.UnsetValue"/> if an error occurred;
/// in which case the error will be logged.
/// </returns>
protected object? ConvertBack(
IValueConverter converter,
CultureInfo? converterCulture,
object? converterParameter,
object? value,
Type targetType)
{
try
{
return converter.ConvertBack(
value,
targetType,
converterParameter,
converterCulture ?? CultureInfo.CurrentCulture);
}
catch (Exception e)
{
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{targetType}' using '{converter}'";
if (ShouldLogError(out var target))
Log(target, $"{message}: {e.Message}", LogEventLevel.Warning);
return AvaloniaProperty.UnsetValue;
}
}
/// <summary>
/// Logs a binding error.
/// </summary>
/// <param name="error">The error message.</param>
/// <param name="level">The log level.</param>
protected void Log(string error, LogEventLevel level = LogEventLevel.Warning)
{
if (!TryGetTarget(out var target))
return;
if (!Logger.TryGet(level, LogArea.Binding, out var log))
return;
log.Log(
target,
"An error occurred binding {Property} to {Expression}: {Message}",
(object?)TargetProperty ?? "(unknown)",
Description,
error);
}
/// <summary>
/// Logs a binding error.
/// </summary>
/// <param name="target">The target of the binding expression.</param>
/// <param name="error">The error message.</param>
/// <param name="level">The log level.</param>
protected void Log(AvaloniaObject target, string error, LogEventLevel level = LogEventLevel.Warning)
{
if (!Logger.TryGet(level, LogArea.Binding, out var log))
return;
log.Log(
target,
"An error occurred binding {Property} to {Expression}: {Message}",
(object?)TargetProperty ?? "(unknown)",
Description,
error);
}
/// <summary>
/// Publishes a new value and/or error state to the target.
/// </summary>
/// <param name="value">The new value, or <see cref="UnchangedValue"/>.</param>
/// <param name="error">The new binding or data validation error.</param>
private protected void PublishValue(object? value, BindingError? error = null)
{
// When binding to DataContext and the expression results in a binding error, the binding
// expression should produce null rather than UnsetValue in order to not propagate
// incorrect DataContexts from parent controls while things are being set up.
if (TargetProperty == StyledElement.DataContextProperty &&
value == AvaloniaProperty.UnsetValue &&
error?.ErrorType == BindingErrorType.Error)
{
value = null;
}
var hasValueChanged = value != UnchangedValue && !TypeUtilities.IdentityEquals(value, GetValue(), TargetType);
var hasErrorChanged = error is not null || _error is not null;
if (hasValueChanged)
_value = value;
_error = error;
if (!_produceValue || _sink is null)
return;
if (Dispatcher.UIThread.CheckAccess())
{
_sink.OnChanged(this, hasValueChanged, hasErrorChanged, GetValueOrDefault(), _error);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var sink = _sink;
var vc = hasValueChanged;
var ec = hasErrorChanged;
var v = GetValueOrDefault();
var e = _error;
Dispatcher.UIThread.Post(() => sink.OnChanged(this, vc, ec, v, e));
}
}
/// <summary>
/// Gets a value indicating whether an error should be logged given the current state of the
/// binding expression.
/// </summary>
/// <param name="target">
/// When the method returns, contains the target object, if it is available.
/// </param>
/// <returns>True if an error should be logged; otherwise false.</returns>
protected virtual bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target)
{
return TryGetTarget(out target);
}
/// <summary>
/// Starts the binding expression by calling <see cref="StartCore"/>.
/// </summary>
/// <param name="produceValue">
/// Indicates whether the binding expression should produce an initial value.
/// </param>
protected void Start(bool produceValue)
{
if (_isRunning)
return;
_isRunning = true;
try
{
_produceValue = produceValue;
StartCore();
}
finally
{
_produceValue = true;
}
}
/// <summary>
/// When overridden in a derived class, starts the binding expression.
/// </summary>
/// <remarks>
/// This method should not be called directly; instead call <see cref="Start(bool)"/>.
/// </remarks>
protected abstract void StartCore();
/// <summary>
/// Stops the binding expression by calling <see cref="StopCore"/>.
/// </summary>
protected void Stop()
{
if (!_isRunning)
return;
StopCore();
_isRunning = false;
_value = AvaloniaProperty.UnsetValue;
}
/// <summary>
/// When overridden in a derived class, stops the binding expression.
/// </summary>
/// <remarks>
/// This method should not be called directly; instead call <see cref="Stop"/>.
/// </remarks>
protected abstract void StopCore();
/// <summary>
/// Tries to retrieve the target for the binding expression.
/// </summary>
/// <param name="target">
/// When this method returns, contains the target object, if it is available.
/// </param>
/// <returns>true if the target was retrieved; otherwise, false.</returns>
protected bool TryGetTarget([NotNullWhen(true)] out AvaloniaObject? target)
{
if (_target is not null)
return _target.TryGetTarget(out target);
target = null!;
return false;
}
private object? GetCachedDefaultValue()
{
if (_isDefaultValueInitialized == true)
return _defaultValue;
if (TargetProperty is not null && _target?.TryGetTarget(out var target) == true)
{
if (TargetProperty.IsDirect)
_defaultValue = ((IDirectPropertyAccessor)TargetProperty).GetUnsetValue(target.GetType());
else
_defaultValue = ((IStyledPropertyAccessor)TargetProperty).GetDefaultValue(target.GetType());
_isDefaultValueInitialized = true;
return _defaultValue;
}
return AvaloniaProperty.UnsetValue;
}
private sealed class ObservableSink : LightweightObservableBase<object?>,
IBindingExpressionSink,
IAvaloniaSubject<object?>
{
private readonly UntypedBindingExpressionBase _expression;
private object? _value = AvaloniaProperty.UnsetValue;
public ObservableSink(UntypedBindingExpressionBase expression) => _expression = expression;
void IBindingExpressionSink.OnChanged(
UntypedBindingExpressionBase instance,
bool hasValueChanged,
bool hasErrorChanged,
object? value,
BindingError? error)
{
if (instance.IsDataValidationEnabled || error is not null)
{
BindingNotification notification;
if (error?.ErrorType == BindingErrorType.Error)
notification = new(error.Exception, BindingErrorType.Error, value);
else if (error?.ErrorType == BindingErrorType.DataValidationError)
notification = new(error.Exception, BindingErrorType.DataValidationError, value);
else
notification = new(value);
PublishNext(notification);
}
else if (hasValueChanged)
{
PublishNext(value);
}
}
void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance) => PublishCompleted();
void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => _expression.WriteValueToSource(value);
protected override void Initialize() => _expression.Start(produceValue: true);
protected override void Deinitialize() => _expression.Stop();
protected override void Subscribed(IObserver<object> observer, bool first)
{
if (!first && _value != AvaloniaProperty.UnsetValue)
base.PublishNext(_value);
}
private new void PublishNext(object? value)
{
_value = value;
base.PublishNext(value);
}
}
}

34
src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs

@ -0,0 +1,34 @@
using System;
namespace Avalonia.Data.Core;
internal class UntypedObservableBindingExpression : UntypedBindingExpressionBase, IObserver<object?>
{
private readonly IObservable<object?> _observable;
private IDisposable? _subscription;
public UntypedObservableBindingExpression(
IObservable<object?> observable,
BindingPriority priority)
: base(priority)
{
_observable = observable;
}
public override string Description => "Observable";
protected override void StartCore()
{
_subscription = _observable.Subscribe(this);
}
protected override void StopCore()
{
_subscription?.Dispose();
_subscription = null;
}
void IObserver<object?>.OnCompleted() { }
void IObserver<object?>.OnError(Exception error) { }
void IObserver<object?>.OnNext(object? value) => PublishValue(value);
}

0
src/Markup/Avalonia.Markup/Data/CultureInfoIetfLanguageTagConverter.cs → src/Avalonia.Base/Data/CultureInfoIetfLanguageTagConverter.cs

3
src/Avalonia.Base/Data/IBinding.cs

@ -1,3 +1,5 @@
using System;
using Avalonia.Diagnostics;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Data namespace Avalonia.Data
@ -23,6 +25,7 @@ namespace Avalonia.Data
/// <returns> /// <returns>
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved. /// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
/// </returns> /// </returns>
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
InstancedBinding? Initiate( InstancedBinding? Initiate(
AvaloniaObject target, AvaloniaObject target,
AvaloniaProperty? targetProperty, AvaloniaProperty? targetProperty,

18
src/Avalonia.Base/Data/IndexerBinding.cs

@ -1,8 +1,10 @@
using Avalonia.Reactive; using System;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
namespace Avalonia.Data namespace Avalonia.Data
{ {
internal class IndexerBinding : IBinding internal class IndexerBinding : IBinding2
{ {
public IndexerBinding( public IndexerBinding(
AvaloniaObject source, AvaloniaObject source,
@ -18,16 +20,20 @@ namespace Avalonia.Data
public AvaloniaProperty Property { get; } public AvaloniaProperty Property { get; }
private BindingMode Mode { get; } private BindingMode Mode { get; }
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public InstancedBinding? Initiate( public InstancedBinding? Initiate(
AvaloniaObject target, AvaloniaObject target,
AvaloniaProperty? targetProperty, AvaloniaProperty? targetProperty,
object? anchor = null, object? anchor = null,
bool enableDataValidation = false) bool enableDataValidation = false)
{ {
var subject = new CombinedSubject<object?>( var expression = new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
new AnonymousObserver<object?>(x => Source.SetValue(Property, x, BindingPriority.LocalValue)), return new InstancedBinding(expression, Mode, BindingPriority.LocalValue);
Source.GetObservable(Property)); }
return new InstancedBinding(subject, Mode, BindingPriority.LocalValue);
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty, object? anchor)
{
return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
} }
} }
} }

33
src/Avalonia.Base/Data/InstancedBinding.cs

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Data.Core;
using Avalonia.Reactive; using Avalonia.Reactive;
using ObservableEx = Avalonia.Reactive.Observable; using ObservableEx = Avalonia.Reactive.Observable;
@ -16,6 +17,10 @@ namespace Avalonia.Data
/// </remarks> /// </remarks>
public sealed class InstancedBinding public sealed class InstancedBinding
{ {
private readonly AvaloniaObject? _target;
private readonly UntypedBindingExpressionBase? _expression;
private IObservable<object?>? _observable;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class. /// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary> /// </summary>
@ -32,7 +37,29 @@ namespace Avalonia.Data
{ {
Mode = mode; Mode = mode;
Priority = priority; Priority = priority;
Source = source ?? throw new ArgumentNullException(nameof(source)); _observable = source ?? throw new ArgumentNullException(nameof(source));
}
internal InstancedBinding(
UntypedBindingExpressionBase source,
BindingMode mode,
BindingPriority priority)
{
Mode = mode;
Priority = priority;
_expression = source ?? throw new ArgumentNullException(nameof(source));
}
internal InstancedBinding(
AvaloniaObject? target,
UntypedBindingExpressionBase source,
BindingMode mode,
BindingPriority priority)
{
Mode = mode;
Priority = priority;
_expression = source ?? throw new ArgumentNullException(nameof(source));
_target = target;
} }
/// <summary> /// <summary>
@ -48,11 +75,13 @@ namespace Avalonia.Data
/// <summary> /// <summary>
/// Gets the binding source observable. /// Gets the binding source observable.
/// </summary> /// </summary>
public IObservable<object?> Source { get; } public IObservable<object?> Source => _observable ??= _expression!.ToObservable(_target);
[Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)]
public IObservable<object?> Observable => Source; public IObservable<object?> Observable => Source;
internal UntypedBindingExpressionBase? Expression => _expression;
/// <summary> /// <summary>
/// Creates a new one-time binding with a fixed value. /// Creates a new one-time binding with a fixed value.
/// </summary> /// </summary>

21
src/Avalonia.Base/Data/TemplateBinding.Observable.cs

@ -0,0 +1,21 @@
using System;
using Avalonia.Reactive;
namespace Avalonia.Data
{
// TODO12: Remove IAvaloniaSubject<object?> support from TemplateBinding.
public partial class TemplateBinding : IAvaloniaSubject<object?>
{
private IAvaloniaSubject<object?>? _observableAdapter;
public IDisposable Subscribe(IObserver<object?> observer)
{
_observableAdapter ??= ToObservable();
return _observableAdapter.Subscribe(observer);
}
void IObserver<object?>.OnCompleted() => _observableAdapter?.OnCompleted();
void IObserver<object?>.OnError(Exception error) => _observableAdapter?.OnError(error);
void IObserver<object?>.OnNext(object? value) => _observableAdapter?.OnNext(value);
}
}

260
src/Avalonia.Base/Data/TemplateBinding.cs

@ -1,101 +1,52 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Reactive; using Avalonia.Data.Core;
using Avalonia.Logging;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading;
namespace Avalonia.Data namespace Avalonia.Data
{ {
/// <summary> /// <summary>
/// A XAML binding to a property on a control's templated parent. /// A XAML binding to a property on a control's templated parent.
/// </summary> /// </summary>
public class TemplateBinding : IObservable<object?>, public partial class TemplateBinding : UntypedBindingExpressionBase,
IBinding, IBinding,
IBinding2,
IDescription, IDescription,
IAvaloniaSubject<object?>,
ISetterValue, ISetterValue,
IDisposable IDisposable
{ {
private IObserver<object?>? _observer;
private bool _isSetterValue; private bool _isSetterValue;
private StyledElement? _target;
private Type? _targetType;
private bool _hasProducedValue;
public TemplateBinding() public TemplateBinding()
: base(BindingPriority.Template)
{ {
} }
public TemplateBinding(AvaloniaProperty property) public TemplateBinding(AvaloniaProperty property)
: base(BindingPriority.Template)
{ {
Property = property; Property = property;
} }
public IDisposable Subscribe(IObserver<object?> observer)
{
_ = observer ?? throw new ArgumentNullException(nameof(observer));
Dispatcher.UIThread.VerifyAccess();
if (_observer != null)
{
throw new InvalidOperationException("The observable can only be subscribed once.");
}
_observer = observer;
Subscribed();
return this;
}
public virtual void Dispose()
{
Unsubscribed();
_observer = null;
}
/// <inheritdoc/>
public InstancedBinding? Initiate(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor = null,
bool enableDataValidation = false)
{
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
// use the `TemplateBinding` object itself as the instanced binding in order to save
// allocating a new object.
//
// If the binding appears in a `Setter`, then make a clone and instantiate that because
// because the setter can outlive the control and cause a leak.
if (_target is null && !_isSetterValue)
{
_target = (StyledElement)target;
_targetType = targetProperty?.PropertyType;
return new InstancedBinding(
this,
Mode == BindingMode.Default ? BindingMode.OneWay : Mode,
BindingPriority.Template);
}
else
{
var clone = new TemplateBinding
{
Converter = Converter,
ConverterParameter = ConverterParameter,
Property = Property,
Mode = Mode,
};
return clone.Initiate(target, targetProperty, anchor, enableDataValidation);
}
}
/// <summary> /// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use. /// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary> /// </summary>
public IValueConverter? Converter { get; set; } public IValueConverter? Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
/// <value>The default value is null.</value>
/// <remarks>
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
/// </remarks>
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
public CultureInfo? ConverterCulture { get; set; }
/// <summary> /// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>. /// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary> /// </summary>
@ -104,7 +55,11 @@ namespace Avalonia.Data
/// <summary> /// <summary>
/// Gets or sets the binding mode. /// Gets or sets the binding mode.
/// </summary> /// </summary>
public BindingMode Mode { get; set; } public new BindingMode Mode
{
get => base.Mode;
set => base.Mode = value;
}
/// <summary> /// <summary>
/// Gets or sets the name of the source property on the templated parent. /// Gets or sets the name of the source property on the templated parent.
@ -112,108 +67,187 @@ namespace Avalonia.Data
public AvaloniaProperty? Property { get; set; } public AvaloniaProperty? Property { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public string Description => "TemplateBinding: " + Property; public override string Description => "TemplateBinding: " + Property;
void IObserver<object?>.OnCompleted() => throw new NotImplementedException(); public IBinding ProvideValue() => this;
void IObserver<object?>.OnError(Exception error) => throw new NotImplementedException();
void IObserver<object?>.OnNext(object? value) public InstancedBinding? Initiate(
{ AvaloniaObject target,
if (_target?.TemplatedParent is { } templatedParent && Property is not null) AvaloniaProperty? targetProperty,
object? anchor = null,
bool enableDataValidation = false)
{ {
if (Converter is not null) return new(target, InstanceCore(), Mode, BindingPriority.Template);
}
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
{ {
value = Converter.ConvertBack( return InstanceCore();
value,
Property.PropertyType,
ConverterParameter,
CultureInfo.CurrentCulture);
} }
internal override bool WriteValueToSource(object? value)
{
if (Property is not null && TryGetTemplatedParent(out var templatedParent))
{
if (Converter is not null)
value = ConvertBack(Converter, ConverterCulture, ConverterParameter, value, TargetType);
if (value != BindingOperations.DoNothing)
templatedParent.SetCurrentValue(Property, value); templatedParent.SetCurrentValue(Property, value);
return true;
} }
return false;
} }
/// <inheritdoc/> /// <inheritdoc/>
void ISetterValue.Initialize(SetterBase setter) => _isSetterValue = true; void ISetterValue.Initialize(SetterBase setter) => _isSetterValue = true;
private void Subscribed() protected override void StartCore()
{ {
TemplatedParentChanged(); OnTemplatedParentChanged();
if (TryGetTarget(out var target))
target.PropertyChanged += OnTargetPropertyChanged;
}
if (_target is not null) protected override void StopCore()
{
if (TryGetTarget(out var target))
{ {
_target.PropertyChanged += TargetPropertyChanged; if (target is StyledElement targetElement &&
targetElement?.TemplatedParent is { } templatedParent)
{
templatedParent.PropertyChanged -= OnTemplatedParentPropertyChanged;
}
if (target is not null)
{
target.PropertyChanged -= OnTargetPropertyChanged;
}
} }
} }
private void Unsubscribed() private object? ConvertToTargetType(object? value)
{ {
if (_target?.TemplatedParent is { } templatedParent) var converter = TargetTypeConverter.GetDefaultConverter();
if (converter.TryConvert(value, TargetType, CultureInfo.InvariantCulture, out var result))
{ {
templatedParent.PropertyChanged -= TemplatedParentPropertyChanged; return result;
}
else
{
if (TryGetTarget(out var target))
{
var valueString = value?.ToString() ?? "(null)";
var valueTypeName = value?.GetType().FullName ?? "null";
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'.";
Log(target, message, LogEventLevel.Warning);
}
return AvaloniaProperty.UnsetValue;
}
} }
if (_target is not null) private TemplateBinding InstanceCore()
{
if (Mode is BindingMode.OneTime or BindingMode.OneWayToSource)
throw new NotSupportedException("TemplateBinding does not support OneTime or OneWayToSource bindings.");
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
// use the `TemplateBinding` object itself as the binding expression in order to save
// allocating a new object.
//
// If the binding appears in a `Setter`, then make a clone and instantiate that because
// because the setter can outlive the control and cause a leak.
if (!_isSetterValue)
{ {
_target.PropertyChanged -= TargetPropertyChanged; return this;
}
else
{
var clone = new TemplateBinding
{
Converter = Converter,
ConverterCulture = ConverterCulture,
ConverterParameter = ConverterParameter,
Mode = Mode,
Property = Property,
};
return clone;
} }
} }
private void PublishValue() private void PublishValue()
{ {
if (_target?.TemplatedParent is { } templatedParent) if (Mode == BindingMode.OneWayToSource)
return;
if (TryGetTemplatedParent(out var templatedParent))
{ {
var value = Property is not null ? var value = Property is not null ?
templatedParent.GetValue(Property) : templatedParent.GetValue(Property) :
_target.TemplatedParent; templatedParent;
BindingError? error = null;
if (Converter is not null) if (Converter is not null)
{ value = Convert(Converter, ConverterCulture, ConverterParameter, value, TargetType, ref error);
value = Converter.Convert(value, _targetType ?? typeof(object), ConverterParameter, CultureInfo.CurrentCulture);
} value = ConvertToTargetType(value);
PublishValue(value, error);
_observer?.OnNext(value); if (Mode == BindingMode.OneTime)
_hasProducedValue = true; Stop();
} }
else if (_hasProducedValue) else
{ {
_observer?.OnNext(AvaloniaProperty.UnsetValue); PublishValue(AvaloniaProperty.UnsetValue);
_hasProducedValue = false;
} }
} }
private void TemplatedParentChanged() private void OnTemplatedParentChanged()
{ {
if (_target?.TemplatedParent is { } templatedParent) if (TryGetTemplatedParent(out var templatedParent))
{ templatedParent.PropertyChanged += OnTemplatedParentPropertyChanged;
templatedParent.PropertyChanged += TemplatedParentPropertyChanged;
PublishValue();
} }
private void OnTemplatedParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Property)
PublishValue(); PublishValue();
} }
private void TargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (e.Property == StyledElement.TemplatedParentProperty) if (e.Property == StyledElement.TemplatedParentProperty)
{ {
if (e.OldValue is AvaloniaObject oldValue) if (e.OldValue is AvaloniaObject oldValue)
{ oldValue.PropertyChanged -= OnTemplatedParentPropertyChanged;
oldValue.PropertyChanged -= TemplatedParentPropertyChanged;
}
TemplatedParentChanged(); OnTemplatedParentChanged();
}
else if (Mode is BindingMode.TwoWay or BindingMode.OneWayToSource && e.Property == TargetProperty)
{
WriteValueToSource(e.NewValue);
} }
} }
private void TemplatedParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) private bool TryGetTemplatedParent([NotNullWhen(true)] out AvaloniaObject? result)
{ {
if (e.Property == Property) if (TryGetTarget(out var target) &&
target is StyledElement targetElement &&
targetElement.TemplatedParent is { } templatedParent)
{ {
PublishValue(); result = templatedParent;
} return true;
} }
public IBinding ProvideValue() => this; result = null;
return false;
}
} }
} }

29
src/Avalonia.Base/Data/UpdateSourceTrigger.cs

@ -0,0 +1,29 @@
namespace Avalonia.Data;
/// <summary>
/// Describes the timing of binding source updates.
/// </summary>
public enum UpdateSourceTrigger
{
/// <summary>
/// The default <see cref="UpdateSourceTrigger"/> value of the binding target property.
/// This currently defaults to <see cref="PropertyChanged"/>.
/// </summary>
Default,
/// <summary>
/// Updates the binding source immediately whenever the binding target property changes.
/// </summary>
PropertyChanged,
/// <summary>
/// Updates the binding source whenever the binding target element loses focus.
/// </summary>
LostFocus,
/// <summary>
/// Updates the binding source only when you call the
/// <see cref="BindingExpressionBase.UpdateSource()"/> method.
/// </summary>
Explicit,
}

12
src/Avalonia.Base/Diagnostics/ObsoletionMessages.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Diagnostics;
internal static class ObsoletionMessages
{
public const string MayBeRemovedInAvalonia12 = "This API may be removed in Avalonia 12. If you depend on this API, please open an issue with details of your use-case.";
}

7
src/Avalonia.Base/DirectProperty.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data; using Avalonia.Data;
namespace Avalonia namespace Avalonia
@ -142,5 +141,11 @@ namespace Avalonia
Setter((TOwner)instance, (TValue)value!); Setter((TOwner)instance, (TValue)value!);
} }
object? IDirectPropertyAccessor.GetUnsetValue(Type type)
{
var metadata = GetMetadata(type);
return metadata.UnsetValue;
}
} }
} }

7
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
namespace Avalonia namespace Avalonia
@ -157,6 +158,12 @@ namespace Avalonia
return null; return null;
} }
internal override void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value)
{
var bindingValue = BindingValue<TValue>.FromUntypedStrict(value);
o.SetDirectValueUnchecked<TValue>(this, bindingValue);
}
internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) internal override void RouteSetCurrentValue(AvaloniaObject o, object? value)
{ {
RouteSetValue(o, value, BindingPriority.LocalValue); RouteSetValue(o, value, BindingPriority.LocalValue);

6
src/Avalonia.Base/IDirectPropertyAccessor.cs

@ -32,5 +32,11 @@ namespace Avalonia
/// <param name="instance">The instance.</param> /// <param name="instance">The instance.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
void SetValue(AvaloniaObject instance, object? value); void SetValue(AvaloniaObject instance, object? value);
/// <summary>
/// Gets the unset value of the property for the specified type.
/// </summary>
/// <param name="type">The type.</param>
object? GetUnsetValue(Type type);
} }
} }

29
src/Avalonia.Base/Logging/TraceLogSink.cs

@ -80,15 +80,7 @@ namespace Avalonia.Logging
} }
} }
if (source is object) FormatSource(source, result);
{
result.Append(" (");
result.Append(source.GetType().Name);
result.Append(" #");
result.Append(source.GetHashCode());
result.Append(')');
}
return StringBuilderCache.GetStringAndRelease(result); return StringBuilderCache.GetStringAndRelease(result);
} }
@ -132,16 +124,25 @@ namespace Avalonia.Logging
} }
} }
if (source is object) FormatSource(source, result);
return StringBuilderCache.GetStringAndRelease(result);
}
private static void FormatSource(object? source, StringBuilder result)
{ {
result.Append('('); if (source is null)
return;
result.Append(" (");
result.Append(source.GetType().Name); result.Append(source.GetType().Name);
result.Append(" #"); result.Append(" #");
if (source is StyledElement se && se.Name is not null)
result.Append(se.Name);
else
result.Append(source.GetHashCode()); result.Append(source.GetHashCode());
result.Append(')');
}
return StringBuilderCache.GetStringAndRelease(result); result.Append(')');
} }
} }
} }

2
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -1,5 +1,6 @@
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Metadata; using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
@ -33,3 +34,4 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Browser.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Browser.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Browser, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Browser, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

2
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -139,8 +139,6 @@ namespace Avalonia.PropertyStore
var property = instance.Property; var property = instance.Property;
var originalType = value.Type; var originalType = value.Type;
LoggingUtils.LogIfNecessary(owner, property, value);
if (!value.HasValue && value.Type != BindingValueType.DataValidationError) if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
value = value.WithValue(instance.GetCachedDefaultValue()); value = value.WithValue(instance.GetCachedDefaultValue());

12
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -127,6 +127,18 @@ namespace Avalonia.PropertyStore
IValueEntry value, IValueEntry value,
BindingPriority priority); BindingPriority priority);
/// <summary>
/// Sets the value and base value for a LocalValue priority, raising
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being changed.</param>
/// <param name="value">The new value of the property.</param>
public abstract void SetLocalValueAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value);
/// <summary> /// <summary>
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value /// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
/// change. /// change.

18
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -66,6 +66,14 @@ namespace Avalonia.PropertyStore
} }
} }
public override void SetLocalValueAndRaise(
ValueStore owner,
AvaloniaProperty property,
object? value)
{
SetLocalValueAndRaise(owner, (StyledProperty<T>)property, (T)value!);
}
public void SetLocalValueAndRaise( public void SetLocalValueAndRaise(
ValueStore owner, ValueStore owner,
StyledProperty<T> property, StyledProperty<T> property,
@ -138,6 +146,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{ {
var clearDataValidation = ValueEntry?.GetDataValidationState(out _, out _) ??
BaseValueEntry?.GetDataValidationState(out _, out _) ??
false;
ValueEntry?.Unsubscribe(); ValueEntry?.Unsubscribe();
BaseValueEntry?.Unsubscribe(); BaseValueEntry?.Unsubscribe();
@ -163,13 +175,9 @@ namespace Avalonia.PropertyStore
owner.OnInheritedEffectiveValueDisposed(p, Value, newValue); owner.OnInheritedEffectiveValueDisposed(p, Value, newValue);
} }
if (ValueEntry?.GetDataValidationState(out _, out _) ?? if (clearDataValidation)
BaseValueEntry?.GetDataValidationState(out _, out _) ??
false)
{
owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null); owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
} }
}
protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property) protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)
{ {

7
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
@ -14,6 +15,12 @@ namespace Avalonia.PropertyStore
{ {
} }
public IValueEntry AddBinding(UntypedBindingExpressionBase source)
{
Add(source);
return source;
}
public TypedBindingEntry<T> AddBinding<T>( public TypedBindingEntry<T> AddBinding<T>(
StyledProperty<T> property, StyledProperty<T> property,
IObservable<BindingValue<T>> source) IObservable<BindingValue<T>> source)

2
src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs

@ -88,8 +88,6 @@ namespace Avalonia.PropertyStore
var property = instance.Property; var property = instance.Property;
var originalType = value.Type; var originalType = value.Type;
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
// Revert to the default value if the binding value fails validation, or if // Revert to the default value if the binding value fails validation, or if
// there was no value (though not if there was a data validation error). // there was no value (though not if there was a data validation error).
if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) || if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) ||

82
src/Avalonia.Base/PropertyStore/LoggingUtils.cs

@ -1,82 +0,0 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
internal static class LoggingUtils
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary(
AvaloniaObject owner,
AvaloniaProperty property,
BindingNotification value)
{
if (value.ErrorType != BindingErrorType.None)
Log(owner, property, value.Error!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogIfNecessary<T>(
AvaloniaObject owner,
AvaloniaProperty property,
BindingValue<T> value)
{
if (value.HasError)
Log(owner, property, value.Error!);
}
public static void LogInvalidValue(
AvaloniaObject owner,
AvaloniaProperty property,
Type expectedType,
object? value)
{
if (value is not null)
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})",
owner,
property,
expectedType,
value,
value.GetType());
}
else
{
owner.GetBindingWarningLogger(property, null)?.Log(
owner,
"Error in binding to {Target}.{Property}: expected {ExpectedType}, got null",
owner,
property,
expectedType);
}
}
private static void Log(
AvaloniaObject owner,
AvaloniaProperty property,
Exception e)
{
if (e is TargetInvocationException t)
e = t.InnerException!;
if (e is AggregateException a)
{
foreach (var i in a.InnerExceptions)
Log(owner, property, i);
}
else
{
owner.GetBindingWarningLogger(property, e)?.Log(
owner,
"Error in binding to {Target}.{Property}: {Message}",
owner,
property,
e.Message);
}
}
}
}

39
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@ -1,39 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// An <see cref="IValueEntry"/> that holds a binding whose source observable and target
/// property are both untyped.
/// </summary>
internal class UntypedBindingEntry : BindingEntryBase<object?, object?>
{
private readonly Func<object?, bool>? _validate;
public UntypedBindingEntry(
AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable<object?> source)
: base(target, frame, property, source)
{
_validate = ((IStyledPropertyAccessor)property).ValidateValue;
}
protected override BindingValue<object?> ConvertAndValidate(object? value)
{
return UntypedValueUtils.ConvertAndValidate(value, Property.PropertyType, _validate);
}
protected override BindingValue<object?> ConvertAndValidate(BindingValue<object?> value)
{
throw new NotSupportedException();
}
protected override object? GetDefaultValue(Type ownerType)
{
return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue;
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save