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.Globalization; |
|||
using Avalonia.Reactive; |
|||
using System.Linq.Expressions; |
|||
using System.Text; |
|||
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.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>
|
|||
/// Binds to an expression on an object using a type value converter to convert the values
|
|||
/// that are sent and received.
|
|||
/// Initializes a new instance of the <see cref="BindingExpression"/> class.
|
|||
/// </summary>
|
|||
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)] |
|||
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription |
|||
/// <param name="source">The source from which the value will be read.</param>
|
|||
/// <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; |
|||
private readonly Type _targetType; |
|||
private readonly object? _fallbackValue; |
|||
private readonly object? _targetNullValue; |
|||
private readonly BindingPriority _priority; |
|||
InnerListener? _innerListener; |
|||
WeakReference<object>? _value; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|||
/// <param name="targetType">The type to convert the value to.</param>
|
|||
public BindingExpression(ExpressionObserver inner, Type targetType) |
|||
: this(inner, targetType, DefaultValueConverter.Instance, CultureInfo.InvariantCulture) |
|||
if (mode == BindingMode.Default) |
|||
throw new ArgumentException("Binding mode cannot be Default.", nameof(mode)); |
|||
if (updateSourceTrigger == UpdateSourceTrigger.Default) |
|||
throw new ArgumentException("UpdateSourceTrigger cannot be Default.", nameof(updateSourceTrigger)); |
|||
|
|||
if (source == AvaloniaProperty.UnsetValue) |
|||
source = null; |
|||
|
|||
_source = new(source); |
|||
_mode = mode; |
|||
_nodes = nodes ?? s_emptyExpressionNodes; |
|||
_targetTypeConverter = targetTypeConverter; |
|||
|
|||
if (converter is not null || |
|||
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>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|||
/// <param name="targetType">The type to convert the value to.</param>
|
|||
/// <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) |
|||
IPropertyAccessorNode? leafAccessor = null; |
|||
|
|||
if (nodes is not null) |
|||
{ |
|||
for (var i = 0; i < nodes.Count; ++i) |
|||
{ |
|||
var node = nodes[i]; |
|||
node.SetOwner(this, i); |
|||
if (node is IPropertyAccessorNode n) |
|||
leafAccessor = n; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|||
/// <param name="targetType">The type to convert the value to.</param>
|
|||
/// <param name="fallbackValue">
|
|||
/// The value to use when the binding is unable to produce a value.
|
|||
/// </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) |
|||
if (enableDataValidation) |
|||
leafAccessor?.EnableDataValidation(); |
|||
} |
|||
|
|||
public override string Description |
|||
{ |
|||
get |
|||
{ |
|||
_ = inner ?? throw new ArgumentNullException(nameof(inner)); |
|||
_ = targetType ?? throw new ArgumentNullException(nameof(targetType)); |
|||
_ = converter ?? throw new ArgumentNullException(nameof(converter)); |
|||
|
|||
_inner = inner; |
|||
_targetType = targetType; |
|||
Converter = converter; |
|||
ConverterCulture = converterCulture; |
|||
ConverterParameter = converterParameter; |
|||
_fallbackValue = fallbackValue; |
|||
_targetNullValue = targetNullValue; |
|||
_priority = priority; |
|||
var b = new StringBuilder(); |
|||
LeafNode.BuildString(b, _nodes); |
|||
return b.ToString(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the converter to use on the expression.
|
|||
/// </summary>
|
|||
public IValueConverter Converter { get; } |
|||
public Type? SourceType => (LeafNode as ISettableNode)?.ValueType; |
|||
public IValueConverter? Converter => _uncommon?._converter; |
|||
public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture; |
|||
public object? ConverterParameter => _uncommon?._converterParameter; |
|||
public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue; |
|||
public ExpressionNode LeafNode => _nodes[_nodes.Count - 1]; |
|||
public string? StringFormat => _uncommon?._stringFormat; |
|||
public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue; |
|||
public UpdateSourceTrigger UpdateSourceTrigger => _uncommon?._updateSourceTrigger ?? UpdateSourceTrigger.PropertyChanged; |
|||
|
|||
public override void UpdateSource() |
|||
{ |
|||
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource) |
|||
WriteTargetValueToSource(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the culture in which to evaluate the converter.
|
|||
/// </summary>
|
|||
public CultureInfo ConverterCulture { get; set; } |
|||
public override void UpdateTarget() |
|||
{ |
|||
if (_nodes.Count == 0) |
|||
return; |
|||
|
|||
/// <summary>
|
|||
/// Gets a parameter to pass to <see cref="Converter"/>.
|
|||
/// </summary>
|
|||
public object? ConverterParameter { get; } |
|||
var source = _nodes[0].Source; |
|||
|
|||
/// <inheritdoc/>
|
|||
string? IDescription.Description => _inner.Expression; |
|||
for (var i = 0; i < _nodes.Count; ++i) |
|||
_nodes[i].SetSource(null, null); |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnCompleted() |
|||
{ |
|||
} |
|||
_nodes[0].SetSource(source, null); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnError(Exception error) |
|||
{ |
|||
} |
|||
/// <summary>
|
|||
/// 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/>
|
|||
public void OnNext(object? value) |
|||
/// <summary>
|
|||
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when its
|
|||
/// <see cref="ExpressionNode.Value"/> changes.
|
|||
/// </summary>
|
|||
/// <param name="nodeIndex">The <see cref="ExpressionNode.Index"/>.</param>
|
|||
/// <param name="value">The <see cref="ExpressionNode.Value"/>.</param>
|
|||
/// <param name="dataValidationError">
|
|||
/// The data validation error associated with the current value, if any.
|
|||
/// </param>
|
|||
internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataValidationError) |
|||
{ |
|||
Debug.Assert(value is not BindingNotification); |
|||
Debug.Assert(nodeIndex >= 0 && nodeIndex < _nodes.Count); |
|||
|
|||
if (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(_ => { })) |
|||
{ |
|||
var type = _inner.ResultType; |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
// If the binding mode is OneTime, then stop the binding if a valid value was published.
|
|||
if (_mode == BindingMode.OneTime && GetValue() != AvaloniaProperty.UnsetValue) |
|||
Stop(); |
|||
} |
|||
|
|||
protected override void Initialize() => _innerListener = new InnerListener(this); |
|||
protected override void Deinitialize() => _innerListener?.Dispose(); |
|||
|
|||
protected override void Subscribed(IObserver<object> observer, bool first) |
|||
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null) |
|||
{ |
|||
if (!first && _value != null && _value.TryGetTarget(out var val)) |
|||
{ |
|||
observer.OnNext(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
|
|||
// node. First update the leaf node's source, then write the value to its property.
|
|||
_nodes[nodeIndex + 1].SetSource(value, dataValidationError); |
|||
WriteTargetValueToSource(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
private object? ConvertValue(object? value) |
|||
else if (value is null) |
|||
{ |
|||
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
return _targetNullValue; |
|||
} |
|||
OnNodeError(nodeIndex, "Value is null."); |
|||
} |
|||
else |
|||
{ |
|||
_nodes[nodeIndex + 1].SetSource(value, dataValidationError); |
|||
} |
|||
} |
|||
|
|||
if (value == BindingOperations.DoNothing) |
|||
{ |
|||
return value; |
|||
} |
|||
/// <summary>
|
|||
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when an error occurs
|
|||
/// 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 converted = Converter.Convert( |
|||
value, |
|||
_targetType, |
|||
ConverterParameter, |
|||
ConverterCulture); |
|||
var errorPoint = CalculateErrorPoint(nodeIndex); |
|||
|
|||
if (converted == BindingOperations.DoNothing) |
|||
{ |
|||
return converted; |
|||
} |
|||
if (ShouldLogError(out var target)) |
|||
Log(target, error, errorPoint); |
|||
|
|||
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) |
|||
{ |
|||
converted = notification.Value; |
|||
} |
|||
internal void OnDataValidationError(Exception error) |
|||
{ |
|||
var bindingError = new BindingError(error, BindingErrorType.DataValidationError); |
|||
PublishValue(UnchangedValue, bindingError); |
|||
} |
|||
|
|||
if (_fallbackValue != AvaloniaProperty.UnsetValue && |
|||
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification)) |
|||
{ |
|||
var fallback = ConvertFallback(); |
|||
converted = Merge(converted, fallback); |
|||
} |
|||
internal override bool WriteValueToSource(object? value) |
|||
{ |
|||
if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type) |
|||
return false; |
|||
|
|||
return converted; |
|||
} |
|||
else |
|||
{ |
|||
return ConvertValue(notification); |
|||
} |
|||
if (Converter is { } converter && |
|||
value != AvaloniaProperty.UnsetValue && |
|||
value != BindingOperations.DoNothing) |
|||
{ |
|||
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); |
|||
notification = Merge(notification, converted); |
|||
var valueString = value?.ToString() ?? "(null)"; |
|||
var valueTypeName = value?.GetType().FullName ?? "null"; |
|||
var ex = new InvalidCastException( |
|||
$"Could not convert '{valueString}' ({valueTypeName}) to {type}."); |
|||
OnDataValidationError(ex); |
|||
return false; |
|||
} |
|||
else if (_fallbackValue != AvaloniaProperty.UnsetValue) |
|||
else |
|||
{ |
|||
var fallback = ConvertFallback(); |
|||
notification = Merge(notification, fallback); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
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( |
|||
_targetType, |
|||
_fallbackValue, |
|||
ConverterCulture, |
|||
out converted)) |
|||
{ |
|||
return new BindingNotification(converted); |
|||
} |
|||
else |
|||
{ |
|||
return new BindingNotification( |
|||
new InvalidCastException( |
|||
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"), |
|||
BindingErrorType.Error); |
|||
var trigger = UpdateSourceTrigger; |
|||
|
|||
if (trigger is UpdateSourceTrigger.PropertyChanged) |
|||
target.PropertyChanged += OnTargetPropertyChanged; |
|||
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie) |
|||
ie.LostFocus += OnTargetLostFocus; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
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) |
|||
{ |
|||
Merge(an, b); |
|||
return an; |
|||
} |
|||
else |
|||
{ |
|||
return b; |
|||
} |
|||
if (trigger is UpdateSourceTrigger.PropertyChanged) |
|||
target.PropertyChanged -= OnTargetPropertyChanged; |
|||
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie) |
|||
ie.LostFocus -= OnTargetLostFocus; |
|||
} |
|||
} |
|||
|
|||
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) |
|||
{ |
|||
Merge(a, bn); |
|||
} |
|||
else |
|||
{ |
|||
a.SetValue(b); |
|||
} |
|||
// Check this here as the converter may return DoNothing.
|
|||
if (value == BindingOperations.DoNothing) |
|||
return; |
|||
|
|||
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) |
|||
{ |
|||
a.AddError(b.Error, b.ErrorType); |
|||
} |
|||
// FallbackValue applies if the result from the binding, converter or target type converter
|
|||
// is UnsetValue.
|
|||
if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue) |
|||
value = ConvertFallback(FallbackValue, nameof(FallbackValue)); |
|||
|
|||
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; |
|||
private readonly IDisposable _dispose; |
|||
WriteValueToSource(value); |
|||
} |
|||
} |
|||
|
|||
public InnerListener(BindingExpression owner) |
|||
{ |
|||
_owner = owner; |
|||
_dispose = owner._inner.Subscribe(this); |
|||
} |
|||
private void OnTargetLostFocus(object? sender, RoutedEventArgs e) |
|||
{ |
|||
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.LostFocus); |
|||
|
|||
public void Dispose() => _dispose.Dispose(); |
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
WriteTargetValueToSource(); |
|||
} |
|||
|
|||
public void OnNext(object? value) |
|||
{ |
|||
if (value == BindingOperations.DoNothing) |
|||
{ |
|||
return; |
|||
} |
|||
private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource); |
|||
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged); |
|||
|
|||
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) |
|||
{ |
|||
return; |
|||
} |
|||
if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result)) |
|||
return result; |
|||
|
|||
_owner._value = converted is not null ? new WeakReference<object>(converted) : null; |
|||
_owner.PublishNext(converted); |
|||
} |
|||
} |
|||
if (TryGetTarget(out var target)) |
|||
Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error); |
|||
|
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
|
|||
private object? ConvertFrom(TargetTypeConverter? converter, object? value, ref BindingError? error) |
|||
{ |
|||
if (converter is null) |
|||
return value; |
|||
|
|||
if (converter.TryConvert(value, TargetType, ConverterCulture, out var result)) |
|||
return result; |
|||
|
|||
var valueString = value?.ToString() ?? "(null)"; |
|||
var valueTypeName = value?.GetType().FullName ?? "null"; |
|||
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'."; |
|||
|
|||
if (ShouldLogError(out var target)) |
|||
Log(target, message, LogEventLevel.Warning); |
|||
|
|||
error = new(new InvalidCastException(message), BindingErrorType.Error); |
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Uncommonly used fields are separated out to reduce memory usage.
|
|||
/// </summary>
|
|||
private class UncommonFields |
|||
{ |
|||
public IValueConverter? _converter; |
|||
public object? _converterParameter; |
|||
public CultureInfo? _converterCulture; |
|||
public object? _fallbackValue; |
|||
public string? _stringFormat; |
|||
public object? _targetNullValue; |
|||
public UpdateSourceTrigger _updateSourceTrigger; |
|||
} |
|||
} |
|||
|
|||
@ -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.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Data.Core.Plugins |
|||
{ |
|||
/// <summary>
|
|||
/// Holds a registry of plugins used for bindings.
|
|||
/// </summary>
|
|||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] |
|||
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>
|
|||
/// 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 IList<IPropertyAccessorPlugin> PropertyAccessors => ExpressionObserver.PropertyAccessors; |
|||
public static IList<IPropertyAccessorPlugin> PropertyAccessors => s_propertyAccessors; |
|||
|
|||
/// <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 IList<IDataValidationPlugin> DataValidators => ExpressionObserver.DataValidators; |
|||
public static IList<IDataValidationPlugin> DataValidators => s_dataValidators; |
|||
|
|||
/// <summary>
|
|||
/// An ordered collection of stream plugins that can be used to customize the behavior
|
|||
/// of the '^' stream binding operator.
|
|||
/// </summary>
|
|||
public static IList<IStreamPlugin> StreamHandlers => ExpressionObserver.StreamHandlers; |
|||
public static IList<IStreamPlugin> StreamHandlers => s_streamHandlers; |
|||
} |
|||
} |
|||
|
|||
@ -1,142 +1,67 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq.Expressions; |
|||
using System.Diagnostics; |
|||
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 = |
|||
new Dictionary<(Type, string), MethodInfo?>(); |
|||
throw new InvalidOperationException("The MethodAccessorPlugin does not support dynamic matching"); |
|||
} |
|||
|
|||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] |
|||
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; |
|||
public IPropertyAccessor Start(WeakReference<object?> reference, string propertyName) |
|||
{ |
|||
Debug.Assert(_method.Name == propertyName); |
|||
return new Accessor(reference, _method, _delegateType); |
|||
} |
|||
|
|||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] |
|||
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName) |
|||
private sealed class Accessor : PropertyAccessorBase |
|||
{ |
|||
public Accessor(WeakReference<object?> reference, MethodInfo method, Type delegateType) |
|||
{ |
|||
_ = reference ?? throw new ArgumentNullException(nameof(reference)); |
|||
_ = methodName ?? throw new ArgumentNullException(nameof(methodName)); |
|||
|
|||
if (!reference.TryGetTarget(out var instance) || instance is null) |
|||
return null; |
|||
_ = method ?? throw new ArgumentNullException(nameof(method)); |
|||
|
|||
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}'"; |
|||
var exception = new MissingMemberException(message); |
|||
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); |
|||
Value = method.CreateDelegate(PropertyType, target); |
|||
} |
|||
} |
|||
|
|||
private MethodInfo? GetFirstMethodWithName( |
|||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName) |
|||
{ |
|||
var key = (type, methodName); |
|||
public override Type? PropertyType { get; } |
|||
|
|||
if (!_methodLookup.TryGetValue(key, out var methodInfo)) |
|||
{ |
|||
methodInfo = TryFindAndCacheMethod(type, methodName); |
|||
} |
|||
public override object? Value { get; } |
|||
|
|||
return methodInfo; |
|||
} |
|||
public override bool SetValue(object? value, BindingPriority priority) => false; |
|||
|
|||
private MethodInfo? TryFindAndCacheMethod( |
|||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName) |
|||
protected override void SubscribeCore() |
|||
{ |
|||
MethodInfo? found = null; |
|||
|
|||
const BindingFlags bindingFlags = |
|||
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; |
|||
|
|||
var methods = type.GetMethods(bindingFlags); |
|||
|
|||
foreach (var methodInfo in methods) |
|||
try |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
PublishValue(Value); |
|||
} |
|||
|
|||
_methodLookup.Add((type, methodName), found); |
|||
|
|||
return found; |
|||
catch { } |
|||
} |
|||
|
|||
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