Binding System refactor (#13970)
* Update ncrunch config. * WIP: Benchmarks * Initial refactor of binding infrastructure. - `ExpressionObserver` has been removed and its functionality merged with `BindingExpression` - `BindingExpression` handles all types of `BindingMode` itself; doesn't require `BindingOperations.Apply` to set up a separate observable for `TwoWay/`OneWayToSource` bindings - This allows us to fix some long-standing issues with `OneWayToSource` bindings - Expression nodes have been refactored - No longer split between `Avalonia.Base` and `Avalonia.Markup` - Categorize them according to whether they use reflection or not A few tests are failing around binding warnings: this is because the next step here is to fix binding warnings. * Make default binding Source = UnsetProperty. Null is a theoretically valid value for `Source`; setting it to null shouldn't mean "use the data context". * Move logging to BindingExpression. As `BindingExpression` now has enough information to decide when it's appropriate to log an error/warning or not. Fixes #5762 Fixes #9422 * Add compatibility hack for older compiled bindings. Previously, `CompiledBindingPathBuilder` didn't have a `TemplatedParent` method and instead the XAML compiler rewrite templated parent bindings to be a `$self.TemplateParent` property binding. resulting in extraneous logs. Add a constructor with an `apiVersion` to `CompiledBindingPathBuilder` which will be used by newer versions of the XAML compiler, and if a usage is detected using an `apiVersion` of 0, then upgrade `$self.TemplatedParent` to use a `TemplatedParentPathElement`. * Log errors from property accessors. * Don't log errors for named control bindings... ...on elements which aren't yet rooted. * Log errors for failed conversions. * Use consistent wording for binding warnings. "Could not convert" instead of "Cannot convert". * Log warnings for converter exceptions. * Don't convert new TargetTypeConverters each time. * Added failing test for implicit conversion. * Support cast operators in compiled bindings. A bit of a hack as we'd ideally not be using reflection when using compiled bindings. * This shouldn't be a public API. Should only be used for tests. * Make enum/int conversion work. * Check for SetValue equality after conversion. And also use "identity equals" where value types and strings use `object.Equals` and reference types use `object.ReferenceEquals`. * Added ConverterCulture back to bindings. * Fix merge error. Removed deleted files from csproj that were re-added due to indentation changes. * Use BindingExpression directly in ValueStoe. * Introduce BindingExpressionBase. And `UntypedBindingExpressionBase`. * Make TemplateBinding a BindingExpression. * Make DynamicResource use a BindingExpression. * WIP: Start exposing a BindingExpression API. * Finish exposing a BindingExpression API. * Fix OneTimeBinding. * Remove unneeded classes/methods. * Don't call obsolete API. * Make BindingExpressionBase the public API. This matches WPF's API. * Added BindingExpressionBase.UpdateTarget. * Initial implementation of UpdateSourceTrigger. * Don't use weak references for values. If they're boxed values, they can get collected. * No need for virtual/generic methods here now. * Reintroduce support for binding anchors. Turns out these were needed by animations, just our animation system has no unit tests so I missed that fact earlier. Add a basic animation unit test that fails without anchor support, and add binding anchors back in. Currently a private API as I suspect this feature shouldn't be needed outside the framework. * Include new property in clone. And add real-life example of `UpdateSourceTrigger=LostFocus` to BindingDemo. * Fix merge error. * Updated BindingExpression tests. - Make them run for both compiled and reflection bindings (found a bunch of tests that fail with compiled bindings) - Make them not depend on converting the `BindingExpression` to an observable and instead test the end result of the binding on an `AvaloniaObject` * Fix compiled binding indexer tests. * Use data validation plugins in PropertyAccessorNode. Added a warning suppression for now: we may need a separate `DataValidators` list for AOT-friendly plugins. * Don't separate plugins by reflection. `DataAnnotationsValidationPlugin` is public and so it can't be moved. No point in moving the others if this one will be in the wrong place. * Remove unneeded methods. * Make reflection binding tests use a string. Convert the `System.Linq.Expression` to a string and then use this, as reflection bindings will always be instanced with a string path. * Added TODO12 plan for IBinding2. * Use more specific exception. * Fix nits from code review. * Make expression nodes sealed where possible. * Unsubscribe on Stop, don't re-subscribe. D'oh. * Tweak ExpressionNode lists. Saves a few K in benchmarks and it's a cleaner API. * Add a pooled option in BindingExpressionGrammar. Micro-optimization. * Avoid allocations when enumerating binding plugins. * Add IBinding2 support to observable bind overloads. In the case of `TemplateBinding`, the `IObservable<object?>` bind overload is selected by C#. Add an explicit check for an `IBinding2` here to use the more performant code-path. * Remove disposed binding from ImmediateBindingFrame. * Added TemplateBinding benchmarks. * Remove duplicate items. Seems to have been caused by a merge error. * Fix exception when closing color picker. And add tests. * Don't skip converter when binding to self. * Don't pass UnsetValue to converters. This follows WPF behavior. * Log element name if present. More useful than just logging the control hash code. * Respect binding priority. * Throw on mismatched binding priorities. We don't want to respect the binding priority in this case as it breaks `TemplateBindings` when the default `LocalValue` priority is passed. Instead make sure that the priority parameter matches that of the expression. This reverts commit a72765d70514506d94b39ba33a3d91e11cd6dcd4. * Convert to target type in TemplateBinding. * Short-circuit target type conversion for same types.
This commit is contained in:
@@ -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>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<StackPanel Margin="18" Spacing="4" Width="200">
|
<StackPanel Margin="18" Spacing="4" Width="200">
|
||||||
<TextBlock FontSize="16" Text="Simple Bindings"/>
|
<TextBlock FontSize="16" Text="Simple Bindings"/>
|
||||||
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
|
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
|
||||||
|
<TextBox Watermark="Two Way (LostFocus)" UseFloatingWatermark="True" Text="{Binding Path=StringValue, UpdateSourceTrigger=LostFocus}"/>
|
||||||
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
|
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
|
||||||
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
|
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
|
||||||
<!-- Removed due to #2983: reinstate when that's fixed.
|
<!-- Removed due to #2983: reinstate when that's fixed.
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ namespace Avalonia.Animation
|
|||||||
|
|
||||||
if (value is IBinding binding)
|
if (value is IBinding binding)
|
||||||
{
|
{
|
||||||
return this.Bind(ValueProperty, binding, targetControl);
|
return Bind(ValueProperty, binding, targetControl);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return this.Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
|
return Bind(ValueProperty, Observable.SingleValue(value).ToBinding(), targetControl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
using Avalonia.Diagnostics;
|
using Avalonia.Diagnostics;
|
||||||
using Avalonia.Logging;
|
using Avalonia.Logging;
|
||||||
using Avalonia.PropertyStore;
|
using Avalonia.PropertyStore;
|
||||||
|
using Avalonia.Reactive;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace Avalonia
|
namespace Avalonia
|
||||||
@@ -406,6 +408,19 @@ namespace Avalonia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds a <see cref="AvaloniaProperty"/> to an <see cref="IBinding"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="property">The property.</param>
|
||||||
|
/// <param name="binding">The binding.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The binding expression which represents the binding instance on this object.
|
||||||
|
/// </returns>
|
||||||
|
public BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding)
|
||||||
|
{
|
||||||
|
return Bind(property, binding, null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
|
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -440,7 +455,21 @@ namespace Avalonia
|
|||||||
VerifyAccess();
|
VerifyAccess();
|
||||||
ValidatePriority(priority);
|
ValidatePriority(priority);
|
||||||
|
|
||||||
return _values.AddBinding(property, source, priority);
|
if (source is IBinding2 b)
|
||||||
|
{
|
||||||
|
if (b.Instance(this, property, null) is not UntypedBindingExpressionBase expression)
|
||||||
|
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
|
||||||
|
if (priority != expression.Priority)
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"The binding priority passed to AvaloniaObject.Bind ('{priority}') " +
|
||||||
|
"conflicts with the binding priority of the provided binding expression " +
|
||||||
|
$" ({expression.Priority}').");
|
||||||
|
return GetValueStore().AddBinding(property, expression);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _values.AddBinding(property, source, priority);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -512,7 +541,16 @@ namespace Avalonia
|
|||||||
throw new ArgumentException($"The property {property.Name} is readonly.");
|
throw new ArgumentException($"The property {property.Name} is readonly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _values.AddBinding(property, source);
|
if (source is IBinding2 b)
|
||||||
|
{
|
||||||
|
if (b.Instance(this, property, null) is not UntypedBindingExpressionBase expression)
|
||||||
|
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
|
||||||
|
return GetValueStore().AddBinding(property, expression);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _values.AddBinding(property, source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -573,6 +611,30 @@ namespace Avalonia
|
|||||||
/// <param name="property">The property.</param>
|
/// <param name="property">The property.</param>
|
||||||
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
|
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds a <see cref="AvaloniaProperty"/> to an <see cref="IBinding"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="property">The property.</param>
|
||||||
|
/// <param name="binding">The binding.</param>
|
||||||
|
/// <param name="anchor">
|
||||||
|
/// An optional anchor from which to locate required context. When binding to objects that
|
||||||
|
/// are not in the logical tree, certain types of binding need an anchor into the tree in
|
||||||
|
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
|
||||||
|
/// can be used to provide this context.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// The binding expression which represents the binding instance on this object.
|
||||||
|
/// </returns>
|
||||||
|
internal BindingExpressionBase Bind(AvaloniaProperty property, IBinding binding, object? anchor)
|
||||||
|
{
|
||||||
|
if (binding is not IBinding2 b)
|
||||||
|
throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'.");
|
||||||
|
if (b.Instance(this, property, anchor) is not UntypedBindingExpressionBase expression)
|
||||||
|
throw new NotSupportedException("Binding returned unsupported IBindingExpression.");
|
||||||
|
|
||||||
|
return GetValueStore().AddBinding(property, expression);
|
||||||
|
}
|
||||||
|
|
||||||
internal void AddInheritanceChild(AvaloniaObject child)
|
internal void AddInheritanceChild(AvaloniaObject child)
|
||||||
{
|
{
|
||||||
_inheritanceChildren ??= new List<AvaloniaObject>();
|
_inheritanceChildren ??= new List<AvaloniaObject>();
|
||||||
@@ -608,22 +670,6 @@ namespace Avalonia
|
|||||||
internal ValueStore GetValueStore() => _values;
|
internal ValueStore GetValueStore() => _values;
|
||||||
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
|
internal IReadOnlyList<AvaloniaObject>? GetInheritanceChildren() => _inheritanceChildren;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a logger to which a binding warning may be written.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="property">The property that the error occurred on.</param>
|
|
||||||
/// <param name="e">The binding exception, if any.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// This is overridden in <see cref="Visual"/> to prevent logging binding errors when a
|
|
||||||
/// control is not attached to the visual tree.
|
|
||||||
/// </remarks>
|
|
||||||
internal virtual ParametrizedLogger? GetBindingWarningLogger(
|
|
||||||
AvaloniaProperty property,
|
|
||||||
Exception? e)
|
|
||||||
{
|
|
||||||
return Logger.TryGet(LogEventLevel.Warning, LogArea.Binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called to update the validation state for properties for which data validation is
|
/// Called to update the validation state for properties for which data validation is
|
||||||
/// enabled.
|
/// enabled.
|
||||||
@@ -757,8 +803,6 @@ namespace Avalonia
|
|||||||
/// <param name="value">The value.</param>
|
/// <param name="value">The value.</param>
|
||||||
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
|
internal void SetDirectValueUnchecked<T>(DirectPropertyBase<T> property, BindingValue<T> value)
|
||||||
{
|
{
|
||||||
LoggingUtils.LogIfNecessary(this, property, value);
|
|
||||||
|
|
||||||
switch (value.Type)
|
switch (value.Type)
|
||||||
{
|
{
|
||||||
case BindingValueType.UnsetValue:
|
case BindingValueType.UnsetValue:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
using Avalonia.Reactive;
|
using Avalonia.Reactive;
|
||||||
|
|
||||||
namespace Avalonia
|
namespace Avalonia
|
||||||
@@ -233,9 +234,10 @@ namespace Avalonia
|
|||||||
/// An optional anchor from which to locate required context. When binding to objects that
|
/// An optional anchor from which to locate required context. When binding to objects that
|
||||||
/// are not in the logical tree, certain types of binding need an anchor into the tree in
|
/// are not in the logical tree, certain types of binding need an anchor into the tree in
|
||||||
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
|
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
|
||||||
/// can be used to provice this context.
|
/// can be used to provide this context.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
||||||
|
[Obsolete("Use AvaloniaObject.Bind(AvaloniaProperty, IBinding")]
|
||||||
public static IDisposable Bind(
|
public static IDisposable Bind(
|
||||||
this AvaloniaObject target,
|
this AvaloniaObject target,
|
||||||
AvaloniaProperty property,
|
AvaloniaProperty property,
|
||||||
@@ -246,20 +248,7 @@ namespace Avalonia
|
|||||||
property = property ?? throw new ArgumentNullException(nameof(property));
|
property = property ?? throw new ArgumentNullException(nameof(property));
|
||||||
binding = binding ?? throw new ArgumentNullException(nameof(binding));
|
binding = binding ?? throw new ArgumentNullException(nameof(binding));
|
||||||
|
|
||||||
var result = binding.Initiate(
|
return target.Bind(property, binding);
|
||||||
target,
|
|
||||||
property,
|
|
||||||
anchor,
|
|
||||||
property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return BindingOperations.Apply(target, property, result, anchor);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return Disposable.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -367,7 +356,7 @@ namespace Avalonia
|
|||||||
return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action));
|
return observable.Subscribe(new ClassHandlerObserver<TTarget, TValue>(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BindingAdaptor : IBinding
|
private class BindingAdaptor : IBinding2
|
||||||
{
|
{
|
||||||
private readonly IObservable<object?> _source;
|
private readonly IObservable<object?> _source;
|
||||||
|
|
||||||
@@ -382,7 +371,13 @@ namespace Avalonia
|
|||||||
object? anchor = null,
|
object? anchor = null,
|
||||||
bool enableDataValidation = false)
|
bool enableDataValidation = false)
|
||||||
{
|
{
|
||||||
return InstancedBinding.OneWay(_source);
|
var expression = new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
|
||||||
|
return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
|
||||||
|
{
|
||||||
|
return new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -542,6 +542,14 @@ namespace Avalonia
|
|||||||
/// <param name="value">The value.</param>
|
/// <param name="value">The value.</param>
|
||||||
internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value);
|
internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Routes an untyped SetDirectValueUnchecked call to a typed call.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="o">The object instance.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
internal virtual void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Routes an untyped Bind call to a typed call.
|
/// Routes an untyped Bind call to a typed call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Avalonia
|
|||||||
{
|
{
|
||||||
if (!s_RegisteredProperties.TryGetValue(className, out var prop))
|
if (!s_RegisteredProperties.TryGetValue(className, out var prop))
|
||||||
s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
|
s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
|
||||||
return target.Bind(prop, source, anchor);
|
return target.Bind(prop, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice",
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ namespace Avalonia.Data
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An exception returned through <see cref="BindingNotification"/> signaling that a
|
/// An exception returned through <see cref="BindingNotification"/> signaling that a
|
||||||
/// requested binding expression could not be evaluated because of a null in one of the links
|
/// requested binding expression could not be evaluated because of an error in one of
|
||||||
/// of the binding chain.
|
/// the links of the binding chain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BindingChainException : Exception
|
public class BindingChainException : Exception
|
||||||
{
|
{
|
||||||
@@ -60,15 +60,15 @@ namespace Avalonia.Data
|
|||||||
{
|
{
|
||||||
if (Expression != null && ExpressionErrorPoint != null)
|
if (Expression != null && ExpressionErrorPoint != null)
|
||||||
{
|
{
|
||||||
return $"{_message} in expression '{Expression}' at '{ExpressionErrorPoint}'.";
|
return $"An error occured binding to '{Expression}' at '{ExpressionErrorPoint}': '{_message}'";
|
||||||
}
|
}
|
||||||
else if (ExpressionErrorPoint != null)
|
else if (Expression != null)
|
||||||
{
|
{
|
||||||
return $"{_message} in expression '{ExpressionErrorPoint}'.";
|
return $"An error occured binding to '{Expression}': '{_message}'";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return $"{_message} in expression.";
|
return $"An error occured in a binding: '{_message}'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Avalonia.Data
|
namespace Avalonia.Data
|
||||||
{
|
{
|
||||||
@@ -55,6 +57,7 @@ namespace Avalonia.Data
|
|||||||
/// <param name="value">The binding value.</param>
|
/// <param name="value">The binding value.</param>
|
||||||
public BindingNotification(object? value)
|
public BindingNotification(object? value)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(value is not BindingNotification);
|
||||||
_value = value;
|
_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +165,31 @@ namespace Avalonia.Data
|
|||||||
return notification is not null ? notification.Value : o;
|
return notification is not null ? notification.Value : o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the value of an object that may be a <see cref="BindingNotification"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="o">The object that may be a binding notification.</param>
|
||||||
|
/// <param name="value">The new value.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The updated binding notification if <paramref name="o"/> is a binding notification;
|
||||||
|
/// otherwise <paramref name="value"/>.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// If <paramref name="o"/> is a <see cref="BindingNotification"/> then sets its value
|
||||||
|
/// to <paramref name="value"/>. If <paramref name="value"/> is a
|
||||||
|
/// <see cref="BindingNotification"/> then the value will first be extracted.
|
||||||
|
/// </remarks>
|
||||||
|
public static object? UpdateValue(object o, object value)
|
||||||
|
{
|
||||||
|
if (o is BindingNotification n)
|
||||||
|
{
|
||||||
|
n.SetValue(ExtractValue(value));
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an exception from an object that may be a <see cref="BindingNotification"/>.
|
/// Gets an exception from an object that may be a <see cref="BindingNotification"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -261,4 +289,17 @@ namespace Avalonia.Data
|
|||||||
a?.Message == b?.Message;
|
a?.Message == b?.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static class BindingErrorTypeExtensions
|
||||||
|
{
|
||||||
|
public static BindingValueType ToBindingValueType(this BindingErrorType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
BindingErrorType.Error => BindingValueType.BindingError,
|
||||||
|
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
|
||||||
|
_ => BindingValueType.Value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Avalonia.Diagnostics;
|
||||||
using Avalonia.Reactive;
|
using Avalonia.Reactive;
|
||||||
|
|
||||||
namespace Avalonia.Data
|
namespace Avalonia.Data
|
||||||
@@ -13,23 +14,22 @@ namespace Avalonia.Data
|
|||||||
/// <param name="target">The target object.</param>
|
/// <param name="target">The target object.</param>
|
||||||
/// <param name="property">The property to bind.</param>
|
/// <param name="property">The property to bind.</param>
|
||||||
/// <param name="binding">The instanced binding.</param>
|
/// <param name="binding">The instanced binding.</param>
|
||||||
/// <param name="anchor">
|
|
||||||
/// An optional anchor from which to locate required context. When binding to objects that
|
|
||||||
/// are not in the logical tree, certain types of binding need an anchor into the tree in
|
|
||||||
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
|
|
||||||
/// can be used to provide this context.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
||||||
|
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||||
public static IDisposable Apply(
|
public static IDisposable Apply(
|
||||||
AvaloniaObject target,
|
AvaloniaObject target,
|
||||||
AvaloniaProperty property,
|
AvaloniaProperty property,
|
||||||
InstancedBinding binding,
|
InstancedBinding binding)
|
||||||
object? anchor)
|
|
||||||
{
|
{
|
||||||
_ = target ?? throw new ArgumentNullException(nameof(target));
|
_ = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
_ = property ?? throw new ArgumentNullException(nameof(property));
|
_ = property ?? throw new ArgumentNullException(nameof(property));
|
||||||
_ = binding ?? throw new ArgumentNullException(nameof(binding));
|
_ = binding ?? throw new ArgumentNullException(nameof(binding));
|
||||||
|
|
||||||
|
if (binding.Expression is { } expression)
|
||||||
|
{
|
||||||
|
return target.GetValueStore().AddBinding(property, expression);
|
||||||
|
}
|
||||||
|
|
||||||
var mode = binding.Mode;
|
var mode = binding.Mode;
|
||||||
|
|
||||||
if (mode == BindingMode.Default)
|
if (mode == BindingMode.Default)
|
||||||
@@ -83,6 +83,24 @@ namespace Avalonia.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies an <see cref="InstancedBinding"/> a property on an <see cref="AvaloniaObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">The target object.</param>
|
||||||
|
/// <param name="property">The property to bind.</param>
|
||||||
|
/// <param name="binding">The instanced binding.</param>
|
||||||
|
/// <param name="anchor">Obsolete, unused.</param>
|
||||||
|
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
|
||||||
|
[Obsolete("Use the Apply(AvaloniaObject, AvaloniaProperty, InstancedBinding) overload.")]
|
||||||
|
public static IDisposable Apply(
|
||||||
|
AvaloniaObject target,
|
||||||
|
AvaloniaProperty property,
|
||||||
|
InstancedBinding binding,
|
||||||
|
object? anchor)
|
||||||
|
{
|
||||||
|
return Apply(target, property, binding);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class TwoWayBindingDisposable : IDisposable
|
private sealed class TwoWayBindingDisposable : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDisposable _toTargetSubscription;
|
private readonly IDisposable _toTargetSubscription;
|
||||||
|
|||||||
@@ -415,6 +415,45 @@ namespace Avalonia.Data
|
|||||||
e);
|
e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
|
||||||
|
/// <see cref="AvaloniaProperty.UnsetValue"/>, <see cref="BindingOperations.DoNothing"/> and
|
||||||
|
/// <see cref="BindingNotification"/> without type conversion.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The untyped value.</param>
|
||||||
|
/// <returns>The typed binding value.</returns>
|
||||||
|
internal static BindingValue<T> FromUntypedStrict(object? value)
|
||||||
|
{
|
||||||
|
if (value == AvaloniaProperty.UnsetValue)
|
||||||
|
return Unset;
|
||||||
|
else if (value == BindingOperations.DoNothing)
|
||||||
|
return DoNothing;
|
||||||
|
|
||||||
|
var type = BindingValueType.Value;
|
||||||
|
T? v = default;
|
||||||
|
Exception? error = null;
|
||||||
|
|
||||||
|
if (value is BindingNotification n)
|
||||||
|
{
|
||||||
|
error = n.Error;
|
||||||
|
type = n.ErrorType switch
|
||||||
|
{
|
||||||
|
BindingErrorType.Error => BindingValueType.BindingError,
|
||||||
|
BindingErrorType.DataValidationError => BindingValueType.DataValidationError,
|
||||||
|
_ => BindingValueType.Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (n.HasValue)
|
||||||
|
type |= BindingValueType.HasValue;
|
||||||
|
value = n.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type & BindingValueType.HasValue) != 0)
|
||||||
|
v = (T)value!;
|
||||||
|
|
||||||
|
return new BindingValue<T>(type, v, error);
|
||||||
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
[Conditional("DEBUG")]
|
||||||
private static void ValidateValue(T value)
|
private static void ValidateValue(T value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Reactive;
|
|
||||||
|
|
||||||
namespace Avalonia.Data.Core
|
|
||||||
{
|
|
||||||
internal class AvaloniaPropertyAccessorNode : SettableNode
|
|
||||||
{
|
|
||||||
private IDisposable? _subscription;
|
|
||||||
private readonly bool _enableValidation;
|
|
||||||
private readonly AvaloniaProperty _property;
|
|
||||||
|
|
||||||
public AvaloniaPropertyAccessorNode(AvaloniaProperty property, bool enableValidation)
|
|
||||||
{
|
|
||||||
_property = property;
|
|
||||||
_enableValidation = enableValidation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string? Description => PropertyName;
|
|
||||||
public string? PropertyName { get; }
|
|
||||||
public override Type PropertyType => _property.PropertyType;
|
|
||||||
|
|
||||||
protected override bool SetTargetValueCore(object? value, BindingPriority priority)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Target.TryGetTarget(out var target) && target is AvaloniaObject obj)
|
|
||||||
{
|
|
||||||
obj.SetValue(_property, value, priority);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void StartListeningCore(WeakReference<object?> reference)
|
|
||||||
{
|
|
||||||
if (reference.TryGetTarget(out var target) && target is AvaloniaObject obj)
|
|
||||||
{
|
|
||||||
_subscription = new AvaloniaPropertyObservable<object?,object?>(obj, _property).Subscribe(ValueChanged);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_subscription = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void StopListeningCore()
|
|
||||||
{
|
|
||||||
_subscription?.Dispose();
|
|
||||||
_subscription = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Avalonia.Data.Core;
|
||||||
|
|
||||||
|
internal class BindingError
|
||||||
|
{
|
||||||
|
public BindingError(Exception exception, BindingErrorType errorType)
|
||||||
|
{
|
||||||
|
Debug.Assert(errorType != BindingErrorType.None);
|
||||||
|
|
||||||
|
Exception = exception;
|
||||||
|
ErrorType = errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception Exception { get; }
|
||||||
|
public BindingErrorType ErrorType { get; }
|
||||||
|
}
|
||||||
@@ -1,389 +1,565 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia.Reactive;
|
using System.Linq.Expressions;
|
||||||
|
using System.Text;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Data.Core.ExpressionNodes;
|
||||||
|
using Avalonia.Data.Core.Parsers;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Logging;
|
using Avalonia.Logging;
|
||||||
using Avalonia.Utilities;
|
using Avalonia.Utilities;
|
||||||
|
|
||||||
namespace Avalonia.Data.Core
|
namespace Avalonia.Data.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A binding expression which accepts and produces (possibly boxed) object values.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A <see cref="BindingExpression"/> represents a untyped binding which has been
|
||||||
|
/// instantiated on an object.
|
||||||
|
/// </remarks>
|
||||||
|
internal partial class BindingExpression : UntypedBindingExpressionBase, IDescription, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly List<ExpressionNode> s_emptyExpressionNodes = new();
|
||||||
|
private readonly WeakReference<object?>? _source;
|
||||||
|
private readonly BindingMode _mode;
|
||||||
|
private readonly List<ExpressionNode> _nodes;
|
||||||
|
private readonly TargetTypeConverter? _targetTypeConverter;
|
||||||
|
private readonly UncommonFields? _uncommon;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds to an expression on an object using a type value converter to convert the values
|
/// Initializes a new instance of the <see cref="BindingExpression"/> class.
|
||||||
/// that are sent and received.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]
|
/// <param name="source">The source from which the value will be read.</param>
|
||||||
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
|
/// <param name="nodes">The nodes representing the binding path.</param>
|
||||||
|
/// <param name="fallbackValue">
|
||||||
|
/// The fallback value. Pass <see cref="AvaloniaProperty.UnsetValue"/> for no fallback.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="converter">The converter to use.</param>
|
||||||
|
/// <param name="converterCulture">The converter culture to use.</param>
|
||||||
|
/// <param name="converterParameter">The converter parameter.</param>
|
||||||
|
/// <param name="enableDataValidation">
|
||||||
|
/// Whether data validation should be enabled for the binding.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="mode">The binding mode.</param>
|
||||||
|
/// <param name="priority">The binding priority.</param>
|
||||||
|
/// <param name="stringFormat">The format string to use.</param>
|
||||||
|
/// <param name="targetNullValue">The null target value.</param>
|
||||||
|
/// <param name="targetTypeConverter">
|
||||||
|
/// A final type converter to be run on the produced value.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="updateSourceTrigger">The trigger for updating the source value.</param>
|
||||||
|
public BindingExpression(
|
||||||
|
object? source,
|
||||||
|
List<ExpressionNode>? nodes,
|
||||||
|
object? fallbackValue,
|
||||||
|
IValueConverter? converter = null,
|
||||||
|
CultureInfo? converterCulture = null,
|
||||||
|
object? converterParameter = null,
|
||||||
|
bool enableDataValidation = false,
|
||||||
|
BindingMode mode = BindingMode.OneWay,
|
||||||
|
BindingPriority priority = BindingPriority.LocalValue,
|
||||||
|
string? stringFormat = null,
|
||||||
|
object? targetNullValue = null,
|
||||||
|
TargetTypeConverter? targetTypeConverter = null,
|
||||||
|
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
|
||||||
|
: base(priority, enableDataValidation)
|
||||||
{
|
{
|
||||||
private readonly ExpressionObserver _inner;
|
if (mode == BindingMode.Default)
|
||||||
private readonly Type _targetType;
|
throw new ArgumentException("Binding mode cannot be Default.", nameof(mode));
|
||||||
private readonly object? _fallbackValue;
|
if (updateSourceTrigger == UpdateSourceTrigger.Default)
|
||||||
private readonly object? _targetNullValue;
|
throw new ArgumentException("UpdateSourceTrigger cannot be Default.", nameof(updateSourceTrigger));
|
||||||
private readonly BindingPriority _priority;
|
|
||||||
InnerListener? _innerListener;
|
|
||||||
WeakReference<object>? _value;
|
|
||||||
|
|
||||||
/// <summary>
|
if (source == AvaloniaProperty.UnsetValue)
|
||||||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
source = null;
|
||||||
/// </summary>
|
|
||||||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
_source = new(source);
|
||||||
/// <param name="targetType">The type to convert the value to.</param>
|
_mode = mode;
|
||||||
public BindingExpression(ExpressionObserver inner, Type targetType)
|
_nodes = nodes ?? s_emptyExpressionNodes;
|
||||||
: this(inner, targetType, DefaultValueConverter.Instance, CultureInfo.InvariantCulture)
|
_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()
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
_ = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the converter to use on the expression.
|
|
||||||
/// </summary>
|
|
||||||
public IValueConverter Converter { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the culture in which to evaluate the converter.
|
|
||||||
/// </summary>
|
|
||||||
public CultureInfo ConverterCulture { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a parameter to pass to <see cref="Converter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public object? ConverterParameter { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
string? IDescription.Description => _inner.Expression;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnCompleted()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnError(Exception error)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void OnNext(object? value)
|
|
||||||
{
|
|
||||||
if (value == BindingOperations.DoNothing)
|
|
||||||
{
|
{
|
||||||
return;
|
_converter = converter,
|
||||||
}
|
_converterCulture = converterCulture,
|
||||||
|
_converterParameter = converterParameter,
|
||||||
using (_inner.Subscribe(_ => { }))
|
_fallbackValue = fallbackValue,
|
||||||
{
|
_stringFormat = stringFormat switch
|
||||||
var type = _inner.ResultType;
|
|
||||||
|
|
||||||
if (type != null)
|
|
||||||
{
|
{
|
||||||
var converted = Converter.ConvertBack(
|
string s when string.IsNullOrWhiteSpace(s) => null,
|
||||||
value,
|
string s when !s.Contains('{') => $"{{0:{stringFormat}}}",
|
||||||
type,
|
_ => stringFormat,
|
||||||
ConverterParameter,
|
},
|
||||||
ConverterCulture);
|
_targetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
|
||||||
|
_updateSourceTrigger = updateSourceTrigger,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (converted == BindingOperations.DoNothing)
|
IPropertyAccessorNode? leafAccessor = null;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (converted == AvaloniaProperty.UnsetValue)
|
if (nodes is not null)
|
||||||
{
|
{
|
||||||
converted = TypeUtilities.Default(type);
|
for (var i = 0; i < nodes.Count; ++i)
|
||||||
_inner.SetValue(converted, _priority);
|
{
|
||||||
}
|
var node = nodes[i];
|
||||||
else if (converted is BindingNotification notification)
|
node.SetOwner(this, i);
|
||||||
{
|
if (node is IPropertyAccessorNode n)
|
||||||
if (notification.ErrorType == BindingErrorType.None)
|
leafAccessor = n;
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Initialize() => _innerListener = new InnerListener(this);
|
if (enableDataValidation)
|
||||||
protected override void Deinitialize() => _innerListener?.Dispose();
|
leafAccessor?.EnableDataValidation();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Subscribed(IObserver<object> observer, bool first)
|
public override string Description
|
||||||
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
if (!first && _value != null && _value.TryGetTarget(out var val))
|
var b = new StringBuilder();
|
||||||
{
|
LeafNode.BuildString(b, _nodes);
|
||||||
observer.OnNext(val);
|
return b.ToString();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
private object? ConvertValue(object? value)
|
|
||||||
{
|
|
||||||
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
|
|
||||||
{
|
|
||||||
return _targetNullValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == BindingOperations.DoNothing)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = value as BindingNotification;
|
|
||||||
|
|
||||||
if (notification == null)
|
|
||||||
{
|
|
||||||
var converted = Converter.Convert(
|
|
||||||
value,
|
|
||||||
_targetType,
|
|
||||||
ConverterParameter,
|
|
||||||
ConverterCulture);
|
|
||||||
|
|
||||||
if (converted == BindingOperations.DoNothing)
|
|
||||||
{
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
|
|
||||||
notification = converted as BindingNotification;
|
|
||||||
|
|
||||||
if (notification?.ErrorType == BindingErrorType.None)
|
|
||||||
{
|
|
||||||
converted = notification.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
|
|
||||||
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
|
|
||||||
{
|
|
||||||
var fallback = ConvertFallback();
|
|
||||||
converted = Merge(converted, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return converted;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ConvertValue(notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BindingNotification ConvertValue(BindingNotification notification)
|
|
||||||
{
|
|
||||||
if (notification.HasValue)
|
|
||||||
{
|
|
||||||
var converted = ConvertValue(notification.Value);
|
|
||||||
notification = Merge(notification, converted);
|
|
||||||
}
|
|
||||||
else if (_fallbackValue != AvaloniaProperty.UnsetValue)
|
|
||||||
{
|
|
||||||
var fallback = ConvertFallback();
|
|
||||||
notification = Merge(notification, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BindingNotification ConvertFallback()
|
|
||||||
{
|
|
||||||
object? converted;
|
|
||||||
|
|
||||||
if (_fallbackValue == AvaloniaProperty.UnsetValue)
|
|
||||||
{
|
|
||||||
throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BindingNotification Merge(object a, BindingNotification b)
|
|
||||||
{
|
|
||||||
var an = a as BindingNotification;
|
|
||||||
|
|
||||||
if (an != null)
|
|
||||||
{
|
|
||||||
Merge(an, b);
|
|
||||||
return an;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BindingNotification Merge(BindingNotification a, object? b)
|
|
||||||
{
|
|
||||||
var bn = b as BindingNotification;
|
|
||||||
|
|
||||||
if (bn != null)
|
|
||||||
{
|
|
||||||
Merge(a, bn);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
a.SetValue(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BindingNotification Merge(BindingNotification a, BindingNotification b)
|
|
||||||
{
|
|
||||||
if (b.HasValue)
|
|
||||||
{
|
|
||||||
a.SetValue(b.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
a.ClearValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b.Error != null)
|
|
||||||
{
|
|
||||||
a.AddError(b.Error, b.ErrorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InnerListener : IObserver<object?>, IDisposable
|
|
||||||
{
|
|
||||||
private readonly BindingExpression _owner;
|
|
||||||
private readonly IDisposable _dispose;
|
|
||||||
|
|
||||||
public InnerListener(BindingExpression owner)
|
|
||||||
{
|
|
||||||
_owner = owner;
|
|
||||||
_dispose = owner._inner.Subscribe(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() => _dispose.Dispose();
|
|
||||||
public void OnCompleted() => _owner.PublishCompleted();
|
|
||||||
public void OnError(Exception error) => _owner.PublishError(error);
|
|
||||||
|
|
||||||
public void OnNext(object? value)
|
|
||||||
{
|
|
||||||
if (value == BindingOperations.DoNothing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var converted = _owner.ConvertValue(value);
|
|
||||||
|
|
||||||
if (converted == BindingOperations.DoNothing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_owner._value = converted is not null ? new WeakReference<object>(converted) : null;
|
|
||||||
_owner.PublishNext(converted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void UpdateTarget()
|
||||||
|
{
|
||||||
|
if (_nodes.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var source = _nodes[0].Source;
|
||||||
|
|
||||||
|
for (var i = 0; i < _nodes.Count; ++i)
|
||||||
|
_nodes[i].SetSource(null, null);
|
||||||
|
|
||||||
|
_nodes[0].SetSource(source, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
// The leaf node has changed. If the binding mode is not OneWayToSource, publish the
|
||||||
|
// value to the target.
|
||||||
|
if (_mode != BindingMode.OneWayToSource)
|
||||||
|
{
|
||||||
|
var error = dataValidationError is not null ?
|
||||||
|
new BindingError(dataValidationError, BindingErrorType.DataValidationError) :
|
||||||
|
null;
|
||||||
|
ConvertAndPublishValue(value, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the binding mode is OneTime, then stop the binding if a valid value was published.
|
||||||
|
if (_mode == BindingMode.OneTime && GetValue() != AvaloniaProperty.UnsetValue)
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
|
||||||
|
{
|
||||||
|
// When the binding mode is OneWayToSource, we need to write the value to the source
|
||||||
|
// when the object holding the source property changes; this is node before the leaf
|
||||||
|
// node. First update the leaf node's source, then write the value to its property.
|
||||||
|
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
|
||||||
|
WriteTargetValueToSource();
|
||||||
|
}
|
||||||
|
else if (value is null)
|
||||||
|
{
|
||||||
|
OnNodeError(nodeIndex, "Value is null.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_nodes[nodeIndex + 1].SetSource(value, dataValidationError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
if (_mode == BindingMode.OneWayToSource)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var errorPoint = CalculateErrorPoint(nodeIndex);
|
||||||
|
|
||||||
|
if (ShouldLogError(out var target))
|
||||||
|
Log(target, error, errorPoint);
|
||||||
|
|
||||||
|
// Clear the current value and publish the error.
|
||||||
|
var bindingError = new BindingError(
|
||||||
|
new BindingChainException(error, Description, errorPoint.ToString()),
|
||||||
|
BindingErrorType.Error);
|
||||||
|
ConvertAndPublishValue(AvaloniaProperty.UnsetValue, bindingError);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnDataValidationError(Exception error)
|
||||||
|
{
|
||||||
|
var bindingError = new BindingError(error, BindingErrorType.DataValidationError);
|
||||||
|
PublishValue(UnchangedValue, bindingError);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override bool WriteValueToSource(object? value)
|
||||||
|
{
|
||||||
|
if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Converter is { } converter &&
|
||||||
|
value != AvaloniaProperty.UnsetValue &&
|
||||||
|
value != BindingOperations.DoNothing)
|
||||||
|
{
|
||||||
|
value = ConvertBack(converter, ConverterCulture, ConverterParameter, value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (_targetTypeConverter.TryConvert(value, type, ConverterCulture, out var converted))
|
||||||
|
{
|
||||||
|
value = converted;
|
||||||
|
}
|
||||||
|
else if (FallbackValue != AvaloniaProperty.UnsetValue)
|
||||||
|
{
|
||||||
|
value = FallbackValue;
|
||||||
|
}
|
||||||
|
else if (IsDataValidationEnabled)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void StartCore()
|
||||||
|
{
|
||||||
|
if (_source?.TryGetTarget(out var source) == true)
|
||||||
|
{
|
||||||
|
if (_nodes.Count > 0)
|
||||||
|
_nodes[0].SetSource(source, null);
|
||||||
|
else
|
||||||
|
ConvertAndPublishValue(source, null);
|
||||||
|
|
||||||
|
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
|
||||||
|
TryGetTarget(out var target) &&
|
||||||
|
TargetProperty is not null)
|
||||||
|
{
|
||||||
|
if (_mode is BindingMode.OneWayToSource)
|
||||||
|
PublishValue(target.GetValue(TargetProperty));
|
||||||
|
|
||||||
|
var trigger = UpdateSourceTrigger;
|
||||||
|
|
||||||
|
if (trigger is UpdateSourceTrigger.PropertyChanged)
|
||||||
|
target.PropertyChanged += OnTargetPropertyChanged;
|
||||||
|
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
|
||||||
|
ie.LostFocus += OnTargetLostFocus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnNodeError(-1, "Binding Source is null.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void StopCore()
|
||||||
|
{
|
||||||
|
foreach (var node in _nodes)
|
||||||
|
node.Reset();
|
||||||
|
|
||||||
|
if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
|
||||||
|
TryGetTarget(out var target))
|
||||||
|
{
|
||||||
|
var trigger = UpdateSourceTrigger;
|
||||||
|
|
||||||
|
if (trigger is UpdateSourceTrigger.PropertyChanged)
|
||||||
|
target.PropertyChanged -= OnTargetPropertyChanged;
|
||||||
|
else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
|
||||||
|
ie.LostFocus -= OnTargetLostFocus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 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)
|
||||||
|
{
|
||||||
|
value = Convert(converter, ConverterCulture, ConverterParameter, value, TargetType, ref error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check this here as the converter may return DoNothing.
|
||||||
|
if (value == BindingOperations.DoNothing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TargetNullValue only applies when the value is null: UnsetValue indicates that there
|
||||||
|
// was a binding error so we don't want to use TargetNullValue in that case.
|
||||||
|
if (value is null && TargetNullValue != AvaloniaProperty.UnsetValue)
|
||||||
|
{
|
||||||
|
value = ConvertFallback(TargetNullValue, nameof(TargetNullValue));
|
||||||
|
isTargetNullValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a value, try to convert it to the target type.
|
||||||
|
if (value != AvaloniaProperty.UnsetValue)
|
||||||
|
{
|
||||||
|
if (StringFormat is { } stringFormat &&
|
||||||
|
(TargetType == typeof(object) || TargetType == typeof(string)) &&
|
||||||
|
!isTargetNullValue)
|
||||||
|
{
|
||||||
|
// The string format applies if we're targeting a type that can accept a string
|
||||||
|
// and the value isn't the TargetNullValue.
|
||||||
|
value = string.Format(ConverterCulture, stringFormat, value);
|
||||||
|
}
|
||||||
|
else if (_targetTypeConverter is not null)
|
||||||
|
{
|
||||||
|
// Otherwise, if we have a target type converter, convert the value to the target type.
|
||||||
|
value = ConvertFrom(_targetTypeConverter, value, ref error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FallbackValue applies if the result from the binding, converter or target type converter
|
||||||
|
// is UnsetValue.
|
||||||
|
if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue)
|
||||||
|
value = ConvertFallback(FallbackValue, nameof(FallbackValue));
|
||||||
|
|
||||||
|
// Publish the value.
|
||||||
|
PublishValue(value, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteTargetValueToSource()
|
||||||
|
{
|
||||||
|
Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
|
||||||
|
|
||||||
|
if (TryGetTarget(out var target) &&
|
||||||
|
TargetProperty is not null &&
|
||||||
|
target.GetValue(TargetProperty) is var value &&
|
||||||
|
!TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType))
|
||||||
|
{
|
||||||
|
WriteValueToSource(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTargetLostFocus(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.LostFocus);
|
||||||
|
|
||||||
|
WriteTargetValueToSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? ConvertFallback(object? fallback, string fallbackName)
|
||||||
|
{
|
||||||
|
if (_targetTypeConverter is null || TargetType == typeof(object) || fallback == AvaloniaProperty.UnsetValue)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
if (TryGetTarget(out var target))
|
||||||
|
Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error);
|
||||||
|
|
||||||
|
return AvaloniaProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? ConvertFrom(TargetTypeConverter? converter, object? value, ref BindingError? error)
|
||||||
|
{
|
||||||
|
if (converter is null)
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (converter.TryConvert(value, TargetType, ConverterCulture, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var valueString = value?.ToString() ?? "(null)";
|
||||||
|
var valueTypeName = value?.GetType().FullName ?? "null";
|
||||||
|
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'.";
|
||||||
|
|
||||||
|
if (ShouldLogError(out var target))
|
||||||
|
Log(target, message, LogEventLevel.Warning);
|
||||||
|
|
||||||
|
error = new(new InvalidCastException(message), BindingErrorType.Error);
|
||||||
|
return AvaloniaProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uncommonly used fields are separated out to reduce memory usage.
|
||||||
|
/// </summary>
|
||||||
|
private class UncommonFields
|
||||||
|
{
|
||||||
|
public IValueConverter? _converter;
|
||||||
|
public object? _converterParameter;
|
||||||
|
public CultureInfo? _converterCulture;
|
||||||
|
public object? _fallbackValue;
|
||||||
|
public string? _stringFormat;
|
||||||
|
public object? _targetNullValue;
|
||||||
|
public UpdateSourceTrigger _updateSourceTrigger;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -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,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using Avalonia.Utilities;
|
using Avalonia.Utilities;
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
@@ -11,7 +9,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
|
internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
public bool Match(object obj, string propertyName)
|
public bool Match(object obj, string propertyName)
|
||||||
{
|
{
|
||||||
if (obj is AvaloniaObject o)
|
if (obj is AvaloniaObject o)
|
||||||
@@ -31,7 +28,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
||||||
/// property will be made.
|
/// property will be made.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
|
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
|
||||||
{
|
{
|
||||||
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||||
|
|||||||
@@ -1,28 +1,50 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Holds a registry of plugins used for bindings.
|
/// Holds a registry of plugins used for bindings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
||||||
public static class BindingPlugins
|
public static class BindingPlugins
|
||||||
{
|
{
|
||||||
|
internal static readonly List<IPropertyAccessorPlugin> s_propertyAccessors = new()
|
||||||
|
{
|
||||||
|
new AvaloniaPropertyAccessorPlugin(),
|
||||||
|
new ReflectionMethodAccessorPlugin(),
|
||||||
|
new InpcPropertyAccessorPlugin(),
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static readonly List<IDataValidationPlugin> s_dataValidators = new()
|
||||||
|
{
|
||||||
|
new DataAnnotationsValidationPlugin(),
|
||||||
|
new IndeiValidationPlugin(),
|
||||||
|
new ExceptionValidationPlugin(),
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static readonly List<IStreamPlugin> s_streamHandlers = new()
|
||||||
|
{
|
||||||
|
new TaskStreamPlugin(),
|
||||||
|
new ObservableStreamPlugin(),
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An ordered collection of property accessor plugins that can be used to customize
|
/// An ordered collection of property accessor plugins that can be used to customize
|
||||||
/// the reading and subscription of property values on a type.
|
/// the reading and subscription of property values on a type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IList<IPropertyAccessorPlugin> PropertyAccessors => ExpressionObserver.PropertyAccessors;
|
public static IList<IPropertyAccessorPlugin> PropertyAccessors => s_propertyAccessors;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An ordered collection of validation checker plugins that can be used to customize
|
/// An ordered collection of validation checker plugins that can be used to customize
|
||||||
/// the validation of view model and model data.
|
/// the validation of view model and model data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IList<IDataValidationPlugin> DataValidators => ExpressionObserver.DataValidators;
|
public static IList<IDataValidationPlugin> DataValidators => s_dataValidators;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An ordered collection of stream plugins that can be used to customize the behavior
|
/// An ordered collection of stream plugins that can be used to customize the behavior
|
||||||
/// of the '^' stream binding operator.
|
/// of the '^' stream binding operator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IList<IStreamPlugin> StreamHandlers => ExpressionObserver.StreamHandlers;
|
public static IList<IStreamPlugin> StreamHandlers => s_streamHandlers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
|
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
||||||
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
|
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public bool Match(WeakReference<object?> reference, string memberName)
|
public bool Match(WeakReference<object?> reference, string memberName)
|
||||||
{
|
{
|
||||||
reference.TryGetTarget(out var target);
|
reference.TryGetTarget(out var target);
|
||||||
@@ -26,7 +26,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
|
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
|
||||||
{
|
{
|
||||||
return new Accessor(reference, name, inner);
|
return new Accessor(reference, name, inner);
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
public class ExceptionValidationPlugin : IDataValidationPlugin
|
public class ExceptionValidationPlugin : IDataValidationPlugin
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public bool Match(WeakReference<object?> reference, string memberName) => true;
|
public bool Match(WeakReference<object?> reference, string memberName) => true;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
|
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
|
||||||
{
|
{
|
||||||
return new Validator(reference, name, inner);
|
return new Validator(reference, name, inner);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines how data validation is observed by an <see cref="ExpressionObserver"/>.
|
/// Defines how data validation is observed by an <see cref="BindingExpression"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDataValidationPlugin
|
public interface IDataValidationPlugin
|
||||||
{
|
{
|
||||||
@@ -14,7 +13,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <param name="reference">A weak reference to the object.</param>
|
/// <param name="reference">A weak reference to the object.</param>
|
||||||
/// <param name="memberName">The name of the member to validate.</param>
|
/// <param name="memberName">The name of the member to validate.</param>
|
||||||
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
|
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
bool Match(WeakReference<object?> reference, string memberName);
|
bool Match(WeakReference<object?> reference, string memberName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -27,7 +25,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
||||||
/// property will be made.
|
/// property will be made.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
IPropertyAccessor Start(WeakReference<object?> reference,
|
IPropertyAccessor Start(WeakReference<object?> reference,
|
||||||
string propertyName,
|
string propertyName,
|
||||||
IPropertyAccessor inner);
|
IPropertyAccessor inner);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines how a member is read, written and observed by an
|
/// Defines how a member is read, written and observed by a binding.
|
||||||
/// <see cref="ExpressionObserver"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPropertyAccessorPlugin
|
public interface IPropertyAccessorPlugin
|
||||||
{
|
{
|
||||||
@@ -15,7 +13,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <param name="obj">The object.</param>
|
/// <param name="obj">The object.</param>
|
||||||
/// <param name="propertyName">The property name.</param>
|
/// <param name="propertyName">The property name.</param>
|
||||||
/// <returns>True if the plugin can handle the property on the object; otherwise false.</returns>
|
/// <returns>True if the plugin can handle the property on the object; otherwise false.</returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
bool Match(object obj, string propertyName);
|
bool Match(object obj, string propertyName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -27,7 +24,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
||||||
/// property will be made.
|
/// property will be made.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
IPropertyAccessor? Start(WeakReference<object?> reference,
|
IPropertyAccessor? Start(WeakReference<object?> reference,
|
||||||
string propertyName);
|
string propertyName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
{
|
{
|
||||||
@@ -13,7 +12,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reference">A weak reference to the value.</param>
|
/// <param name="reference">A weak reference to the value.</param>
|
||||||
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
bool Match(WeakReference<object?> reference);
|
bool Match(WeakReference<object?> reference);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -23,7 +21,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// An observable that produces the output for the value.
|
/// An observable that produces the output for the value.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
IObservable<object?> Start(WeakReference<object?> reference);
|
IObservable<object?> Start(WeakReference<object?> reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia.Utilities;
|
using Avalonia.Utilities;
|
||||||
|
|
||||||
@@ -19,7 +18,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public bool Match(WeakReference<object?> reference, string memberName)
|
public bool Match(WeakReference<object?> reference, string memberName)
|
||||||
{
|
{
|
||||||
reference.TryGetTarget(out var target);
|
reference.TryGetTarget(out var target);
|
||||||
@@ -28,7 +26,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor accessor)
|
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor accessor)
|
||||||
{
|
{
|
||||||
return new Validator(reference, name, accessor);
|
return new Validator(reference, name, accessor);
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// Reads a property from a standard C# object that optionally supports the
|
/// Reads a property from a standard C# object that optionally supports the
|
||||||
/// <see cref="INotifyPropertyChanged"/> interface.
|
/// <see cref="INotifyPropertyChanged"/> interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
||||||
internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
|
internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
|
||||||
{
|
{
|
||||||
private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup =
|
private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup =
|
||||||
new Dictionary<(Type, string), PropertyInfo?>();
|
new Dictionary<(Type, string), PropertyInfo?>();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null;
|
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,7 +29,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
|
||||||
/// property will be made.
|
/// property will be made.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
|
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
|
||||||
{
|
{
|
||||||
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||||
@@ -55,14 +54,13 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
private const BindingFlags PropertyBindingFlags =
|
private const BindingFlags PropertyBindingFlags =
|
||||||
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
|
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
|
||||||
|
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
|
||||||
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
|
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
|
||||||
{
|
{
|
||||||
if (instance is IReflectableType reflectableType && instance is not Type)
|
if (instance is IReflectableType reflectableType && instance is not Type)
|
||||||
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
|
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
|
||||||
|
|
||||||
var type = instance.GetType();
|
var type = instance.GetType();
|
||||||
|
|
||||||
var key = (type, propertyName);
|
var key = (type, propertyName);
|
||||||
|
|
||||||
if (!_propertyLookup.TryGetValue(key, out var propertyInfo))
|
if (!_propertyLookup.TryGetValue(key, out var propertyInfo))
|
||||||
@@ -80,7 +78,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
|
|
||||||
var properties = type.GetProperties(PropertyBindingFlags);
|
var properties = type.GetProperties(PropertyBindingFlags);
|
||||||
|
|
||||||
foreach (PropertyInfo propertyInfo in properties)
|
foreach (var propertyInfo in properties)
|
||||||
{
|
{
|
||||||
if (propertyInfo.Name == propertyName)
|
if (propertyInfo.Name == propertyName)
|
||||||
{
|
{
|
||||||
@@ -95,6 +93,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
||||||
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
|
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
|
||||||
{
|
{
|
||||||
private readonly WeakReference<object?> _reference;
|
private readonly WeakReference<object?> _reference;
|
||||||
@@ -117,7 +116,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
var o = GetReferenceTarget();
|
var o = GetReferenceTarget();
|
||||||
return (o != null) ? _property.GetValue(o) : null;
|
return o != null ? _property.GetValue(o) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +124,12 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
{
|
{
|
||||||
if (_property.CanWrite)
|
if (_property.CanWrite)
|
||||||
{
|
{
|
||||||
|
if (!TypeUtilities.TryConvert(_property.PropertyType, value, null, out var converted))
|
||||||
|
throw new ArgumentException($"Object of type '{value?.GetType()}' " +
|
||||||
|
$"cannot be converted to type '{_property.PropertyType}'.");
|
||||||
|
|
||||||
_eventRaised = false;
|
_eventRaised = false;
|
||||||
_property.SetValue(GetReferenceTarget(), value);
|
_property.SetValue(GetReferenceTarget(), converted);
|
||||||
|
|
||||||
if (!_eventRaised)
|
if (!_eventRaised)
|
||||||
{
|
{
|
||||||
@@ -159,7 +162,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
{
|
{
|
||||||
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
|
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
|
||||||
|
|
||||||
if (inpc != null)
|
if (inpc != null)
|
||||||
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
|
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +187,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
{
|
{
|
||||||
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
|
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
|
||||||
|
|
||||||
if (inpc != null)
|
if (inpc != null)
|
||||||
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
|
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,67 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins;
|
||||||
|
|
||||||
|
internal class MethodAccessorPlugin : IPropertyAccessorPlugin
|
||||||
{
|
{
|
||||||
internal class MethodAccessorPlugin : IPropertyAccessorPlugin
|
private readonly MethodInfo _method;
|
||||||
|
private readonly Type _delegateType;
|
||||||
|
|
||||||
|
public MethodAccessorPlugin(MethodInfo method, Type delegateType)
|
||||||
{
|
{
|
||||||
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
|
_method = method;
|
||||||
new Dictionary<(Type, string), MethodInfo?>();
|
_delegateType = delegateType;
|
||||||
|
}
|
||||||
|
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
public bool Match(object obj, string propertyName)
|
||||||
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
|
{
|
||||||
|
throw new InvalidOperationException("The MethodAccessorPlugin does not support dynamic matching");
|
||||||
|
}
|
||||||
|
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
|
public IPropertyAccessor Start(WeakReference<object?> reference, string propertyName)
|
||||||
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName)
|
{
|
||||||
|
Debug.Assert(_method.Name == propertyName);
|
||||||
|
return new Accessor(reference, _method, _delegateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Accessor : PropertyAccessorBase
|
||||||
|
{
|
||||||
|
public Accessor(WeakReference<object?> reference, MethodInfo method, Type delegateType)
|
||||||
{
|
{
|
||||||
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
_ = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||||
_ = methodName ?? throw new ArgumentNullException(nameof(methodName));
|
_ = method ?? throw new ArgumentNullException(nameof(method));
|
||||||
|
|
||||||
if (!reference.TryGetTarget(out var instance) || instance is null)
|
PropertyType = delegateType;
|
||||||
return null;
|
|
||||||
|
|
||||||
var method = GetFirstMethodWithName(instance.GetType(), methodName);
|
if (method.IsStatic)
|
||||||
|
|
||||||
if (method is not null)
|
|
||||||
{
|
{
|
||||||
return new Accessor(reference, method);
|
Value = method.CreateDelegate(PropertyType);
|
||||||
}
|
}
|
||||||
else
|
else if (reference.TryGetTarget(out var target))
|
||||||
{
|
{
|
||||||
var message = $"Could not find CLR method '{methodName}' on '{instance}'";
|
Value = method.CreateDelegate(PropertyType, target);
|
||||||
var exception = new MissingMemberException(message);
|
|
||||||
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodInfo? GetFirstMethodWithName(
|
public override Type? PropertyType { get; }
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
|
|
||||||
|
public override object? Value { get; }
|
||||||
|
|
||||||
|
public override bool SetValue(object? value, BindingPriority priority) => false;
|
||||||
|
|
||||||
|
protected override void SubscribeCore()
|
||||||
{
|
{
|
||||||
var key = (type, methodName);
|
try
|
||||||
|
|
||||||
if (!_methodLookup.TryGetValue(key, out var methodInfo))
|
|
||||||
{
|
{
|
||||||
methodInfo = TryFindAndCacheMethod(type, methodName);
|
PublishValue(Value);
|
||||||
}
|
}
|
||||||
|
catch { }
|
||||||
return methodInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodInfo? TryFindAndCacheMethod(
|
protected override void UnsubscribeCore()
|
||||||
[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++)
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia.Reactive;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Avalonia.Reactive;
|
||||||
|
|
||||||
namespace Avalonia.Data.Core.Plugins
|
namespace Avalonia.Data.Core.Plugins
|
||||||
{
|
{
|
||||||
@@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
|
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
|
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
||||||
internal class ObservableStreamPlugin : IStreamPlugin
|
internal class ObservableStreamPlugin : IStreamPlugin
|
||||||
{
|
{
|
||||||
private static MethodInfo? s_observableGeneric;
|
private static MethodInfo? s_observableGeneric;
|
||||||
@@ -18,15 +19,14 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")]
|
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")]
|
||||||
public ObservableStreamPlugin()
|
public ObservableStreamPlugin()
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether this plugin handles the specified value.
|
/// Checks whether this plugin handles the specified value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reference">A weak reference to the value.</param>
|
/// <param name="reference">A weak reference to the value.</param>
|
||||||
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public virtual bool Match(WeakReference<object?> reference)
|
public virtual bool Match(WeakReference<object?> reference)
|
||||||
{
|
{
|
||||||
reference.TryGetTarget(out var target);
|
reference.TryGetTarget(out var target);
|
||||||
@@ -43,7 +43,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// An observable that produces the output for the value.
|
/// An observable that produces the output for the value.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public virtual IObservable<object?> Start(WeakReference<object?> reference)
|
public virtual IObservable<object?> Start(WeakReference<object?> reference)
|
||||||
{
|
{
|
||||||
if (!reference.TryGetTarget(out var target) || target is null)
|
if (!reference.TryGetTarget(out var target) || target is null)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
|
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
|
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
|
||||||
|
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
||||||
internal class TaskStreamPlugin : IStreamPlugin
|
internal class TaskStreamPlugin : IStreamPlugin
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -17,7 +18,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reference">A weak reference to the value.</param>
|
/// <param name="reference">A weak reference to the value.</param>
|
||||||
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public virtual bool Match(WeakReference<object?> reference)
|
public virtual bool Match(WeakReference<object?> reference)
|
||||||
{
|
{
|
||||||
reference.TryGetTarget(out var target);
|
reference.TryGetTarget(out var target);
|
||||||
@@ -32,7 +32,6 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// An observable that produces the output for the value.
|
/// An observable that produces the output for the value.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
|
|
||||||
public virtual IObservable<object?> Start(WeakReference<object?> reference)
|
public virtual IObservable<object?> Start(WeakReference<object?> reference)
|
||||||
{
|
{
|
||||||
reference.TryGetTarget(out var target);
|
reference.TryGetTarget(out var target);
|
||||||
@@ -66,7 +65,7 @@ namespace Avalonia.Data.Core.Plugins
|
|||||||
private static IObservable<object?> HandleCompleted(Task task)
|
private static IObservable<object?> HandleCompleted(Task task)
|
||||||
{
|
{
|
||||||
var resultProperty = task.GetType().GetRuntimeProperty("Result");
|
var resultProperty = task.GetType().GetRuntimeProperty("Result");
|
||||||
|
|
||||||
if (resultProperty != null)
|
if (resultProperty != null)
|
||||||
{
|
{
|
||||||
switch (task.Status)
|
switch (task.Status)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Diagnostics;
|
||||||
using Avalonia.Metadata;
|
using Avalonia.Metadata;
|
||||||
|
|
||||||
namespace Avalonia.Data
|
namespace Avalonia.Data
|
||||||
@@ -23,6 +25,7 @@ namespace Avalonia.Data
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
|
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
|
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||||
InstancedBinding? Initiate(
|
InstancedBinding? Initiate(
|
||||||
AvaloniaObject target,
|
AvaloniaObject target,
|
||||||
AvaloniaProperty? targetProperty,
|
AvaloniaProperty? targetProperty,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Avalonia.Reactive;
|
using System;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
|
using Avalonia.Diagnostics;
|
||||||
|
|
||||||
namespace Avalonia.Data
|
namespace Avalonia.Data
|
||||||
{
|
{
|
||||||
internal class IndexerBinding : IBinding
|
internal class IndexerBinding : IBinding2
|
||||||
{
|
{
|
||||||
public IndexerBinding(
|
public IndexerBinding(
|
||||||
AvaloniaObject source,
|
AvaloniaObject source,
|
||||||
@@ -18,16 +20,20 @@ namespace Avalonia.Data
|
|||||||
public AvaloniaProperty Property { get; }
|
public AvaloniaProperty Property { get; }
|
||||||
private BindingMode Mode { get; }
|
private BindingMode Mode { get; }
|
||||||
|
|
||||||
|
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||||
public InstancedBinding? Initiate(
|
public InstancedBinding? Initiate(
|
||||||
AvaloniaObject target,
|
AvaloniaObject target,
|
||||||
AvaloniaProperty? targetProperty,
|
AvaloniaProperty? targetProperty,
|
||||||
object? anchor = null,
|
object? anchor = null,
|
||||||
bool enableDataValidation = false)
|
bool enableDataValidation = false)
|
||||||
{
|
{
|
||||||
var subject = new CombinedSubject<object?>(
|
var expression = new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
|
||||||
new AnonymousObserver<object?>(x => Source.SetValue(Property, x, BindingPriority.LocalValue)),
|
return new InstancedBinding(expression, Mode, BindingPriority.LocalValue);
|
||||||
Source.GetObservable(Property));
|
}
|
||||||
return new InstancedBinding(subject, Mode, BindingPriority.LocalValue);
|
|
||||||
|
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty, object? anchor)
|
||||||
|
{
|
||||||
|
return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
using Avalonia.Reactive;
|
using Avalonia.Reactive;
|
||||||
using ObservableEx = Avalonia.Reactive.Observable;
|
using ObservableEx = Avalonia.Reactive.Observable;
|
||||||
|
|
||||||
@@ -16,6 +17,10 @@ namespace Avalonia.Data
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class InstancedBinding
|
public sealed class InstancedBinding
|
||||||
{
|
{
|
||||||
|
private readonly AvaloniaObject? _target;
|
||||||
|
private readonly UntypedBindingExpressionBase? _expression;
|
||||||
|
private IObservable<object?>? _observable;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
|
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -32,7 +37,29 @@ namespace Avalonia.Data
|
|||||||
{
|
{
|
||||||
Mode = mode;
|
Mode = mode;
|
||||||
Priority = priority;
|
Priority = priority;
|
||||||
Source = source ?? throw new ArgumentNullException(nameof(source));
|
_observable = source ?? throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal InstancedBinding(
|
||||||
|
UntypedBindingExpressionBase source,
|
||||||
|
BindingMode mode,
|
||||||
|
BindingPriority priority)
|
||||||
|
{
|
||||||
|
Mode = mode;
|
||||||
|
Priority = priority;
|
||||||
|
_expression = source ?? throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal InstancedBinding(
|
||||||
|
AvaloniaObject? target,
|
||||||
|
UntypedBindingExpressionBase source,
|
||||||
|
BindingMode mode,
|
||||||
|
BindingPriority priority)
|
||||||
|
{
|
||||||
|
Mode = mode;
|
||||||
|
Priority = priority;
|
||||||
|
_expression = source ?? throw new ArgumentNullException(nameof(source));
|
||||||
|
_target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,11 +75,13 @@ namespace Avalonia.Data
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the binding source observable.
|
/// Gets the binding source observable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IObservable<object?> Source { get; }
|
public IObservable<object?> Source => _observable ??= _expression!.ToObservable(_target);
|
||||||
|
|
||||||
[Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)]
|
[Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public IObservable<object?> Observable => Source;
|
public IObservable<object?> Observable => Source;
|
||||||
|
|
||||||
|
internal UntypedBindingExpressionBase? Expression => _expression;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new one-time binding with a fixed value.
|
/// Creates a new one-time binding with a fixed value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +1,52 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Reactive;
|
using Avalonia.Data.Core;
|
||||||
|
using Avalonia.Logging;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
|
||||||
|
|
||||||
namespace Avalonia.Data
|
namespace Avalonia.Data
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A XAML binding to a property on a control's templated parent.
|
/// A XAML binding to a property on a control's templated parent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TemplateBinding : IObservable<object?>,
|
public partial class TemplateBinding : UntypedBindingExpressionBase,
|
||||||
IBinding,
|
IBinding,
|
||||||
|
IBinding2,
|
||||||
IDescription,
|
IDescription,
|
||||||
IAvaloniaSubject<object?>,
|
|
||||||
ISetterValue,
|
ISetterValue,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
private IObserver<object?>? _observer;
|
|
||||||
private bool _isSetterValue;
|
private bool _isSetterValue;
|
||||||
private StyledElement? _target;
|
|
||||||
private Type? _targetType;
|
|
||||||
private bool _hasProducedValue;
|
|
||||||
|
|
||||||
public TemplateBinding()
|
public TemplateBinding()
|
||||||
|
: base(BindingPriority.Template)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public TemplateBinding(AvaloniaProperty property)
|
public TemplateBinding(AvaloniaProperty property)
|
||||||
|
: base(BindingPriority.Template)
|
||||||
{
|
{
|
||||||
Property = property;
|
Property = property;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable Subscribe(IObserver<object?> observer)
|
|
||||||
{
|
|
||||||
_ = observer ?? throw new ArgumentNullException(nameof(observer));
|
|
||||||
Dispatcher.UIThread.VerifyAccess();
|
|
||||||
|
|
||||||
if (_observer != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("The observable can only be subscribed once.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_observer = observer;
|
|
||||||
Subscribed();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
Unsubscribed();
|
|
||||||
_observer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public InstancedBinding? Initiate(
|
|
||||||
AvaloniaObject target,
|
|
||||||
AvaloniaProperty? targetProperty,
|
|
||||||
object? anchor = null,
|
|
||||||
bool enableDataValidation = false)
|
|
||||||
{
|
|
||||||
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
|
|
||||||
// use the `TemplateBinding` object itself as the instanced binding in order to save
|
|
||||||
// allocating a new object.
|
|
||||||
//
|
|
||||||
// If the binding appears in a `Setter`, then make a clone and instantiate that because
|
|
||||||
// because the setter can outlive the control and cause a leak.
|
|
||||||
if (_target is null && !_isSetterValue)
|
|
||||||
{
|
|
||||||
_target = (StyledElement)target;
|
|
||||||
_targetType = targetProperty?.PropertyType;
|
|
||||||
|
|
||||||
return new InstancedBinding(
|
|
||||||
this,
|
|
||||||
Mode == BindingMode.Default ? BindingMode.OneWay : Mode,
|
|
||||||
BindingPriority.Template);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var clone = new TemplateBinding
|
|
||||||
{
|
|
||||||
Converter = Converter,
|
|
||||||
ConverterParameter = ConverterParameter,
|
|
||||||
Property = Property,
|
|
||||||
Mode = Mode,
|
|
||||||
};
|
|
||||||
|
|
||||||
return clone.Initiate(target, targetProperty, anchor, enableDataValidation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IValueConverter"/> to use.
|
/// Gets or sets the <see cref="IValueConverter"/> to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IValueConverter? Converter { get; set; }
|
public IValueConverter? Converter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the culture in which to evaluate the converter.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The default value is null.</value>
|
||||||
|
/// <remarks>
|
||||||
|
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
|
||||||
|
/// </remarks>
|
||||||
|
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
|
||||||
|
public CultureInfo? ConverterCulture { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
|
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -104,7 +55,11 @@ namespace Avalonia.Data
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binding mode.
|
/// Gets or sets the binding mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BindingMode Mode { get; set; }
|
public new BindingMode Mode
|
||||||
|
{
|
||||||
|
get => base.Mode;
|
||||||
|
set => base.Mode = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the name of the source property on the templated parent.
|
/// Gets or sets the name of the source property on the templated parent.
|
||||||
@@ -112,108 +67,187 @@ namespace Avalonia.Data
|
|||||||
public AvaloniaProperty? Property { get; set; }
|
public AvaloniaProperty? Property { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Description => "TemplateBinding: " + Property;
|
public override string Description => "TemplateBinding: " + Property;
|
||||||
|
|
||||||
void IObserver<object?>.OnCompleted() => throw new NotImplementedException();
|
public IBinding ProvideValue() => this;
|
||||||
void IObserver<object?>.OnError(Exception error) => throw new NotImplementedException();
|
|
||||||
|
|
||||||
void IObserver<object?>.OnNext(object? value)
|
public InstancedBinding? Initiate(
|
||||||
|
AvaloniaObject target,
|
||||||
|
AvaloniaProperty? targetProperty,
|
||||||
|
object? anchor = null,
|
||||||
|
bool enableDataValidation = false)
|
||||||
{
|
{
|
||||||
if (_target?.TemplatedParent is { } templatedParent && Property is not null)
|
return new(target, InstanceCore(), Mode, BindingPriority.Template);
|
||||||
|
}
|
||||||
|
|
||||||
|
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor)
|
||||||
|
{
|
||||||
|
return InstanceCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override bool WriteValueToSource(object? value)
|
||||||
|
{
|
||||||
|
if (Property is not null && TryGetTemplatedParent(out var templatedParent))
|
||||||
{
|
{
|
||||||
if (Converter is not null)
|
if (Converter is not null)
|
||||||
{
|
value = ConvertBack(Converter, ConverterCulture, ConverterParameter, value, TargetType);
|
||||||
value = Converter.ConvertBack(
|
|
||||||
value,
|
|
||||||
Property.PropertyType,
|
|
||||||
ConverterParameter,
|
|
||||||
CultureInfo.CurrentCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
templatedParent.SetCurrentValue(Property, value);
|
if (value != BindingOperations.DoNothing)
|
||||||
|
templatedParent.SetCurrentValue(Property, value);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void ISetterValue.Initialize(SetterBase setter) => _isSetterValue = true;
|
void ISetterValue.Initialize(SetterBase setter) => _isSetterValue = true;
|
||||||
|
|
||||||
private void Subscribed()
|
protected override void StartCore()
|
||||||
{
|
{
|
||||||
TemplatedParentChanged();
|
OnTemplatedParentChanged();
|
||||||
|
if (TryGetTarget(out var target))
|
||||||
|
target.PropertyChanged += OnTargetPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
if (_target is not null)
|
protected override void StopCore()
|
||||||
|
{
|
||||||
|
if (TryGetTarget(out var target))
|
||||||
{
|
{
|
||||||
_target.PropertyChanged += TargetPropertyChanged;
|
if (target is StyledElement targetElement &&
|
||||||
|
targetElement?.TemplatedParent is { } templatedParent)
|
||||||
|
{
|
||||||
|
templatedParent.PropertyChanged -= OnTemplatedParentPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target is not null)
|
||||||
|
{
|
||||||
|
target.PropertyChanged -= OnTargetPropertyChanged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Unsubscribed()
|
private object? ConvertToTargetType(object? value)
|
||||||
{
|
{
|
||||||
if (_target?.TemplatedParent is { } templatedParent)
|
var converter = TargetTypeConverter.GetDefaultConverter();
|
||||||
{
|
|
||||||
templatedParent.PropertyChanged -= TemplatedParentPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_target is not null)
|
if (converter.TryConvert(value, TargetType, CultureInfo.InvariantCulture, out var result))
|
||||||
{
|
{
|
||||||
_target.PropertyChanged -= TargetPropertyChanged;
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (TryGetTarget(out var target))
|
||||||
|
{
|
||||||
|
var valueString = value?.ToString() ?? "(null)";
|
||||||
|
var valueTypeName = value?.GetType().FullName ?? "null";
|
||||||
|
var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'.";
|
||||||
|
Log(target, message, LogEventLevel.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AvaloniaProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TemplateBinding InstanceCore()
|
||||||
|
{
|
||||||
|
if (Mode is BindingMode.OneTime or BindingMode.OneWayToSource)
|
||||||
|
throw new NotSupportedException("TemplateBinding does not support OneTime or OneWayToSource bindings.");
|
||||||
|
|
||||||
|
// Usually each `TemplateBinding` will only be instantiated once; in this case we can
|
||||||
|
// use the `TemplateBinding` object itself as the binding expression in order to save
|
||||||
|
// allocating a new object.
|
||||||
|
//
|
||||||
|
// If the binding appears in a `Setter`, then make a clone and instantiate that because
|
||||||
|
// because the setter can outlive the control and cause a leak.
|
||||||
|
if (!_isSetterValue)
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var clone = new TemplateBinding
|
||||||
|
{
|
||||||
|
Converter = Converter,
|
||||||
|
ConverterCulture = ConverterCulture,
|
||||||
|
ConverterParameter = ConverterParameter,
|
||||||
|
Mode = Mode,
|
||||||
|
Property = Property,
|
||||||
|
};
|
||||||
|
|
||||||
|
return clone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishValue()
|
private void PublishValue()
|
||||||
{
|
{
|
||||||
if (_target?.TemplatedParent is { } templatedParent)
|
if (Mode == BindingMode.OneWayToSource)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryGetTemplatedParent(out var templatedParent))
|
||||||
{
|
{
|
||||||
var value = Property is not null ?
|
var value = Property is not null ?
|
||||||
templatedParent.GetValue(Property) :
|
templatedParent.GetValue(Property) :
|
||||||
_target.TemplatedParent;
|
templatedParent;
|
||||||
|
BindingError? error = null;
|
||||||
|
|
||||||
if (Converter is not null)
|
if (Converter is not null)
|
||||||
{
|
value = Convert(Converter, ConverterCulture, ConverterParameter, value, TargetType, ref error);
|
||||||
value = Converter.Convert(value, _targetType ?? typeof(object), ConverterParameter, CultureInfo.CurrentCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
_observer?.OnNext(value);
|
value = ConvertToTargetType(value);
|
||||||
_hasProducedValue = true;
|
PublishValue(value, error);
|
||||||
|
|
||||||
|
if (Mode == BindingMode.OneTime)
|
||||||
|
Stop();
|
||||||
}
|
}
|
||||||
else if (_hasProducedValue)
|
else
|
||||||
{
|
{
|
||||||
_observer?.OnNext(AvaloniaProperty.UnsetValue);
|
PublishValue(AvaloniaProperty.UnsetValue);
|
||||||
_hasProducedValue = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TemplatedParentChanged()
|
private void OnTemplatedParentChanged()
|
||||||
{
|
{
|
||||||
if (_target?.TemplatedParent is { } templatedParent)
|
if (TryGetTemplatedParent(out var templatedParent))
|
||||||
{
|
templatedParent.PropertyChanged += OnTemplatedParentPropertyChanged;
|
||||||
templatedParent.PropertyChanged += TemplatedParentPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
PublishValue();
|
PublishValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
private void OnTemplatedParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Property == Property)
|
||||||
|
PublishValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Property == StyledElement.TemplatedParentProperty)
|
if (e.Property == StyledElement.TemplatedParentProperty)
|
||||||
{
|
{
|
||||||
if (e.OldValue is AvaloniaObject oldValue)
|
if (e.OldValue is AvaloniaObject oldValue)
|
||||||
{
|
oldValue.PropertyChanged -= OnTemplatedParentPropertyChanged;
|
||||||
oldValue.PropertyChanged -= TemplatedParentPropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
TemplatedParentChanged();
|
OnTemplatedParentChanged();
|
||||||
}
|
}
|
||||||
}
|
else if (Mode is BindingMode.TwoWay or BindingMode.OneWayToSource && e.Property == TargetProperty)
|
||||||
|
|
||||||
private void TemplatedParentPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Property == Property)
|
|
||||||
{
|
{
|
||||||
PublishValue();
|
WriteValueToSource(e.NewValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IBinding ProvideValue() => this;
|
private bool TryGetTemplatedParent([NotNullWhen(true)] out AvaloniaObject? result)
|
||||||
|
{
|
||||||
|
if (TryGetTarget(out var target) &&
|
||||||
|
target is StyledElement targetElement &&
|
||||||
|
targetElement.TemplatedParent is { } templatedParent)
|
||||||
|
{
|
||||||
|
result = templatedParent;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
|
||||||
namespace Avalonia
|
namespace Avalonia
|
||||||
@@ -142,5 +141,11 @@ namespace Avalonia
|
|||||||
|
|
||||||
Setter((TOwner)instance, (TValue)value!);
|
Setter((TOwner)instance, (TValue)value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object? IDirectPropertyAccessor.GetUnsetValue(Type type)
|
||||||
|
{
|
||||||
|
var metadata = GetMetadata(type);
|
||||||
|
return metadata.UnsetValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
using Avalonia.PropertyStore;
|
using Avalonia.PropertyStore;
|
||||||
|
|
||||||
namespace Avalonia
|
namespace Avalonia
|
||||||
@@ -157,6 +158,12 @@ namespace Avalonia
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal override void RouteSetDirectValueUnchecked(AvaloniaObject o, object? value)
|
||||||
|
{
|
||||||
|
var bindingValue = BindingValue<TValue>.FromUntypedStrict(value);
|
||||||
|
o.SetDirectValueUnchecked<TValue>(this, bindingValue);
|
||||||
|
}
|
||||||
|
|
||||||
internal override void RouteSetCurrentValue(AvaloniaObject o, object? value)
|
internal override void RouteSetCurrentValue(AvaloniaObject o, object? value)
|
||||||
{
|
{
|
||||||
RouteSetValue(o, value, BindingPriority.LocalValue);
|
RouteSetValue(o, value, BindingPriority.LocalValue);
|
||||||
|
|||||||
@@ -32,5 +32,11 @@ namespace Avalonia
|
|||||||
/// <param name="instance">The instance.</param>
|
/// <param name="instance">The instance.</param>
|
||||||
/// <param name="value">The value.</param>
|
/// <param name="value">The value.</param>
|
||||||
void SetValue(AvaloniaObject instance, object? value);
|
void SetValue(AvaloniaObject instance, object? value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the unset value of the property for the specified type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type.</param>
|
||||||
|
object? GetUnsetValue(Type type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,15 +80,7 @@ namespace Avalonia.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source is object)
|
FormatSource(source, result);
|
||||||
{
|
|
||||||
result.Append(" (");
|
|
||||||
result.Append(source.GetType().Name);
|
|
||||||
result.Append(" #");
|
|
||||||
result.Append(source.GetHashCode());
|
|
||||||
result.Append(')');
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringBuilderCache.GetStringAndRelease(result);
|
return StringBuilderCache.GetStringAndRelease(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,16 +124,25 @@ namespace Avalonia.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source is object)
|
FormatSource(source, result);
|
||||||
{
|
|
||||||
result.Append('(');
|
|
||||||
result.Append(source.GetType().Name);
|
|
||||||
result.Append(" #");
|
|
||||||
result.Append(source.GetHashCode());
|
|
||||||
result.Append(')');
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringBuilderCache.GetStringAndRelease(result);
|
return StringBuilderCache.GetStringAndRelease(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void FormatSource(object? source, StringBuilder result)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
result.Append(" (");
|
||||||
|
result.Append(source.GetType().Name);
|
||||||
|
result.Append(" #");
|
||||||
|
|
||||||
|
if (source is StyledElement se && se.Name is not null)
|
||||||
|
result.Append(se.Name);
|
||||||
|
else
|
||||||
|
result.Append(source.GetHashCode());
|
||||||
|
|
||||||
|
result.Append(')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using Avalonia.Data;
|
||||||
using Avalonia.Metadata;
|
using Avalonia.Metadata;
|
||||||
|
|
||||||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
|
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
|
||||||
@@ -33,3 +34,4 @@ using Avalonia.Metadata;
|
|||||||
[assembly: InternalsVisibleTo("Avalonia.Browser.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
|
[assembly: InternalsVisibleTo("Avalonia.Browser.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
|
||||||
[assembly: InternalsVisibleTo("Avalonia.Browser, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
|
[assembly: InternalsVisibleTo("Avalonia.Browser, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||||
|
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ namespace Avalonia.PropertyStore
|
|||||||
var property = instance.Property;
|
var property = instance.Property;
|
||||||
var originalType = value.Type;
|
var originalType = value.Type;
|
||||||
|
|
||||||
LoggingUtils.LogIfNecessary(owner, property, value);
|
|
||||||
|
|
||||||
if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
|
if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
|
||||||
value = value.WithValue(instance.GetCachedDefaultValue());
|
value = value.WithValue(instance.GetCachedDefaultValue());
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ namespace Avalonia.PropertyStore
|
|||||||
IValueEntry value,
|
IValueEntry value,
|
||||||
BindingPriority priority);
|
BindingPriority priority);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the value and base value for a LocalValue priority, raising
|
||||||
|
/// <see cref="AvaloniaObject.PropertyChanged"/> where necessary.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="owner">The associated value store.</param>
|
||||||
|
/// <param name="property">The property being changed.</param>
|
||||||
|
/// <param name="value">The new value of the property.</param>
|
||||||
|
public abstract void SetLocalValueAndRaise(
|
||||||
|
ValueStore owner,
|
||||||
|
AvaloniaProperty property,
|
||||||
|
object? value);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
|
/// Raises <see cref="AvaloniaObject.PropertyChanged"/> in response to an inherited value
|
||||||
/// change.
|
/// change.
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ namespace Avalonia.PropertyStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void SetLocalValueAndRaise(
|
||||||
|
ValueStore owner,
|
||||||
|
AvaloniaProperty property,
|
||||||
|
object? value)
|
||||||
|
{
|
||||||
|
SetLocalValueAndRaise(owner, (StyledProperty<T>)property, (T)value!);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetLocalValueAndRaise(
|
public void SetLocalValueAndRaise(
|
||||||
ValueStore owner,
|
ValueStore owner,
|
||||||
StyledProperty<T> property,
|
StyledProperty<T> property,
|
||||||
@@ -138,6 +146,10 @@ namespace Avalonia.PropertyStore
|
|||||||
|
|
||||||
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
|
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
|
||||||
{
|
{
|
||||||
|
var clearDataValidation = ValueEntry?.GetDataValidationState(out _, out _) ??
|
||||||
|
BaseValueEntry?.GetDataValidationState(out _, out _) ??
|
||||||
|
false;
|
||||||
|
|
||||||
ValueEntry?.Unsubscribe();
|
ValueEntry?.Unsubscribe();
|
||||||
BaseValueEntry?.Unsubscribe();
|
BaseValueEntry?.Unsubscribe();
|
||||||
|
|
||||||
@@ -163,12 +175,8 @@ namespace Avalonia.PropertyStore
|
|||||||
owner.OnInheritedEffectiveValueDisposed(p, Value, newValue);
|
owner.OnInheritedEffectiveValueDisposed(p, Value, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ValueEntry?.GetDataValidationState(out _, out _) ??
|
if (clearDataValidation)
|
||||||
BaseValueEntry?.GetDataValidationState(out _, out _) ??
|
|
||||||
false)
|
|
||||||
{
|
|
||||||
owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
|
owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)
|
protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Avalonia.Data;
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
|
|
||||||
namespace Avalonia.PropertyStore
|
namespace Avalonia.PropertyStore
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,12 @@ namespace Avalonia.PropertyStore
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IValueEntry AddBinding(UntypedBindingExpressionBase source)
|
||||||
|
{
|
||||||
|
Add(source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
public TypedBindingEntry<T> AddBinding<T>(
|
public TypedBindingEntry<T> AddBinding<T>(
|
||||||
StyledProperty<T> property,
|
StyledProperty<T> property,
|
||||||
IObservable<BindingValue<T>> source)
|
IObservable<BindingValue<T>> source)
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ namespace Avalonia.PropertyStore
|
|||||||
var property = instance.Property;
|
var property = instance.Property;
|
||||||
var originalType = value.Type;
|
var originalType = value.Type;
|
||||||
|
|
||||||
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
|
|
||||||
|
|
||||||
// Revert to the default value if the binding value fails validation, or if
|
// Revert to the default value if the binding value fails validation, or if
|
||||||
// there was no value (though not if there was a data validation error).
|
// there was no value (though not if there was a data validation error).
|
||||||
if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) ||
|
if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) ||
|
||||||
|
|||||||
@@ -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 have changed in this diff Show More
Reference in New Issue
Block a user