Browse Source
* 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
committed by
GitHub
201 changed files with 9488 additions and 7118 deletions
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -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); |
||||
|
} |
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
||||
|
} |
||||
@ -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>
|
||||
|
/// A binding expression which accepts and produces (possibly boxed) object values.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// A <see cref="BindingExpression"/> represents a untyped binding which has been
|
||||
|
/// instantiated on an object.
|
||||
|
/// </remarks>
|
||||
|
internal partial class BindingExpression : UntypedBindingExpressionBase, IDescription, IDisposable |
||||
{ |
{ |
||||
|
private static readonly List<ExpressionNode> s_emptyExpressionNodes = new(); |
||||
|
private readonly WeakReference<object?>? _source; |
||||
|
private readonly BindingMode _mode; |
||||
|
private readonly List<ExpressionNode> _nodes; |
||||
|
private readonly TargetTypeConverter? _targetTypeConverter; |
||||
|
private readonly UncommonFields? _uncommon; |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Binds to an expression on an object using a type value converter to convert the values
|
/// Initializes a new instance of the <see cref="BindingExpression"/> class.
|
||||
/// that are sent and received.
|
|
||||
/// </summary>
|
/// </summary>
|
||||
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)] |
/// <param name="source">The source from which the value will be read.</param>
|
||||
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription |
/// <param name="nodes">The nodes representing the binding path.</param>
|
||||
|
/// <param name="fallbackValue">
|
||||
|
/// The fallback value. Pass <see cref="AvaloniaProperty.UnsetValue"/> for no fallback.
|
||||
|
/// </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="mode">The binding mode.</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( |
||||
|
object? source, |
||||
|
List<ExpressionNode>? nodes, |
||||
|
object? fallbackValue, |
||||
|
IValueConverter? converter = null, |
||||
|
CultureInfo? converterCulture = null, |
||||
|
object? converterParameter = null, |
||||
|
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) |
||||
{ |
{ |
||||
private readonly ExpressionObserver _inner; |
if (mode == BindingMode.Default) |
||||
private readonly Type _targetType; |
throw new ArgumentException("Binding mode cannot be Default.", nameof(mode)); |
||||
private readonly object? _fallbackValue; |
if (updateSourceTrigger == UpdateSourceTrigger.Default) |
||||
private readonly object? _targetNullValue; |
throw new ArgumentException("UpdateSourceTrigger cannot be Default.", nameof(updateSourceTrigger)); |
||||
private readonly BindingPriority _priority; |
|
||||
InnerListener? _innerListener; |
if (source == AvaloniaProperty.UnsetValue) |
||||
WeakReference<object>? _value; |
source = null; |
||||
|
|
||||
/// <summary>
|
_source = new(source); |
||||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
_mode = mode; |
||||
/// </summary>
|
_nodes = nodes ?? s_emptyExpressionNodes; |
||||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
_targetTypeConverter = targetTypeConverter; |
||||
/// <param name="targetType">The type to convert the value to.</param>
|
|
||||
public BindingExpression(ExpressionObserver inner, Type targetType) |
if (converter is not null || |
||||
: this(inner, targetType, DefaultValueConverter.Instance, CultureInfo.InvariantCulture) |
converterCulture is not null || |
||||
|
converterParameter is not null || |
||||
|
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, |
||||
|
}; |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
IPropertyAccessorNode? leafAccessor = null; |
||||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|
||||
/// </summary>
|
if (nodes is not null) |
||||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|
||||
/// <param name="targetType">The type to convert the value to.</param>
|
|
||||
/// <param name="converter">The value converter to use.</param>
|
|
||||
/// <param name="converterCulture">The converter culture to use.</param>
|
|
||||
/// <param name="converterParameter">
|
|
||||
/// A parameter to pass to <paramref name="converter"/>.
|
|
||||
/// </param>
|
|
||||
/// <param name="priority">The binding priority.</param>
|
|
||||
public BindingExpression( |
|
||||
ExpressionObserver inner, |
|
||||
Type targetType, |
|
||||
IValueConverter converter, |
|
||||
CultureInfo converterCulture, |
|
||||
object? converterParameter = null, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
: this( |
|
||||
inner, |
|
||||
targetType, |
|
||||
AvaloniaProperty.UnsetValue, |
|
||||
AvaloniaProperty.UnsetValue, |
|
||||
converter, |
|
||||
converterCulture, |
|
||||
converterParameter, priority) |
|
||||
{ |
{ |
||||
|
for (var i = 0; i < nodes.Count; ++i) |
||||
|
{ |
||||
|
var node = nodes[i]; |
||||
|
node.SetOwner(this, i); |
||||
|
if (node is IPropertyAccessorNode n) |
||||
|
leafAccessor = n; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
if (enableDataValidation) |
||||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
leafAccessor?.EnableDataValidation(); |
||||
/// </summary>
|
} |
||||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|
||||
/// <param name="targetType">The type to convert the value to.</param>
|
public override string Description |
||||
/// <param name="fallbackValue">
|
{ |
||||
/// The value to use when the binding is unable to produce a value.
|
get |
||||
/// </param>
|
|
||||
/// <param name="targetNullValue">
|
|
||||
/// The value to use when the binding result is null.
|
|
||||
/// </param>
|
|
||||
/// <param name="converter">The value converter to use.</param>
|
|
||||
/// <param name="converterCulture">The converter culture to use.</param>
|
|
||||
/// <param name="converterParameter">
|
|
||||
/// A parameter to pass to <paramref name="converter"/>.
|
|
||||
/// </param>
|
|
||||
/// <param name="priority">The binding priority.</param>
|
|
||||
public BindingExpression( |
|
||||
ExpressionObserver inner, |
|
||||
Type targetType, |
|
||||
object? fallbackValue, |
|
||||
object? targetNullValue, |
|
||||
IValueConverter converter, |
|
||||
CultureInfo converterCulture, |
|
||||
object? converterParameter = null, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
{ |
||||
_ = inner ?? throw new ArgumentNullException(nameof(inner)); |
var b = new StringBuilder(); |
||||
_ = targetType ?? throw new ArgumentNullException(nameof(targetType)); |
LeafNode.BuildString(b, _nodes); |
||||
_ = converter ?? throw new ArgumentNullException(nameof(converter)); |
return b.ToString(); |
||||
|
|
||||
_inner = inner; |
|
||||
_targetType = targetType; |
|
||||
Converter = converter; |
|
||||
ConverterCulture = converterCulture; |
|
||||
ConverterParameter = converterParameter; |
|
||||
_fallbackValue = fallbackValue; |
|
||||
_targetNullValue = targetNullValue; |
|
||||
_priority = priority; |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
/// <summary>
|
public Type? SourceType => (LeafNode as ISettableNode)?.ValueType; |
||||
/// Gets the converter to use on the expression.
|
public IValueConverter? Converter => _uncommon?._converter; |
||||
/// </summary>
|
public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture; |
||||
public IValueConverter Converter { get; } |
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; |
||||
|
|
||||
|
public override void UpdateSource() |
||||
|
{ |
||||
|
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource) |
||||
|
WriteTargetValueToSource(); |
||||
|
} |
||||
|
|
||||
/// <summary>
|
public override void UpdateTarget() |
||||
/// Gets or sets the culture in which to evaluate the converter.
|
{ |
||||
/// </summary>
|
if (_nodes.Count == 0) |
||||
public CultureInfo ConverterCulture { get; set; } |
return; |
||||
|
|
||||
/// <summary>
|
var source = _nodes[0].Source; |
||||
/// Gets a parameter to pass to <see cref="Converter"/>.
|
|
||||
/// </summary>
|
|
||||
public object? ConverterParameter { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
for (var i = 0; i < _nodes.Count; ++i) |
||||
string? IDescription.Description => _inner.Expression; |
_nodes[i].SetSource(null, null); |
||||
|
|
||||
/// <inheritdoc/>
|
_nodes[0].SetSource(source, null); |
||||
public void OnCompleted() |
} |
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
/// <summary>
|
||||
public void OnError(Exception error) |
/// Creates an <see cref="BindingExpression"/> from an expression tree.
|
||||
{ |
/// </summary>
|
||||
} |
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
|
||||
|
/// <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()); |
||||
|
} |
||||
|
|
||||
/// <inheritdoc/>
|
/// <summary>
|
||||
public void OnNext(object? value) |
/// 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 (nodeIndex == _nodes.Count - 1) |
||||
{ |
{ |
||||
if (value == BindingOperations.DoNothing) |
// The leaf node has changed. If the binding mode is not OneWayToSource, publish the
|
||||
|
// value to the target.
|
||||
|
if (_mode != BindingMode.OneWayToSource) |
||||
{ |
{ |
||||
return; |
var error = dataValidationError is not null ? |
||||
|
new BindingError(dataValidationError, BindingErrorType.DataValidationError) : |
||||
|
null; |
||||
|
ConvertAndPublishValue(value, error); |
||||
} |
} |
||||
|
|
||||
using (_inner.Subscribe(_ => { })) |
// If the binding mode is OneTime, then stop the binding if a valid value was published.
|
||||
{ |
if (_mode == BindingMode.OneTime && GetValue() != AvaloniaProperty.UnsetValue) |
||||
var type = _inner.ResultType; |
Stop(); |
||||
|
|
||||
if (type != null) |
|
||||
{ |
|
||||
var converted = Converter.ConvertBack( |
|
||||
value, |
|
||||
type, |
|
||||
ConverterParameter, |
|
||||
ConverterCulture); |
|
||||
|
|
||||
if (converted == BindingOperations.DoNothing) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (converted == AvaloniaProperty.UnsetValue) |
|
||||
{ |
|
||||
converted = TypeUtilities.Default(type); |
|
||||
_inner.SetValue(converted, _priority); |
|
||||
} |
|
||||
else if (converted is BindingNotification notification) |
|
||||
{ |
|
||||
if (notification.ErrorType == BindingErrorType.None) |
|
||||
{ |
|
||||
throw new AvaloniaInternalException( |
|
||||
"IValueConverter should not return non-errored BindingNotification."); |
|
||||
} |
|
||||
|
|
||||
PublishNext(notification); |
|
||||
|
|
||||
if (_fallbackValue != AvaloniaProperty.UnsetValue) |
|
||||
{ |
|
||||
if (TypeUtilities.TryConvert( |
|
||||
type, |
|
||||
_fallbackValue, |
|
||||
ConverterCulture, |
|
||||
out converted)) |
|
||||
{ |
|
||||
_inner.SetValue(converted, _priority); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log( |
|
||||
this, |
|
||||
"Could not convert FallbackValue {FallbackValue} to {Type}", |
|
||||
_fallbackValue, |
|
||||
type); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
_inner.SetValue(converted, _priority); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
|
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null) |
||||
protected override void Initialize() => _innerListener = new InnerListener(this); |
|
||||
protected override void Deinitialize() => _innerListener?.Dispose(); |
|
||||
|
|
||||
protected override void Subscribed(IObserver<object> observer, bool first) |
|
||||
{ |
{ |
||||
if (!first && _value != null && _value.TryGetTarget(out var val)) |
// 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
|
||||
observer.OnNext(val); |
// 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) |
||||
/// <inheritdoc/>
|
|
||||
private object? ConvertValue(object? value) |
|
||||
{ |
{ |
||||
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) |
OnNodeError(nodeIndex, "Value is null."); |
||||
{ |
} |
||||
return _targetNullValue; |
else |
||||
} |
{ |
||||
|
_nodes[nodeIndex + 1].SetSource(value, dataValidationError); |
||||
|
} |
||||
|
} |
||||
|
|
||||
if (value == BindingOperations.DoNothing) |
/// <summary>
|
||||
{ |
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when an error occurs
|
||||
return value; |
/// 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) |
||||
|
{ |
||||
|
// 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); |
||||
|
|
||||
var notification = value as BindingNotification; |
if (_mode == BindingMode.OneWayToSource) |
||||
|
return; |
||||
|
|
||||
if (notification == null) |
var errorPoint = CalculateErrorPoint(nodeIndex); |
||||
{ |
|
||||
var converted = Converter.Convert( |
|
||||
value, |
|
||||
_targetType, |
|
||||
ConverterParameter, |
|
||||
ConverterCulture); |
|
||||
|
|
||||
if (converted == BindingOperations.DoNothing) |
if (ShouldLogError(out var target)) |
||||
{ |
Log(target, error, errorPoint); |
||||
return converted; |
|
||||
} |
|
||||
|
|
||||
notification = converted as BindingNotification; |
// 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 (notification?.ErrorType == BindingErrorType.None) |
internal void OnDataValidationError(Exception error) |
||||
{ |
{ |
||||
converted = notification.Value; |
var bindingError = new BindingError(error, BindingErrorType.DataValidationError); |
||||
} |
PublishValue(UnchangedValue, bindingError); |
||||
|
} |
||||
|
|
||||
if (_fallbackValue != AvaloniaProperty.UnsetValue && |
internal override bool WriteValueToSource(object? value) |
||||
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification)) |
{ |
||||
{ |
if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type) |
||||
var fallback = ConvertFallback(); |
return false; |
||||
converted = Merge(converted, fallback); |
|
||||
} |
|
||||
|
|
||||
return converted; |
if (Converter is { } converter && |
||||
} |
value != AvaloniaProperty.UnsetValue && |
||||
else |
value != BindingOperations.DoNothing) |
||||
{ |
{ |
||||
return ConvertValue(notification); |
value = ConvertBack(converter, ConverterCulture, ConverterParameter, value, type); |
||||
} |
|
||||
} |
} |
||||
|
|
||||
private BindingNotification ConvertValue(BindingNotification notification) |
if (value == BindingOperations.DoNothing) |
||||
|
return true; |
||||
|
|
||||
|
// Use the target type converter to convert the value to the target type if necessary.
|
||||
|
if (_targetTypeConverter is not null) |
||||
{ |
{ |
||||
if (notification.HasValue) |
if (_targetTypeConverter.TryConvert(value, type, ConverterCulture, out var converted)) |
||||
|
{ |
||||
|
value = converted; |
||||
|
} |
||||
|
else if (FallbackValue != AvaloniaProperty.UnsetValue) |
||||
|
{ |
||||
|
value = FallbackValue; |
||||
|
} |
||||
|
else if (IsDataValidationEnabled) |
||||
{ |
{ |
||||
var converted = ConvertValue(notification.Value); |
var valueString = value?.ToString() ?? "(null)"; |
||||
notification = Merge(notification, converted); |
var valueTypeName = value?.GetType().FullName ?? "null"; |
||||
|
var ex = new InvalidCastException( |
||||
|
$"Could not convert '{valueString}' ({valueTypeName}) to {type}."); |
||||
|
OnDataValidationError(ex); |
||||
|
return false; |
||||
} |
} |
||||
else if (_fallbackValue != AvaloniaProperty.UnsetValue) |
else |
||||
{ |
{ |
||||
var fallback = ConvertFallback(); |
return false; |
||||
notification = Merge(notification, fallback); |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
return notification; |
// Don't set the value if it's unchanged.
|
||||
|
if (TypeUtilities.IdentityEquals(LeafNode.Value, value, type)) |
||||
|
return true; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
return setter.WriteValueToSource(value, _nodes); |
||||
} |
} |
||||
|
catch |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target) |
||||
|
{ |
||||
|
if (!TryGetTarget(out target)) |
||||
|
return false; |
||||
|
if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode) |
||||
|
return sourceNode.ShouldLogErrors(target); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
private BindingNotification ConvertFallback() |
protected override void StartCore() |
||||
|
{ |
||||
|
if (_source?.TryGetTarget(out var source) == true) |
||||
{ |
{ |
||||
object? converted; |
if (_nodes.Count > 0) |
||||
|
_nodes[0].SetSource(source, null); |
||||
|
else |
||||
|
ConvertAndPublishValue(source, null); |
||||
|
|
||||
if (_fallbackValue == AvaloniaProperty.UnsetValue) |
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && |
||||
|
TryGetTarget(out var target) && |
||||
|
TargetProperty is not null) |
||||
{ |
{ |
||||
throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value"); |
if (_mode is BindingMode.OneWayToSource) |
||||
} |
PublishValue(target.GetValue(TargetProperty)); |
||||
|
|
||||
if (TypeUtilities.TryConvert( |
var trigger = UpdateSourceTrigger; |
||||
_targetType, |
|
||||
_fallbackValue, |
if (trigger is UpdateSourceTrigger.PropertyChanged) |
||||
ConverterCulture, |
target.PropertyChanged += OnTargetPropertyChanged; |
||||
out converted)) |
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie) |
||||
{ |
ie.LostFocus += OnTargetLostFocus; |
||||
return new BindingNotification(converted); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return new BindingNotification( |
|
||||
new InvalidCastException( |
|
||||
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"), |
|
||||
BindingErrorType.Error); |
|
||||
} |
} |
||||
} |
} |
||||
|
else |
||||
|
{ |
||||
|
OnNodeError(-1, "Binding Source is null."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
private static BindingNotification Merge(object a, BindingNotification b) |
protected override void StopCore() |
||||
|
{ |
||||
|
foreach (var node in _nodes) |
||||
|
node.Reset(); |
||||
|
|
||||
|
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && |
||||
|
TryGetTarget(out var target)) |
||||
{ |
{ |
||||
var an = a as BindingNotification; |
var trigger = UpdateSourceTrigger; |
||||
|
|
||||
if (an != null) |
if (trigger is UpdateSourceTrigger.PropertyChanged) |
||||
{ |
target.PropertyChanged -= OnTargetPropertyChanged; |
||||
Merge(an, b); |
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie) |
||||
return an; |
ie.LostFocus -= OnTargetLostFocus; |
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return b; |
|
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
|
private string CalculateErrorPoint(int nodeIndex) |
||||
|
{ |
||||
|
// Build a string describing the binding chain up to the node that errored.
|
||||
|
var result = new StringBuilder(); |
||||
|
|
||||
|
if (nodeIndex >= 0) |
||||
|
_nodes[nodeIndex].BuildString(result); |
||||
|
else |
||||
|
result.Append("(source)"); |
||||
|
|
||||
|
return result.ToString(); |
||||
|
} |
||||
|
|
||||
|
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, object? b) |
private void ConvertAndPublishValue(object? value, BindingError? error) |
||||
|
{ |
||||
|
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) |
||||
{ |
{ |
||||
var bn = b as BindingNotification; |
value = Convert(converter, ConverterCulture, ConverterParameter, value, TargetType, ref error); |
||||
|
} |
||||
|
|
||||
if (bn != null) |
// Check this here as the converter may return DoNothing.
|
||||
{ |
if (value == BindingOperations.DoNothing) |
||||
Merge(a, bn); |
return; |
||||
} |
|
||||
else |
|
||||
{ |
|
||||
a.SetValue(b); |
|
||||
} |
|
||||
|
|
||||
return a; |
// 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) |
||||
|
{ |
||||
|
value = ConvertFallback(TargetNullValue, nameof(TargetNullValue)); |
||||
|
isTargetNullValue = true; |
||||
} |
} |
||||
|
|
||||
private static BindingNotification Merge(BindingNotification a, BindingNotification b) |
// If we have a value, try to convert it to the target type.
|
||||
|
if (value != AvaloniaProperty.UnsetValue) |
||||
{ |
{ |
||||
if (b.HasValue) |
if (StringFormat is { } stringFormat && |
||||
|
(TargetType == typeof(object) || TargetType == typeof(string)) && |
||||
|
!isTargetNullValue) |
||||
{ |
{ |
||||
a.SetValue(b.Value); |
// 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 |
else if (_targetTypeConverter is not null) |
||||
{ |
{ |
||||
a.ClearValue(); |
// Otherwise, if we have a target type converter, convert the value to the target type.
|
||||
|
value = ConvertFrom(_targetTypeConverter, value, ref error); |
||||
} |
} |
||||
|
} |
||||
|
|
||||
if (b.Error != null) |
// FallbackValue applies if the result from the binding, converter or target type converter
|
||||
{ |
// is UnsetValue.
|
||||
a.AddError(b.Error, b.ErrorType); |
if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue) |
||||
} |
value = ConvertFallback(FallbackValue, nameof(FallbackValue)); |
||||
|
|
||||
return a; |
// Publish the value.
|
||||
} |
PublishValue(value, error); |
||||
|
} |
||||
|
|
||||
|
private void WriteTargetValueToSource() |
||||
|
{ |
||||
|
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource); |
||||
|
|
||||
public class InnerListener : IObserver<object?>, IDisposable |
if (TryGetTarget(out var target) && |
||||
|
TargetProperty is not null && |
||||
|
target.GetValue(TargetProperty) is var value && |
||||
|
!TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType)) |
||||
{ |
{ |
||||
private readonly BindingExpression _owner; |
WriteValueToSource(value); |
||||
private readonly IDisposable _dispose; |
} |
||||
|
} |
||||
|
|
||||
public InnerListener(BindingExpression owner) |
private void OnTargetLostFocus(object? sender, RoutedEventArgs e) |
||||
{ |
{ |
||||
_owner = owner; |
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.LostFocus); |
||||
_dispose = owner._inner.Subscribe(this); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() => _dispose.Dispose(); |
WriteTargetValueToSource(); |
||||
public void OnCompleted() => _owner.PublishCompleted(); |
} |
||||
public void OnError(Exception error) => _owner.PublishError(error); |
|
||||
|
|
||||
public void OnNext(object? value) |
private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
||||
{ |
{ |
||||
if (value == BindingOperations.DoNothing) |
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource); |
||||
{ |
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged); |
||||
return; |
|
||||
} |
if (e.Property == TargetProperty) |
||||
|
WriteValueToSource(e.NewValue); |
||||
|
} |
||||
|
|
||||
var converted = _owner.ConvertValue(value); |
private object? ConvertFallback(object? fallback, string fallbackName) |
||||
|
{ |
||||
|
if (_targetTypeConverter is null || TargetType == typeof(object) || fallback == AvaloniaProperty.UnsetValue) |
||||
|
return fallback; |
||||
|
|
||||
if (converted == BindingOperations.DoNothing) |
if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result)) |
||||
{ |
return result; |
||||
return; |
|
||||
} |
|
||||
|
|
||||
_owner._value = converted is not null ? new WeakReference<object>(converted) : null; |
if (TryGetTarget(out var target)) |
||||
_owner.PublishNext(converted); |
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; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,7 +0,0 @@ |
|||||
namespace Avalonia.Data.Core |
|
||||
{ |
|
||||
internal class EmptyExpressionNode : ExpressionNode |
|
||||
{ |
|
||||
public override string Description => "."; |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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); } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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."); |
||||
|
} |
||||
|
} |
||||
@ -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) { } |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -1,7 +0,0 @@ |
|||||
namespace Avalonia.Data.Core |
|
||||
{ |
|
||||
interface ITransformNode |
|
||||
{ |
|
||||
object? Transform(object? value); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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?; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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}."); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,142 +1,67 @@ |
|||||
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 |
||||
{ |
{ |
||||
internal class MethodAccessorPlugin : IPropertyAccessorPlugin |
private readonly MethodInfo _method; |
||||
|
private readonly Type _delegateType; |
||||
|
|
||||
|
public MethodAccessorPlugin(MethodInfo method, Type delegateType) |
||||
|
{ |
||||
|
_method = method; |
||||
|
_delegateType = delegateType; |
||||
|
} |
||||
|
|
||||
|
public bool Match(object obj, string propertyName) |
||||
{ |
{ |
||||
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = |
throw new InvalidOperationException("The MethodAccessorPlugin does not support dynamic matching"); |
||||
new Dictionary<(Type, string), MethodInfo?>(); |
} |
||||
|
|
||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] |
public IPropertyAccessor Start(WeakReference<object?> reference, string propertyName) |
||||
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; |
{ |
||||
|
Debug.Assert(_method.Name == propertyName); |
||||
|
return new Accessor(reference, _method, _delegateType); |
||||
|
} |
||||
|
|
||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] |
private sealed class Accessor : PropertyAccessorBase |
||||
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName) |
{ |
||||
|
public Accessor(WeakReference<object?> reference, MethodInfo method, Type delegateType) |
||||
{ |
{ |
||||
_ = reference ?? throw new ArgumentNullException(nameof(reference)); |
_ = reference ?? throw new ArgumentNullException(nameof(reference)); |
||||
_ = methodName ?? throw new ArgumentNullException(nameof(methodName)); |
_ = method ?? throw new ArgumentNullException(nameof(method)); |
||||
|
|
||||
if (!reference.TryGetTarget(out var instance) || instance is null) |
|
||||
return null; |
|
||||
|
|
||||
var method = GetFirstMethodWithName(instance.GetType(), methodName); |
PropertyType = delegateType; |
||||
|
|
||||
if (method is not null) |
if (method.IsStatic) |
||||
{ |
{ |
||||
return new Accessor(reference, method); |
Value = method.CreateDelegate(PropertyType); |
||||
} |
} |
||||
else |
else if (reference.TryGetTarget(out var target)) |
||||
{ |
{ |
||||
var message = $"Could not find CLR method '{methodName}' on '{instance}'"; |
Value = method.CreateDelegate(PropertyType, target); |
||||
var exception = new MissingMemberException(message); |
|
||||
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); |
|
||||
} |
} |
||||
} |
} |
||||
|
|
||||
private MethodInfo? GetFirstMethodWithName( |
public override Type? PropertyType { get; } |
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName) |
|
||||
{ |
|
||||
var key = (type, methodName); |
|
||||
|
|
||||
if (!_methodLookup.TryGetValue(key, out var methodInfo)) |
public override object? Value { get; } |
||||
{ |
|
||||
methodInfo = TryFindAndCacheMethod(type, methodName); |
|
||||
} |
|
||||
|
|
||||
return methodInfo; |
public override bool SetValue(object? value, BindingPriority priority) => false; |
||||
} |
|
||||
|
|
||||
private MethodInfo? TryFindAndCacheMethod( |
protected override void SubscribeCore() |
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName) |
|
||||
{ |
{ |
||||
MethodInfo? found = null; |
try |
||||
|
|
||||
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) |
PublishValue(Value); |
||||
{ |
|
||||
var parameters = methodInfo.GetParameters(); |
|
||||
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object)) |
|
||||
{ |
|
||||
found = methodInfo; |
|
||||
break; |
|
||||
} |
|
||||
else if (parameters.Length == 0) |
|
||||
{ |
|
||||
found = methodInfo; |
|
||||
} |
|
||||
} |
|
||||
} |
} |
||||
|
catch { } |
||||
_methodLookup.Add((type, methodName), found); |
|
||||
|
|
||||
return found; |
|
||||
} |
} |
||||
|
|
||||
private sealed class Accessor : PropertyAccessorBase |
protected override void UnsubscribeCore() |
||||
{ |
{ |
||||
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++) |
|
||||
{ |
|
||||
ParameterInfo 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() |
|
||||
{ |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,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); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
} |
||||
@ -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."; |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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…
Reference in new issue