Browse Source
* Update ncrunch config. * Tidy up reflection and multi-binding APIs: - Move `BindingBase` and `MultiBinding` into Avalonia.Base - `BindingBase` becomes a true base class for all bindings, and contains only the `Instance` method - Properties common between reflection and compiled bindings are moved into `StandardBindingBase` - `Binding` is moved to Avalonia.Base and renamed to `ReflectionBinding` - A compatibility shim for `Binding` remains in Avalonia.Markup - Remove `IBinding` and `IBinding2` - Remove `ITreeDataTemplate's usage of `InstancedBinding` - Remove `NativeMenuBarPresenter`s usage of `InstancedBinding` - Remove `InstancedBinding` as it is now unused This required an update to the DataGrid submodule: cell data validation has been temporarily removed as this used `InstancedBinding`. * `Instance()` => `CreateInstance()`. The use of "Instance" as a verb is quite unusual apparently ;) * Seal classes where appropriate. * Seal classes where appropriate. * Remove `StandardBindingBase`. Simply duplicate the members in reflection and compiled binding classes. * Delete deleted submodule directory. * Add missing attribute. Fixes compile error. * Fix reference to removed class. * Update suppressions.pull/20442/head
committed by
GitHub
85 changed files with 1264 additions and 1304 deletions
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,28 @@ |
|||||
|
namespace Avalonia.Data; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Base class for the various types of binding supported by Avalonia.
|
||||
|
/// </summary>
|
||||
|
public abstract class BindingBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a <see cref="BindingExpressionBase"/> from a binding.
|
||||
|
/// </summary>
|
||||
|
/// <param name="target">The target of the binding.</param>
|
||||
|
/// <param name="targetProperty">The target property of the binding.</param>
|
||||
|
/// <param name="anchor">
|
||||
|
/// If <paramref name="target"/> is not a control, provides an anchor object from which to
|
||||
|
/// locate a data context or other controls.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// A newly instantiated <see cref="BindingExpressionBase"/>.
|
||||
|
/// </returns>
|
||||
|
/// <remarks>
|
||||
|
/// This is a low-level method which returns a binding expression that is not yet connected to
|
||||
|
/// a binding sink, and so is inactive.
|
||||
|
/// </remarks>
|
||||
|
internal abstract BindingExpressionBase CreateInstance( |
||||
|
AvaloniaObject target, |
||||
|
AvaloniaProperty? targetProperty, |
||||
|
object? anchor); |
||||
|
} |
||||
@ -1,20 +0,0 @@ |
|||||
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); |
|
||||
} |
|
||||
@ -1,8 +1,7 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using Avalonia.Data.Core; |
|
||||
using Avalonia.Utilities; |
using Avalonia.Utilities; |
||||
|
|
||||
namespace Avalonia.Markup.Parsers |
namespace Avalonia.Data.Core.Parsers |
||||
{ |
{ |
||||
internal static class ArgumentListParser |
internal static class ArgumentListParser |
||||
{ |
{ |
||||
@ -1,35 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Diagnostics; |
|
||||
using Avalonia.Metadata; |
|
||||
|
|
||||
namespace Avalonia.Data |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Holds a binding that can be applied to a property on an object.
|
|
||||
/// </summary>
|
|
||||
[NotClientImplementable] |
|
||||
public interface IBinding |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initiates the binding on a target object.
|
|
||||
/// </summary>
|
|
||||
/// <param name="target">The target instance.</param>
|
|
||||
/// <param name="targetProperty">The target property. May be null.</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>
|
|
||||
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
|
|
||||
/// <returns>
|
|
||||
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
|
|
||||
/// </returns>
|
|
||||
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] |
|
||||
InstancedBinding? Initiate( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
object? anchor = null, |
|
||||
bool enableDataValidation = false); |
|
||||
} |
|
||||
} |
|
||||
@ -1,172 +0,0 @@ |
|||||
using System; |
|
||||
using System.ComponentModel; |
|
||||
using Avalonia.Data.Core; |
|
||||
using Avalonia.Reactive; |
|
||||
using ObservableEx = Avalonia.Reactive.Observable; |
|
||||
|
|
||||
namespace Avalonia.Data |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Holds the result of calling <see cref="IBinding.Initiate"/>.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// Whereas an <see cref="IBinding"/> holds a description of a binding such as "Bind to the X
|
|
||||
/// property on a control's DataContext"; this class represents a binding that has been
|
|
||||
/// *instanced* by calling <see cref="IBinding.Initiate(AvaloniaObject, AvaloniaProperty, object, bool)"/>
|
|
||||
/// on a target object.
|
|
||||
/// </remarks>
|
|
||||
public sealed class InstancedBinding |
|
||||
{ |
|
||||
private readonly AvaloniaObject? _target; |
|
||||
private readonly UntypedBindingExpressionBase? _expression; |
|
||||
private IObservable<object?>? _observable; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="source">The binding source.</param>
|
|
||||
/// <param name="mode">The binding mode.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <remarks>
|
|
||||
/// This constructor can be used to create any type of binding and as such requires an
|
|
||||
/// <see cref="IObservable{Object}"/> as the binding source because this is the only binding
|
|
||||
/// source which can be used for all binding modes. If you wish to create an instance with
|
|
||||
/// something other than a subject, use one of the static creation methods on this class.
|
|
||||
/// </remarks>
|
|
||||
internal InstancedBinding(IObservable<object?> source, BindingMode mode, BindingPriority priority) |
|
||||
{ |
|
||||
Mode = mode; |
|
||||
Priority = priority; |
|
||||
_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>
|
|
||||
/// Gets the binding mode with which the binding was initiated.
|
|
||||
/// </summary>
|
|
||||
public BindingMode Mode { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the binding priority.
|
|
||||
/// </summary>
|
|
||||
public BindingPriority Priority { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the binding source observable.
|
|
||||
/// </summary>
|
|
||||
public IObservable<object?> Source => _observable ??= _expression!.ToObservable(_target); |
|
||||
|
|
||||
[Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] |
|
||||
public IObservable<object?> Observable => Source; |
|
||||
|
|
||||
internal UntypedBindingExpressionBase? Expression => _expression; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new one-time binding with a fixed value.
|
|
||||
/// </summary>
|
|
||||
/// <param name="value">The value.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public static InstancedBinding OneTime( |
|
||||
object value, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
|
||||
return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new one-time binding.
|
|
||||
/// </summary>
|
|
||||
/// <param name="observable">The source observable.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public static InstancedBinding OneTime( |
|
||||
IObservable<object?> observable, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
|
||||
_ = observable ?? throw new ArgumentNullException(nameof(observable)); |
|
||||
|
|
||||
return new InstancedBinding(observable, BindingMode.OneTime, priority); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new one-way binding.
|
|
||||
/// </summary>
|
|
||||
/// <param name="observable">The source observable.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public static InstancedBinding OneWay( |
|
||||
IObservable<object?> observable, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
|
||||
_ = observable ?? throw new ArgumentNullException(nameof(observable)); |
|
||||
|
|
||||
return new InstancedBinding(observable, BindingMode.OneWay, priority); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new one-way to source binding.
|
|
||||
/// </summary>
|
|
||||
/// <param name="observer">The binding source.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public static InstancedBinding OneWayToSource( |
|
||||
IObserver<object?> observer, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
|
||||
_ = observer ?? throw new ArgumentNullException(nameof(observer)); |
|
||||
|
|
||||
return new InstancedBinding((IObservable<object?>)observer, BindingMode.OneWayToSource, priority); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new two-way binding.
|
|
||||
/// </summary>
|
|
||||
/// <param name="observable">The binding source.</param>
|
|
||||
/// <param name="observer">The binding source.</param>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public static InstancedBinding TwoWay( |
|
||||
IObservable<object?> observable, |
|
||||
IObserver<object?> observer, |
|
||||
BindingPriority priority = BindingPriority.LocalValue) |
|
||||
{ |
|
||||
_ = observable ?? throw new ArgumentNullException(nameof(observable)); |
|
||||
_ = observer ?? throw new ArgumentNullException(nameof(observer)); |
|
||||
|
|
||||
var subject = observable == observer ? observable : new CombinedSubject<object?>(observer, observable); |
|
||||
return new InstancedBinding(subject, BindingMode.TwoWay, priority); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a copy of the <see cref="InstancedBinding"/> with a different priority.
|
|
||||
/// </summary>
|
|
||||
/// <param name="priority">The priority of the binding.</param>
|
|
||||
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
|
|
||||
public InstancedBinding WithPriority(BindingPriority priority) |
|
||||
{ |
|
||||
return new InstancedBinding(Source, Mode, priority); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,237 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Data.Core; |
||||
|
using Avalonia.Data.Core.ExpressionNodes; |
||||
|
using Avalonia.Data.Core.Parsers; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Data |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A binding that uses reflection to access members.
|
||||
|
/// </summary>
|
||||
|
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] |
||||
|
[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] |
||||
|
public class ReflectionBinding : BindingBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ReflectionBinding"/> class.
|
||||
|
/// </summary>
|
||||
|
public ReflectionBinding() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ReflectionBinding"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="path">The binding path.</param>
|
||||
|
public ReflectionBinding(string path) |
||||
|
{ |
||||
|
Path = path; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ReflectionBinding"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="path">The binding path.</param>
|
||||
|
/// <param name="mode">The binding mode.</param>
|
||||
|
public ReflectionBinding(string path, BindingMode mode) |
||||
|
{ |
||||
|
Path = path; |
||||
|
Mode = mode; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
|
||||
|
/// source after the value on the target changes.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
|
||||
|
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
|
||||
|
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
|
||||
|
/// </remarks>
|
||||
|
public int Delay { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the <see cref="IValueConverter"/> to use.
|
||||
|
/// </summary>
|
||||
|
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>
|
||||
|
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
|
||||
|
/// </summary>
|
||||
|
public object? ConverterParameter { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the name of the element to use as the binding source.
|
||||
|
/// </summary>
|
||||
|
public string? ElementName { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the value to use when the binding is unable to produce a value.
|
||||
|
/// </summary>
|
||||
|
public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the binding mode.
|
||||
|
/// </summary>
|
||||
|
public BindingMode Mode { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the binding path.
|
||||
|
/// </summary>
|
||||
|
public string Path { get; set; } = ""; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the binding priority.
|
||||
|
/// </summary>
|
||||
|
public BindingPriority Priority { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the relative source for the binding.
|
||||
|
/// </summary>
|
||||
|
public RelativeSource? RelativeSource { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the source for the binding.
|
||||
|
/// </summary>
|
||||
|
public object? Source { get; set; } = AvaloniaProperty.UnsetValue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the string format.
|
||||
|
/// </summary>
|
||||
|
public string? StringFormat { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the value to use when the binding result is null.
|
||||
|
/// </summary>
|
||||
|
public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a value that determines the timing of binding source updates for
|
||||
|
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
|
||||
|
/// </summary>
|
||||
|
public UpdateSourceTrigger UpdateSourceTrigger { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a function used to resolve types from names in the binding path.
|
||||
|
/// </summary>
|
||||
|
public Func<string?, string, Type>? TypeResolver { get; set; } |
||||
|
|
||||
|
internal WeakReference? DefaultAnchor { get; set; } |
||||
|
internal WeakReference<INameScope?>? NameScope { get; set; } |
||||
|
|
||||
|
internal override BindingExpressionBase CreateInstance( |
||||
|
AvaloniaObject target, |
||||
|
AvaloniaProperty? targetProperty, |
||||
|
object? anchor) |
||||
|
{ |
||||
|
List<ExpressionNode>? nodes = null; |
||||
|
var isRooted = false; |
||||
|
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false; |
||||
|
|
||||
|
// Build the expression nodes from the binding path.
|
||||
|
if (!string.IsNullOrEmpty(Path)) |
||||
|
{ |
||||
|
var reader = new CharacterReader(Path.AsSpan()); |
||||
|
var (astPool, sourceMode) = BindingExpressionGrammar.ParseToPooledList(ref reader); |
||||
|
nodes = ExpressionNodeFactory.CreateFromAst( |
||||
|
astPool, |
||||
|
TypeResolver, |
||||
|
GetNameScope(), |
||||
|
out isRooted); |
||||
|
} |
||||
|
|
||||
|
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
|
||||
|
// #elementName etc.) then we need to add a source node. The type of source node will
|
||||
|
// depend on the ElementName and RelativeSource properties of the binding and if
|
||||
|
// neither of those are set will default to a data context node.
|
||||
|
if (Source == AvaloniaProperty.UnsetValue && !isRooted && CreateSourceNode(targetProperty) is { } sourceNode) |
||||
|
{ |
||||
|
nodes ??= new(); |
||||
|
nodes.Insert(0, sourceNode); |
||||
|
} |
||||
|
|
||||
|
// If the first node is an ISourceNode then allow it to select the source; otherwise
|
||||
|
// use the binding source if specified, falling back to the target.
|
||||
|
var source = nodes?.Count > 0 && nodes[0] is SourceNode sn ? |
||||
|
sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target) : |
||||
|
Source != AvaloniaProperty.UnsetValue ? Source : target; |
||||
|
|
||||
|
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty); |
||||
|
|
||||
|
return new BindingExpression( |
||||
|
source, |
||||
|
nodes, |
||||
|
FallbackValue, |
||||
|
delay: TimeSpan.FromMilliseconds(Delay), |
||||
|
converter: Converter, |
||||
|
converterCulture: ConverterCulture, |
||||
|
converterParameter: ConverterParameter, |
||||
|
enableDataValidation: enableDataValidation, |
||||
|
mode: mode, |
||||
|
priority: Priority, |
||||
|
stringFormat: StringFormat, |
||||
|
targetProperty: targetProperty, |
||||
|
targetNullValue: TargetNullValue, |
||||
|
targetTypeConverter: TargetTypeConverter.GetReflectionConverter(), |
||||
|
updateSourceTrigger: trigger); |
||||
|
} |
||||
|
|
||||
|
private INameScope? GetNameScope() |
||||
|
{ |
||||
|
INameScope? result = null; |
||||
|
NameScope?.TryGetTarget(out result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private ExpressionNode? CreateSourceNode(AvaloniaProperty? targetProperty) |
||||
|
{ |
||||
|
if (!string.IsNullOrEmpty(ElementName)) |
||||
|
{ |
||||
|
var nameScope = GetNameScope() ?? throw new InvalidOperationException( |
||||
|
"Cannot create ElementName binding when NameScope is null"); |
||||
|
return new NamedElementNode(nameScope, ElementName); |
||||
|
} |
||||
|
|
||||
|
if (RelativeSource is not null) |
||||
|
return ExpressionNodeFactory.CreateRelativeSource(RelativeSource); |
||||
|
|
||||
|
return ExpressionNodeFactory.CreateDataContext(targetProperty); |
||||
|
} |
||||
|
|
||||
|
private (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata( |
||||
|
AvaloniaObject target, |
||||
|
AvaloniaProperty? targetProperty) |
||||
|
{ |
||||
|
var mode = Mode; |
||||
|
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ? |
||||
|
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger; |
||||
|
|
||||
|
if (mode == BindingMode.Default) |
||||
|
{ |
||||
|
if (targetProperty?.GetMetadata(target) is { } metadata) |
||||
|
mode = metadata.DefaultBindingMode; |
||||
|
else |
||||
|
mode = BindingMode.OneWay; |
||||
|
} |
||||
|
|
||||
|
return (mode, trigger); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,21 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Reactive; |
|
||||
|
|
||||
namespace Avalonia.Data |
|
||||
{ |
|
||||
// TODO12: Remove IAvaloniaSubject<object?> support from TemplateBinding.
|
|
||||
public partial class TemplateBinding : IAvaloniaSubject<object?> |
|
||||
{ |
|
||||
private IAvaloniaSubject<object?>? _observableAdapter; |
|
||||
|
|
||||
public IDisposable Subscribe(IObserver<object?> observer) |
|
||||
{ |
|
||||
_observableAdapter ??= ToObservable(); |
|
||||
return _observableAdapter.Subscribe(observer); |
|
||||
} |
|
||||
|
|
||||
void IObserver<object?>.OnCompleted() => _observableAdapter?.OnCompleted(); |
|
||||
void IObserver<object?>.OnError(Exception error) => _observableAdapter?.OnError(error); |
|
||||
void IObserver<object?>.OnNext(object? value) => _observableAdapter?.OnNext(value); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,170 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Globalization; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Data.Core; |
||||
|
using Avalonia.Logging; |
||||
|
|
||||
|
namespace Avalonia.Data; |
||||
|
|
||||
|
internal class TemplateBindingExpression : UntypedBindingExpressionBase |
||||
|
{ |
||||
|
private IValueConverter? _converter; |
||||
|
private CultureInfo? _converterCulture; |
||||
|
private object? _converterParameter; |
||||
|
private BindingMode _mode; |
||||
|
private readonly AvaloniaProperty? _property; |
||||
|
private bool _hasPublishedValue; |
||||
|
|
||||
|
public TemplateBindingExpression( |
||||
|
AvaloniaProperty? property, |
||||
|
IValueConverter? converter, |
||||
|
CultureInfo? converterCulture, |
||||
|
object? converterParameter, |
||||
|
BindingMode mode) |
||||
|
: base(BindingPriority.Template) |
||||
|
{ |
||||
|
_property = property; |
||||
|
_converter = converter; |
||||
|
_converterCulture = converterCulture; |
||||
|
_converterParameter = converterParameter; |
||||
|
_mode = mode; |
||||
|
} |
||||
|
|
||||
|
public override string Description => $"{{TemplateBinding {_property}}}"; |
||||
|
|
||||
|
protected override void StartCore() |
||||
|
{ |
||||
|
_hasPublishedValue = false; |
||||
|
OnTemplatedParentChanged(); |
||||
|
if (TryGetTarget(out var target)) |
||||
|
target.PropertyChanged += OnTargetPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
protected override void StopCore() |
||||
|
{ |
||||
|
if (TryGetTarget(out var target)) |
||||
|
{ |
||||
|
if (target is StyledElement targetElement && |
||||
|
targetElement?.TemplatedParent is { } templatedParent) |
||||
|
{ |
||||
|
templatedParent.PropertyChanged -= OnTemplatedParentPropertyChanged; |
||||
|
} |
||||
|
|
||||
|
if (target is not null) |
||||
|
{ |
||||
|
target.PropertyChanged -= OnTargetPropertyChanged; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal override bool WriteValueToSource(object? value) |
||||
|
{ |
||||
|
if (_property is not null && TryGetTemplatedParent(out var templatedParent)) |
||||
|
{ |
||||
|
if (_converter is not null) |
||||
|
value = ConvertBack(_converter, _converterCulture, _converterParameter, value, TargetType); |
||||
|
|
||||
|
if (value != BindingOperations.DoNothing) |
||||
|
templatedParent.SetCurrentValue(_property, value); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private object? ConvertToTargetType(object? value) |
||||
|
{ |
||||
|
var converter = TargetTypeConverter.GetDefaultConverter(); |
||||
|
|
||||
|
if (converter.TryConvert(value, TargetType, CultureInfo.InvariantCulture, out var result)) |
||||
|
{ |
||||
|
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 void PublishValue() |
||||
|
{ |
||||
|
if (_mode == BindingMode.OneWayToSource) |
||||
|
return; |
||||
|
|
||||
|
if (TryGetTemplatedParent(out var templatedParent)) |
||||
|
{ |
||||
|
var value = _property is not null ? |
||||
|
templatedParent.GetValue(_property) : |
||||
|
templatedParent; |
||||
|
BindingError? error = null; |
||||
|
|
||||
|
if (_converter is not null) |
||||
|
value = Convert(_converter, _converterCulture, _converterParameter, value, TargetType, ref error); |
||||
|
|
||||
|
value = ConvertToTargetType(value); |
||||
|
PublishValue(value, error); |
||||
|
_hasPublishedValue = true; |
||||
|
|
||||
|
if (_mode == BindingMode.OneTime) |
||||
|
Stop(); |
||||
|
} |
||||
|
else if (_hasPublishedValue) |
||||
|
{ |
||||
|
PublishValue(AvaloniaProperty.UnsetValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnTemplatedParentChanged() |
||||
|
{ |
||||
|
if (TryGetTemplatedParent(out var templatedParent)) |
||||
|
templatedParent.PropertyChanged += OnTemplatedParentPropertyChanged; |
||||
|
|
||||
|
PublishValue(); |
||||
|
} |
||||
|
|
||||
|
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.OldValue is AvaloniaObject oldValue) |
||||
|
oldValue.PropertyChanged -= OnTemplatedParentPropertyChanged; |
||||
|
|
||||
|
OnTemplatedParentChanged(); |
||||
|
} |
||||
|
else if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && e.Property == TargetProperty) |
||||
|
{ |
||||
|
WriteValueToSource(e.NewValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
@ -1,214 +1,21 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
using System.Diagnostics.CodeAnalysis; |
||||
using Avalonia.Collections.Pooled; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Data.Converters; |
|
||||
using Avalonia.Data.Core; |
|
||||
using Avalonia.Data.Core.ExpressionNodes; |
|
||||
using Avalonia.Diagnostics; |
|
||||
using Avalonia.Markup.Parsers; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Data |
namespace Avalonia.Data; |
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A XAML binding.
|
|
||||
/// </summary>
|
|
||||
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] |
|
||||
#if NET8_0_OR_GREATER
|
|
||||
[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] |
|
||||
#endif
|
|
||||
public class Binding : BindingBase |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="Binding"/> class.
|
|
||||
/// </summary>
|
|
||||
public Binding() |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="Binding"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="path">The binding path.</param>
|
|
||||
/// <param name="mode">The binding mode.</param>
|
|
||||
public Binding(string path, BindingMode mode = BindingMode.Default) |
|
||||
: base(mode) |
|
||||
{ |
|
||||
Path = path; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the name of the element to use as the binding source.
|
|
||||
/// </summary>
|
|
||||
public string? ElementName { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the relative source for the binding.
|
|
||||
/// </summary>
|
|
||||
public RelativeSource? RelativeSource { get; set; } |
|
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets or sets the source for the binding.
|
/// Provides limited compatibility with the 11.x Binding class. Use <see cref="ReflectionBinding"/>
|
||||
/// </summary>
|
/// for new code.
|
||||
public object? Source { get; set; } = AvaloniaProperty.UnsetValue; |
/// </summary>
|
||||
|
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] |
||||
/// <summary>
|
|
||||
/// Gets or sets the binding path.
|
|
||||
/// </summary>
|
|
||||
public string Path { get; set; } = ""; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets a function used to resolve types from names in the binding path.
|
|
||||
/// </summary>
|
|
||||
public Func<string?, string, Type>? TypeResolver { get; set; } |
|
||||
|
|
||||
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] |
|
||||
public override InstancedBinding? Initiate( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
object? anchor = null, |
|
||||
bool enableDataValidation = false) |
|
||||
{ |
|
||||
var expression = InstanceCore(targetProperty, target, anchor, enableDataValidation); |
|
||||
return new InstancedBinding(target, expression, Mode, Priority); |
|
||||
} |
|
||||
|
|
||||
private protected override BindingExpressionBase Instance( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
object? anchor) |
|
||||
{ |
|
||||
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false; |
|
||||
return InstanceCore(targetProperty, target, anchor, enableDataValidation); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Hack for TreeDataTemplate to create a binding expression for an item.
|
|
||||
/// </summary>
|
|
||||
/// <param name="source">The item.</param>
|
|
||||
/// <remarks>
|
|
||||
/// Ideally we'd do this in a more generic way but didn't have time to refactor
|
|
||||
/// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0.
|
|
||||
/// </remarks>
|
|
||||
// TODO12: Refactor
|
|
||||
#if NET8_0_OR_GREATER
|
#if NET8_0_OR_GREATER
|
||||
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] |
[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] |
||||
#endif
|
#endif
|
||||
internal BindingExpression CreateObservableForTreeDataTemplate(object source) |
public class Binding : ReflectionBinding |
||||
{ |
{ |
||||
if (!string.IsNullOrEmpty(ElementName)) |
public Binding() { } |
||||
throw new NotSupportedException("ElementName bindings are not supported in this context."); |
|
||||
if (RelativeSource is not null && RelativeSource.Mode != RelativeSourceMode.DataContext) |
|
||||
throw new NotSupportedException("RelativeSource bindings are not supported in this context."); |
|
||||
if (Source != AvaloniaProperty.UnsetValue) |
|
||||
throw new NotSupportedException("Source bindings are not supported in this context."); |
|
||||
|
|
||||
List<ExpressionNode>? nodes = null; |
|
||||
var isRooted = false; |
|
||||
|
|
||||
if (!string.IsNullOrEmpty(Path)) |
|
||||
{ |
|
||||
var reader = new CharacterReader(Path.AsSpan()); |
|
||||
var (astNodes, sourceMode) = BindingExpressionGrammar.ParseToPooledList(ref reader); |
|
||||
nodes = ExpressionNodeFactory.CreateFromAst( |
|
||||
astNodes, |
|
||||
TypeResolver, |
|
||||
GetNameScope(), |
|
||||
out isRooted); |
|
||||
} |
|
||||
|
|
||||
if (isRooted) |
|
||||
throw new NotSupportedException("Rooted binding paths are not supported in this context."); |
|
||||
|
|
||||
return new BindingExpression( |
|
||||
source, |
|
||||
nodes, |
|
||||
FallbackValue, |
|
||||
delay: TimeSpan.FromMilliseconds(Delay), |
|
||||
converter: Converter, |
|
||||
converterParameter: ConverterParameter, |
|
||||
targetNullValue: TargetNullValue); |
|
||||
} |
|
||||
|
|
||||
private UntypedBindingExpressionBase InstanceCore( |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
AvaloniaObject target, |
|
||||
object? anchor, |
|
||||
bool enableDataValidation) |
|
||||
{ |
|
||||
List<ExpressionNode>? nodes = null; |
|
||||
var isRooted = false; |
|
||||
|
|
||||
// Build the expression nodes from the binding path.
|
|
||||
if (!string.IsNullOrEmpty(Path)) |
|
||||
{ |
|
||||
var reader = new CharacterReader(Path.AsSpan()); |
|
||||
var (astPool, sourceMode) = BindingExpressionGrammar.ParseToPooledList(ref reader); |
|
||||
nodes = ExpressionNodeFactory.CreateFromAst( |
|
||||
astPool, |
|
||||
TypeResolver, |
|
||||
GetNameScope(), |
|
||||
out isRooted); |
|
||||
} |
|
||||
|
|
||||
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
|
|
||||
// #elementName etc.) then we need to add a source node. The type of source node will
|
|
||||
// depend on the ElementName and RelativeSource properties of the binding and if
|
|
||||
// neither of those are set will default to a data context node.
|
|
||||
if (Source == AvaloniaProperty.UnsetValue && !isRooted && CreateSourceNode(targetProperty) is { } sourceNode) |
|
||||
{ |
|
||||
nodes ??= new(); |
|
||||
nodes.Insert(0, sourceNode); |
|
||||
} |
|
||||
|
|
||||
// If the first node is an ISourceNode then allow it to select the source; otherwise
|
|
||||
// use the binding source if specified, falling back to the target.
|
|
||||
var source = nodes?.Count > 0 && nodes[0] is SourceNode sn ? |
|
||||
sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target) : |
|
||||
Source != AvaloniaProperty.UnsetValue ? Source : target; |
|
||||
|
|
||||
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty); |
|
||||
|
|
||||
return new BindingExpression( |
|
||||
source, |
|
||||
nodes, |
|
||||
FallbackValue, |
|
||||
delay: TimeSpan.FromMilliseconds(Delay), |
|
||||
converter: Converter, |
|
||||
converterCulture: ConverterCulture, |
|
||||
converterParameter: ConverterParameter, |
|
||||
enableDataValidation: enableDataValidation, |
|
||||
mode: mode, |
|
||||
priority: Priority, |
|
||||
stringFormat: StringFormat, |
|
||||
targetProperty: targetProperty, |
|
||||
targetNullValue: TargetNullValue, |
|
||||
targetTypeConverter: TargetTypeConverter.GetReflectionConverter(), |
|
||||
updateSourceTrigger: trigger); |
|
||||
} |
|
||||
|
|
||||
private INameScope? GetNameScope() |
|
||||
{ |
|
||||
INameScope? result = null; |
|
||||
NameScope?.TryGetTarget(out result); |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
private ExpressionNode? CreateSourceNode(AvaloniaProperty? targetProperty) |
|
||||
{ |
|
||||
if (!string.IsNullOrEmpty(ElementName)) |
|
||||
{ |
|
||||
var nameScope = GetNameScope() ?? throw new InvalidOperationException( |
|
||||
"Cannot create ElementName binding when NameScope is null"); |
|
||||
return new NamedElementNode(nameScope, ElementName); |
|
||||
} |
|
||||
|
|
||||
if (RelativeSource is not null) |
public Binding(string path) : base(path) { } |
||||
return ExpressionNodeFactory.CreateRelativeSource(RelativeSource); |
|
||||
|
|
||||
return ExpressionNodeFactory.CreateDataContext(targetProperty); |
public Binding(string path, BindingMode mode) : base(path, mode) { } |
||||
} |
|
||||
} |
|
||||
} |
} |
||||
|
|||||
@ -1,137 +0,0 @@ |
|||||
using System; |
|
||||
using System.ComponentModel; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.Globalization; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Data.Converters; |
|
||||
using Avalonia.Data.Core; |
|
||||
using Avalonia.Diagnostics; |
|
||||
|
|
||||
namespace Avalonia.Data |
|
||||
{ |
|
||||
public abstract class BindingBase : IBinding, IBinding2 |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="Binding"/> class.
|
|
||||
/// </summary>
|
|
||||
public BindingBase() |
|
||||
{ |
|
||||
FallbackValue = AvaloniaProperty.UnsetValue; |
|
||||
TargetNullValue = AvaloniaProperty.UnsetValue; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="Binding"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="mode">The binding mode.</param>
|
|
||||
public BindingBase(BindingMode mode = BindingMode.Default) |
|
||||
:this() |
|
||||
{ |
|
||||
Mode = mode; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
|
|
||||
/// source after the value on the target changes.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
|
|
||||
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
|
|
||||
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
|
|
||||
/// </remarks>
|
|
||||
public int Delay { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the <see cref="IValueConverter"/> to use.
|
|
||||
/// </summary>
|
|
||||
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>
|
|
||||
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
|
|
||||
/// </summary>
|
|
||||
public object? ConverterParameter { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the value to use when the binding is unable to produce a value.
|
|
||||
/// </summary>
|
|
||||
public object? FallbackValue { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the value to use when the binding result is null.
|
|
||||
/// </summary>
|
|
||||
public object? TargetNullValue { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the binding mode.
|
|
||||
/// </summary>
|
|
||||
public BindingMode Mode { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the binding priority.
|
|
||||
/// </summary>
|
|
||||
public BindingPriority Priority { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the string format.
|
|
||||
/// </summary>
|
|
||||
public string? StringFormat { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets a value that determines the timing of binding source updates for
|
|
||||
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
|
|
||||
/// </summary>
|
|
||||
public UpdateSourceTrigger UpdateSourceTrigger { get; set; } |
|
||||
|
|
||||
public WeakReference? DefaultAnchor { get; set; } |
|
||||
|
|
||||
public WeakReference<INameScope?>? NameScope { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)] |
|
||||
[Obsolete(ObsoletionMessages.MayBeRemovedInAvalonia12)] |
|
||||
public abstract InstancedBinding? Initiate( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
object? anchor = null, |
|
||||
bool enableDataValidation = false); |
|
||||
|
|
||||
private protected abstract BindingExpressionBase Instance( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty, |
|
||||
object? anchor); |
|
||||
|
|
||||
private protected (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata( |
|
||||
AvaloniaObject target, |
|
||||
AvaloniaProperty? targetProperty) |
|
||||
{ |
|
||||
var mode = Mode; |
|
||||
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ? |
|
||||
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger; |
|
||||
|
|
||||
if (mode == BindingMode.Default) |
|
||||
{ |
|
||||
if (targetProperty?.GetMetadata(target) is { } metadata) |
|
||||
mode = metadata.DefaultBindingMode; |
|
||||
else |
|
||||
mode = BindingMode.OneWay; |
|
||||
} |
|
||||
|
|
||||
return (mode, trigger); |
|
||||
} |
|
||||
|
|
||||
BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty? property, object? anchor) |
|
||||
{ |
|
||||
return Instance(target, property, anchor); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,51 +0,0 @@ |
|||||
using System.Reactive.Linq; |
|
||||
using Avalonia.Data; |
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions; |
|
||||
using Xunit; |
|
||||
|
|
||||
#nullable enable |
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||
|
|
||||
namespace Avalonia.Base.UnitTests.Data.Core; |
|
||||
|
|
||||
public abstract partial class BindingExpressionTests |
|
||||
{ |
|
||||
public partial class Reflection |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Obsolete_Initiate_Method_Produces_Observable_With_Correct_Target_Type() |
|
||||
{ |
|
||||
// Issue #15081
|
|
||||
var viewModel = new ViewModel { DoubleValue = 42.5 }; |
|
||||
var target = new TargetClass { DataContext = viewModel }; |
|
||||
var binding = new Binding(nameof(viewModel.DoubleValue)); |
|
||||
var instanced = binding.Initiate(target, TargetClass.StringProperty); |
|
||||
|
|
||||
Assert.NotNull(instanced); |
|
||||
|
|
||||
var value = instanced.Observable.First(); |
|
||||
|
|
||||
Assert.Equal("42.5", value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public partial class Compiled |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Obsolete_Initiate_Method_Produces_Observable_With_Correct_Target_Type() |
|
||||
{ |
|
||||
// Issue #15081
|
|
||||
var viewModel = new ViewModel { DoubleValue = 42.5 }; |
|
||||
var target = new TargetClass { DataContext = viewModel }; |
|
||||
var path = CompiledBindingPathFromExpressionBuilder.Build<ViewModel, double>(x => x.DoubleValue, true); |
|
||||
var binding = new CompiledBindingExtension(path); |
|
||||
var instanced = binding.Initiate(target, TargetClass.StringProperty); |
|
||||
|
|
||||
Assert.NotNull(instanced); |
|
||||
|
|
||||
var value = instanced.Observable.First(); |
|
||||
|
|
||||
Assert.Equal("42.5", value); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,10 +1,10 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using Avalonia.Markup.Parsers; |
using Avalonia.Data.Core.Parsers; |
||||
using Avalonia.UnitTests; |
using Avalonia.UnitTests; |
||||
using Avalonia.Utilities; |
using Avalonia.Utilities; |
||||
using Xunit; |
using Xunit; |
||||
|
|
||||
namespace Avalonia.Markup.UnitTests.Parsers |
namespace Avalonia.Base.UnitTests.Data.Core.Parsers |
||||
{ |
{ |
||||
public partial class BindingExpressionGrammarTests : ScopedTestBase |
public partial class BindingExpressionGrammarTests : ScopedTestBase |
||||
{ |
{ |
||||
@ -1,7 +1,7 @@ |
|||||
using Avalonia.Data.Core; |
using Avalonia.Data.Core; |
||||
using Xunit; |
using Xunit; |
||||
|
|
||||
namespace Avalonia.Markup.UnitTests.Parsers |
namespace Avalonia.Base.UnitTests.Data.Core.Parsers |
||||
{ |
{ |
||||
public partial class BindingExpressionGrammarTests |
public partial class BindingExpressionGrammarTests |
||||
{ |
{ |
||||
Loading…
Reference in new issue