diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionChainVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionChainVisitor.cs index 18af9e820b..47552d28cb 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionChainVisitor.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionChainVisitor.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core.Parsers { - public class ExpressionChainVisitor : ExpressionVisitor + internal class ExpressionChainVisitor : ExpressionVisitor { private readonly LambdaExpression _rootExpression; - private readonly List> _links = new(); + private readonly List> _triggers = new(); private Expression? _head; public ExpressionChainVisitor(LambdaExpression expression) @@ -15,11 +17,32 @@ namespace Avalonia.Data.Core.Parsers _rootExpression = expression; } - public static Func[] Build(Expression> expression) + public static TypedBindingTrigger[] BuildTriggers(Expression> expression) { var visitor = new ExpressionChainVisitor(expression); visitor.Visit(expression); - return visitor._links.ToArray(); + return visitor._triggers.ToArray(); + } + + public static Action BuildWriteExpression(Expression> expression) + { + var property = (expression.Body as MemberExpression)?.Member as PropertyInfo ?? + throw new ArgumentException( + $"Cannot create a two-way binding for '{expression}' because the expression does not target a property.", + nameof(expression)); + + if (property.GetSetMethod() is not MethodInfo setMethod) + throw new ArgumentException( + $"Cannot create a two-way binding for '{expression}' because the property has no setter.", + nameof(expression)); + + var instanceParam = Expression.Parameter(typeof(TIn), "x"); + var valueParam = Expression.Parameter(typeof(TOut), "value"); + var lambda = Expression.Lambda>( + Expression.Call(instanceParam, setMethod, valueParam), + instanceParam, + valueParam); + return lambda.Compile(); } protected override Expression VisitBinary(BinaryExpression node) @@ -36,10 +59,16 @@ namespace Avalonia.Data.Core.Parsers if (node.Expression is not null && node.Expression == _head && - node.Expression.Type.IsValueType == false) + node.Expression.Type.IsValueType == false && + node.Member.MemberType == MemberTypes.Property) { - var link = Expression.Lambda>(node.Expression, _rootExpression.Parameters); - _links.Add(link.Compile()); + var i = _triggers.Count; + var trigger = AvaloniaPropertyBindingTrigger.TryCreate(i, node, _rootExpression) ?? + InpcBindingTrigger.TryCreate(i, node, _rootExpression); + + if (trigger is not null) + _triggers.Add(trigger); + _head = node; } @@ -54,8 +83,14 @@ namespace Avalonia.Data.Core.Parsers node.Object == _head && node.Type.IsValueType == false) { - var link = Expression.Lambda>(node.Object, _rootExpression.Parameters); - _links.Add(link.Compile()); + var i = _triggers.Count; + var trigger = InccBindingTrigger.TryCreate(i, node, _rootExpression) ?? + AvaloniaPropertyBindingTrigger.TryCreate(i, node, _rootExpression) ?? + InpcBindingTrigger.TryCreate(i, node, _rootExpression); + + if (trigger is not null) + _triggers.Add(trigger); + _head = node; } diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyBindingTrigger.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyBindingTrigger.cs new file mode 100644 index 0000000000..6d6c83caf2 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyBindingTrigger.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core.Plugins +{ + internal class AvaloniaPropertyBindingTrigger : TypedBindingTrigger, + IWeakEventSubscriber + { + private readonly Func _read; + private readonly AvaloniaProperty _property; + private WeakReference? _source; + + public AvaloniaPropertyBindingTrigger( + int index, + Func read, + AvaloniaProperty property) + : base(index) + { + _read = read; + _property = property; + } + + internal static TypedBindingTrigger? TryCreate( + int index, + MemberExpression node, + LambdaExpression rootExpression) + { + var type = node.Expression?.Type; + var member = node.Member; + + if (member.DeclaringType is null || + member.MemberType != MemberTypes.Property || + !typeof(AvaloniaObject).IsAssignableFrom(type)) + return null; + + var property = GetProperty(member); + + if (property is null) + return null; + + var lambda = Expression.Lambda>(node.Expression!, rootExpression.Parameters); + var read = lambda.Compile(); + + return new AvaloniaPropertyBindingTrigger(index, read, property); + } + + internal static TypedBindingTrigger? TryCreate( + int index, + MethodCallExpression node, + LambdaExpression rootExpression) + { + var type = node.Object?.Type; + var method = node.Method; + + if (method.Name != "get_Item" || + method.DeclaringType is null || + node.Arguments.Count != 1 || + GetProperty(node.Arguments[0]) is not AvaloniaProperty property || + !typeof(AvaloniaObject).IsAssignableFrom(type)) + return null; + + var lambda = Expression.Lambda>(node.Object!, rootExpression.Parameters); + var read = lambda.Compile(); + + return new AvaloniaPropertyBindingTrigger(index, read, property); + } + + void IWeakEventSubscriber.OnEvent( + object? sender, + WeakEvent ev, + AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + OnChanged(); + } + + protected override bool SubscribeCore(TIn root) + { + var o = _read(root) as AvaloniaObject; + _source = new(o); + + if (o is null) + return false; + + WeakEvents.AvaloniaPropertyChanged.Subscribe(o, this); + return true; + } + + protected override void UnsubscribeCore() + { + if (_source?.TryGetTarget(out var o) == true) + WeakEvents.AvaloniaPropertyChanged.Unsubscribe(o, this); + } + + private static AvaloniaProperty? GetProperty(Expression expression) + { + if (expression is not MemberExpression member || + member.Member is not FieldInfo field || + !field.IsStatic) + return null; + + return field.GetValue(null) as AvaloniaProperty; + } + + private static AvaloniaProperty? GetProperty(MemberInfo member) + { + var propertyName = member.Name; + var propertyField = member.DeclaringType?.GetField( + propertyName + "Property", + BindingFlags.Static); + return propertyField?.GetValue(null) as AvaloniaProperty; + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/InccBindingTrigger.cs b/src/Avalonia.Base/Data/Core/Plugins/InccBindingTrigger.cs new file mode 100644 index 0000000000..fe6d0b4108 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/InccBindingTrigger.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Specialized; +using System.Linq.Expressions; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core.Plugins +{ + internal class InccBindingTrigger : TypedBindingTrigger, + IWeakEventSubscriber + { + private readonly Func _read; + private WeakReference? _source; + + public InccBindingTrigger( + int index, + Func read) + : base(index) + { + _read = read; + } + + internal static TypedBindingTrigger? TryCreate( + int index, + MethodCallExpression node, + LambdaExpression rootExpression) + { + var type = node.Object?.Type; + var method = node.Method; + + if (method.Name != "get_Item" || + !typeof(INotifyCollectionChanged).IsAssignableFrom(type)) + return null; + + var lambda = Expression.Lambda>(node.Object!, rootExpression.Parameters); + var read = lambda.Compile(); + + return new InccBindingTrigger(index, read); + } + + void IWeakEventSubscriber.OnEvent( + object? sender, + WeakEvent ev, + NotifyCollectionChangedEventArgs e) + { + OnChanged(); + } + + protected override bool SubscribeCore(TIn root) + { + var o = _read(root) as INotifyCollectionChanged; + _source = new(o); + + if (o is null) + return false; + + WeakEvents.CollectionChanged.Subscribe(o, this); + return true; + } + + protected override void UnsubscribeCore() + { + if (_source?.TryGetTarget(out var o) == true) + WeakEvents.CollectionChanged.Unsubscribe(o, this); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcBindingTrigger.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcBindingTrigger.cs new file mode 100644 index 0000000000..60747f13ca --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcBindingTrigger.cs @@ -0,0 +1,89 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using Avalonia.Utilities; + +namespace Avalonia.Data.Core.Plugins +{ + internal class InpcBindingTrigger : TypedBindingTrigger, + IWeakEventSubscriber + { + private readonly Func _read; + private readonly string _propertyName; + private WeakReference? _source; + + public InpcBindingTrigger( + int index, + Func read, + string propertyName) + : base(index) + { + _read = read; + _propertyName = propertyName; + } + + internal static TypedBindingTrigger? TryCreate( + int index, + MemberExpression node, + LambdaExpression rootExpression) + { + var type = node.Expression?.Type; + var member = node.Member; + + if (member.MemberType != MemberTypes.Property || + !typeof(INotifyPropertyChanged).IsAssignableFrom(type)) + return null; + + var lambda = Expression.Lambda>(node.Expression!, rootExpression.Parameters); + var read = lambda.Compile(); + + return new InpcBindingTrigger(index, read, member.Name); + } + + internal static TypedBindingTrigger? TryCreate( + int index, + MethodCallExpression node, + LambdaExpression rootExpression) + { + var type = node.Object?.Type; + + if (node.Method.Name != "get_Item" || + !typeof(INotifyPropertyChanged).IsAssignableFrom(type) || + node.Arguments.Count != 1) + return null; + + var lambda = Expression.Lambda>(node.Object!, rootExpression.Parameters); + var read = lambda.Compile(); + + return new InpcBindingTrigger(index, read, CommonPropertyNames.IndexerName); + } + + void IWeakEventSubscriber.OnEvent( + object? sender, + WeakEvent ev, + PropertyChangedEventArgs e) + { + if (e.PropertyName == _propertyName || string.IsNullOrEmpty(e.PropertyName)) + OnChanged(); + } + + protected override bool SubscribeCore(TIn root) + { + var o = _read(root) as INotifyPropertyChanged; + _source = new(o); + + if (o is null) + return false; + + WeakEvents.PropertyChanged.Subscribe(o, this); + return true; + } + + protected override void UnsubscribeCore() + { + if (_source?.TryGetTarget(out var o) == true) + WeakEvents.PropertyChanged.Unsubscribe(o, this); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/TypedBindingExpression.cs b/src/Avalonia.Base/Data/Core/TypedBindingExpression.cs index 0e6f1dfe9b..60199048f0 100644 --- a/src/Avalonia.Base/Data/Core/TypedBindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/TypedBindingExpression.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data.Core new Single(root), read.Compile(), null, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -34,7 +34,7 @@ namespace Avalonia.Data.Core root, read.Compile(), null, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -51,7 +51,7 @@ namespace Avalonia.Data.Core new Single(root), x => convert(compiledRead(x)), null, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -68,7 +68,7 @@ namespace Avalonia.Data.Core root, x => convert(compiledRead(x)), null, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -83,7 +83,7 @@ namespace Avalonia.Data.Core new Single(root), read.Compile(), write, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -98,7 +98,7 @@ namespace Avalonia.Data.Core root, read.Compile(), write, - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -117,7 +117,7 @@ namespace Avalonia.Data.Core new Single(root), x => convert(compiledRead(x)), (o, v) => write(o, convertBack(v)), - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } @@ -136,7 +136,7 @@ namespace Avalonia.Data.Core root, x => convert(compiledRead(x)), (o, v) => write(o, convertBack(v)), - ExpressionChainVisitor.Build(read), + ExpressionChainVisitor.BuildTriggers(read), fallbackValue); } diff --git a/src/Avalonia.Base/Data/Core/TypedBindingExpression`2.cs b/src/Avalonia.Base/Data/Core/TypedBindingExpression`2.cs index 005eb19529..500a7517dc 100644 --- a/src/Avalonia.Base/Data/Core/TypedBindingExpression`2.cs +++ b/src/Avalonia.Base/Data/Core/TypedBindingExpression`2.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Specialized; -using System.ComponentModel; using System.Reactive.Subjects; using Avalonia.Reactive; -using Avalonia.Utilities; namespace Avalonia.Data.Core { @@ -24,34 +21,29 @@ namespace Avalonia.Data.Core private readonly IObservable _rootSource; private readonly Func? _read; private readonly Action? _write; - private readonly Link[]? _chain; + private readonly TypedBindingTrigger[]? _triggers; private readonly Optional _fallbackValue; + private readonly Action _triggerFired; private IDisposable? _rootSourceSubsciption; private WeakReference? _root; - private Flags _flags; + private bool _initialized; + private bool _rootHasFired; + private bool _listening; private int _publishCount; - public TypedBindingExpression( + internal TypedBindingExpression( IObservable root, Func? read, Action? write, - Func[]? links, + TypedBindingTrigger[]? triggers, Optional fallbackValue) { _rootSource = root ?? throw new ArgumentNullException(nameof(root)); _read = read; _write = write; + _triggers = triggers; _fallbackValue = fallbackValue; - - if (links != null) - { - _chain = new Link[links.Length]; - - for (var i = 0; i < links.Length; ++i) - { - _chain[i] = new Link(links[i]); - } - } + _triggerFired = TriggerFired; } public string Description => "TODO"; @@ -72,7 +64,7 @@ namespace Avalonia.Data.Core try { var c = _publishCount; - if ((_flags & Flags.Initialized) != 0) + if (_initialized) _write.Invoke(root, value); if (_publishCount == c) PublishValue(); @@ -108,9 +100,9 @@ namespace Avalonia.Data.Core protected override void Initialize() { - _flags &= ~Flags.RootHasFired; + _rootHasFired = false; _rootSourceSubsciption = _rootSource.Subscribe(RootChanged); - _flags |= Flags.Initialized; + _initialized = true; } protected override void Deinitialize() @@ -118,7 +110,7 @@ namespace Avalonia.Data.Core StopListeningToChain(0); _rootSourceSubsciption?.Dispose(); _rootSourceSubsciption = null; - _flags &= ~Flags.Initialized; + _initialized = false; } protected override void Subscribed(IObserver> observer, bool first) @@ -139,7 +131,7 @@ namespace Avalonia.Data.Core private void RootChanged(TIn? value) { _root = value is null ? null : new WeakReference(value); - _flags |= Flags.RootHasFired; + _rootHasFired = true; StopListeningToChain(0); ListenToChain(0); PublishValue(); @@ -147,126 +139,27 @@ namespace Avalonia.Data.Core private void ListenToChain(int from) { - if (_chain != null && _root != null && _root.TryGetTarget(out var root)) + if (_triggers != null && _root != null && _root.TryGetTarget(out var root)) { - object? last = null; - - try - { - for (var i = from; i < _chain.Length; ++i) - { - var o = _chain[i].Eval(root); - - if (o != last) - { - _chain[i].Value = new WeakReference(o); - - if (SubscribeToChanges(o)) - { - last = o; - } - } - } - } - catch - { - // Broken expression chain. - } - finally - { - _flags |= Flags.ListeningToChain; - } + for (var i = from; i < _triggers.Length; ++i) + if (!_triggers[i].Subscribe(root, _triggerFired)) + break; + _listening = true; } } private void StopListeningToChain(int from) { - if ((_flags & Flags.ListeningToChain) == 0) + if (!_listening) return; - if (_chain != null && _root != null && _root.TryGetTarget(out _)) + if (_triggers != null && _root != null && _root.TryGetTarget(out _)) { - for (var i = from; i < _chain.Length; ++i) - { - var link = _chain[i]; - - if (link.Value is not null && link.Value.TryGetTarget(out var o)) - { - UnsubscribeToChanges(o); - } - } + for (var i = from; i < _triggers.Length; ++i) + _triggers[i].Unsubscribe(); } - _flags &= ~Flags.ListeningToChain; - } - - private bool SubscribeToChanges(object o) - { - if (o is null) - { - return false; - } - - var result = false; - - if (o is IAvaloniaObject ao) - { - WeakEventHandlerManager.Subscribe>( - ao, - nameof(ao.PropertyChanged), - ChainPropertyChanged); - result |= true; - } - else if (o is INotifyPropertyChanged inpc) - { - WeakEventHandlerManager.Subscribe>( - inpc, - nameof(inpc.PropertyChanged), - ChainPropertyChanged); - result |= true; - } - - if (o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Subscribe>( - incc, - nameof(incc.CollectionChanged), - ChainCollectionChanged); - result |= true; - } - - return result; - } - - private void UnsubscribeToChanges(object o) - { - if (o is null) - { - return; - } - - if (o is IAvaloniaObject ao) - { - WeakEventHandlerManager.Unsubscribe>( - ao, - nameof(ao.PropertyChanged), - ChainPropertyChanged); - } - else if (o is INotifyPropertyChanged inpc) - { - WeakEventHandlerManager.Unsubscribe>( - inpc, - nameof(inpc.PropertyChanged), - ChainPropertyChanged); - } - - if (o is INotifyCollectionChanged incc) - { - WeakEventHandlerManager.Unsubscribe>( - incc, - nameof(incc.CollectionChanged), - ChainCollectionChanged); - } + _listening = false; } private BindingValue? GetResult() @@ -286,7 +179,7 @@ namespace Avalonia.Data.Core return BindingValue.BindingError(e, _fallbackValue); } } - else if ((_flags & Flags.RootHasFired) != 0) + else if (_rootHasFired) { return BindingValue.BindingError(new NullReferenceException(), _fallbackValue); } @@ -307,46 +200,13 @@ namespace Avalonia.Data.Core } } - private int ChainIndexOf(object o) + private void TriggerFired(int index) { - if (_chain != null) - { - for (var i = 0; i < _chain.Length; ++i) - { - var link = _chain[i]; - - if (link.Value != null && - link.Value.TryGetTarget(out var q) && - ReferenceEquals(o, q)) - { - return i; - } - } - } - - return -1; - } - - private void ChainPropertyChanged(object? sender) - { - if (sender is null) - return; - - var index = ChainIndexOf(sender); - - if (index != -1) - { - StopListeningToChain(index); - ListenToChain(index); - } - + StopListeningToChain(index + 1); + ListenToChain(index + 1); PublishValue(); } - private void ChainPropertyChanged(object? sender, PropertyChangedEventArgs e) => ChainPropertyChanged(sender); - private void ChainPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => ChainPropertyChanged(sender); - private void ChainCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => ChainPropertyChanged(sender); - private struct Link { public Link(Func eval) diff --git a/src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs b/src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs new file mode 100644 index 0000000000..98c7963152 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs @@ -0,0 +1,40 @@ +using System; + +namespace Avalonia.Data.Core +{ + internal abstract class TypedBindingTrigger + { + private readonly int _index; + private Action? _changed; + + public TypedBindingTrigger(int index) => _index = index; + + + public bool Subscribe(TIn root, Action changed) + { + if (_changed is not null) + throw new AvaloniaInternalException("Trigger is already subscribed."); + + try + { + var result = SubscribeCore(root); + _changed = changed; + return result; + } + catch + { + return false; + } + } + + public void Unsubscribe() + { + _changed = null; + UnsubscribeCore(); + } + + protected void OnChanged() => _changed?.Invoke(_index); + protected abstract bool SubscribeCore(TIn root); + protected abstract void UnsubscribeCore(); + } +} diff --git a/src/Avalonia.Base/Data/TypedBinding`1.cs b/src/Avalonia.Base/Data/TypedBinding`1.cs index d2165cf648..32e5c48121 100644 --- a/src/Avalonia.Base/Data/TypedBinding`1.cs +++ b/src/Avalonia.Base/Data/TypedBinding`1.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data { Read = read.Compile(), Write = write, - Links = ExpressionChainVisitor.Build(read), + ReadTriggers = ExpressionChainVisitor.BuildTriggers(read), Mode = BindingMode.Default, }; } @@ -31,43 +31,17 @@ namespace Avalonia.Data return new TypedBinding { Read = read.Compile(), - Links = ExpressionChainVisitor.Build(read), + ReadTriggers = ExpressionChainVisitor.BuildTriggers(read), }; } public static TypedBinding TwoWay(Expression> expression) { - var property = (expression.Body as MemberExpression)?.Member as PropertyInfo ?? - throw new ArgumentException( - $"Cannot create a two-way binding for '{expression}' because the expression does not target a property.", - nameof(expression)); - - if (property.GetGetMethod() is null) - throw new ArgumentException( - $"Cannot create a two-way binding for '{expression}' because the property has no getter.", - nameof(expression)); - - if (property.GetSetMethod() is null) - throw new ArgumentException( - $"Cannot create a two-way binding for '{expression}' because the property has no setter.", - nameof(expression)); - - // TODO: This is using reflection and mostly untested. Unit test it properly and - // benchmark it against creating an expression. - var links = ExpressionChainVisitor.Build(expression); - Action write = links.Length == 1 ? - (o, v) => property.SetValue(o, v) : - (root, v) => - { - var o = links[links.Length - 2](root); - property.SetValue(o, v); - }; - return new TypedBinding { Read = expression.Compile(), - Write = write, - Links = links, + Write = ExpressionChainVisitor.BuildWriteExpression(expression), + ReadTriggers = ExpressionChainVisitor.BuildTriggers(expression), Mode = BindingMode.TwoWay, }; } @@ -80,7 +54,7 @@ namespace Avalonia.Data { Read = read.Compile(), Write = write, - Links = ExpressionChainVisitor.Build(read), + ReadTriggers = ExpressionChainVisitor.BuildTriggers(read), Mode = BindingMode.TwoWay, }; } @@ -90,7 +64,7 @@ namespace Avalonia.Data return new TypedBinding { Read = read.Compile(), - Links = ExpressionChainVisitor.Build(read), + ReadTriggers = ExpressionChainVisitor.BuildTriggers(read), Mode = BindingMode.OneTime, }; } diff --git a/src/Avalonia.Base/Data/TypedBinding`2.cs b/src/Avalonia.Base/Data/TypedBinding`2.cs index 8ce8a1a9ed..56278affa1 100644 --- a/src/Avalonia.Base/Data/TypedBinding`2.cs +++ b/src/Avalonia.Base/Data/TypedBinding`2.cs @@ -13,14 +13,15 @@ namespace Avalonia.Data /// The binding output. /// /// represents a strongly-typed binding as opposed to - /// which boxes value types. It is represented as a set of delegates: + /// Binding which boxes value types. It is represented as a read and write delegate plus a + /// collection of triggers: /// /// - reads the value given a binding input /// - writes a value given a binding input - /// - holds a collection of delegates which when passed a binding input - /// return each object traversed by . For example if Read is implemented - /// as `x => x.Foo.Bar.Baz` then there would be three links: `x => x.Foo`, `x => x.Foo.Bar` - /// and `x => x.Foo.Bar.Baz`. These links are used to subscribe to change notifications. + /// - holds a collection of objects which listen for changes in each + /// object traversed by . For example if Read is implemented as + /// `x => x.Foo.Bar.Baz` then there would be three triggers: `x => x.Foo`, `x => x.Foo.Bar` + /// and `x => x.Foo.Bar.Baz`. /// /// This class represents a binding which has not been instantiated on an object. When the /// or @@ -34,32 +35,32 @@ namespace Avalonia.Data /// /// Gets or sets the read function. /// - public Func? Read { get; set; } + internal Func? Read { get; set; } /// /// Gets or sets the write function. /// - public Action? Write { get; set; } + internal Action? Write { get; set; } /// - /// Gets or sets the links in the binding chain. + /// Gets or sets the read triggers. /// - public Func[]? Links { get; set; } + internal TypedBindingTrigger[]? ReadTriggers { get; set; } /// /// Gets or sets the binding mode. /// - public BindingMode Mode { get; set; } + internal BindingMode Mode { get; set; } /// /// Gets or sets the binding priority. /// - public BindingPriority Priority { get; set; } + internal BindingPriority Priority { get; set; } /// /// Gets or sets the value to use when the binding is unable to produce a value. /// - public Optional FallbackValue { get; set; } + internal Optional FallbackValue { get; set; } /// /// Gets or sets the source for the binding. @@ -67,7 +68,7 @@ namespace Avalonia.Data /// /// If unset the source is the target control's property. /// - public Optional Source { get; set; } + internal Optional Source { get; set; } /// /// Creates a binding to the specified styled property. @@ -164,13 +165,13 @@ namespace Avalonia.Data if (mode != BindingMode.OneWayToSource) { _ = Read ?? throw new InvalidOperationException("Cannot bind TypedBinding: Read is uninitialized."); - _ = Links ?? throw new InvalidOperationException("Cannot bind TypedBinding: Links is uninitialized."); + _ = ReadTriggers ?? throw new InvalidOperationException("Cannot bind TypedBinding: Links is uninitialized."); } if ((mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource) && Write is null) throw new InvalidOperationException($"Cannot bind TypedBinding {Mode}: Write is uninitialized."); - return new TypedBindingExpression(source, Read, Write, Links, fallback); + return new TypedBindingExpression(source, Read, Write, ReadTriggers, fallback); } private BindingMode GetMode(IAvaloniaObject target, AvaloniaProperty property) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/ExpressionChainVisitorTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/ExpressionChainVisitorTests.cs index ed8b444d11..96b807cb6a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/ExpressionChainVisitorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Parsers/ExpressionChainVisitorTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.Base.UnitTests.Data.Core.Parsers // // In this case we don't want to subscribe to INPC notifications from `this`. var data = new Class2(); - var result = ExpressionChainVisitor.Build(x => data.PrependHello(x.Foo)); + var result = ExpressionChainVisitor.BuildTriggers(x => data.PrependHello(x.Foo)); Assert.Equal(1, result.Length); } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/TypedBindingExpressionTests_Indexer.cs b/tests/Avalonia.Base.UnitTests/Data/Core/TypedBindingExpressionTests_Indexer.cs index f25537ec41..0c0a71bd9e 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/TypedBindingExpressionTests_Indexer.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/TypedBindingExpressionTests_Indexer.cs @@ -129,8 +129,7 @@ namespace Avalonia.Base.UnitTests.Data.Core data.Foo.RemoveAt(0); } - // Second "bar" comes from Count property changing. - Assert.Equal(new[] { "foo", "bar", "bar" }, result); + Assert.Equal(new[] { "foo", "bar" }, result); Assert.Null(((INotifyCollectionChangedDebug)data.Foo).GetCollectionChangedSubscribers()); GC.KeepAlive(data); @@ -167,8 +166,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var sub = binding.Subscribe(x => result.Add(x.Value)); data.Foo.Move(0, 1); - // Second "foo" comes from Count property changing. - Assert.Equal(new[] { "bar", "foo", "foo" }, result); + Assert.Equal(new[] { "bar", "foo" }, result); GC.KeepAlive(sub); GC.KeepAlive(data);