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/20249/merge
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 Avalonia.Data.Core; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
namespace Avalonia.Data.Core.Parsers |
|||
{ |
|||
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.Collections.Generic; |
|||
using System; |
|||
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 |
|||
{ |
|||
/// <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; } |
|||
namespace Avalonia.Data; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the source for the binding.
|
|||
/// </summary>
|
|||
public object? Source { get; set; } = AvaloniaProperty.UnsetValue; |
|||
|
|||
/// <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
|
|||
/// <summary>
|
|||
/// Provides limited compatibility with the 11.x Binding class. Use <see cref="ReflectionBinding"/>
|
|||
/// for new code.
|
|||
/// </summary>
|
|||
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] |
|||
#if NET8_0_OR_GREATER
|
|||
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)] |
|||
[RequiresDynamicCode(TrimmingMessages.ReflectionBindingRequiresDynamicCodeMessage)] |
|||
#endif
|
|||
internal BindingExpression CreateObservableForTreeDataTemplate(object source) |
|||
{ |
|||
if (!string.IsNullOrEmpty(ElementName)) |
|||
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); |
|||
} |
|||
public class Binding : ReflectionBinding |
|||
{ |
|||
public Binding() { } |
|||
|
|||
if (RelativeSource is not null) |
|||
return ExpressionNodeFactory.CreateRelativeSource(RelativeSource); |
|||
public Binding(string path) : base(path) { } |
|||
|
|||
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 Avalonia.Markup.Parsers; |
|||
using Avalonia.Data.Core.Parsers; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.UnitTests.Parsers |
|||
namespace Avalonia.Base.UnitTests.Data.Core.Parsers |
|||
{ |
|||
public partial class BindingExpressionGrammarTests : ScopedTestBase |
|||
{ |
|||
@ -1,7 +1,7 @@ |
|||
using Avalonia.Data.Core; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.UnitTests.Parsers |
|||
namespace Avalonia.Base.UnitTests.Data.Core.Parsers |
|||
{ |
|||
public partial class BindingExpressionGrammarTests |
|||
{ |
|||
Loading…
Reference in new issue