From 74228d6bffba85dde06f400ad06d2c783ec6b096 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 5 Aug 2022 16:36:21 +0200 Subject: [PATCH] Refactored typed bindings to use "triggers". A little more complex than just a list of delegates, but allows us to listen for property change notifications for an individual property. Also made much of the API internal. You must go via factory methods to create TypedBindings/Expressions. --- .../Core/Parsers/ExpressionChainVisitor.cs | 53 ++++- .../Plugins/AvaloniaPropertyBindingTrigger.cs | 116 +++++++++++ .../Data/Core/Plugins/InccBindingTrigger.cs | 66 ++++++ .../Data/Core/Plugins/InpcBindingTrigger.cs | 89 ++++++++ .../Data/Core/TypedBindingExpression.cs | 16 +- .../Data/Core/TypedBindingExpression`2.cs | 196 +++--------------- .../Data/Core/TypedBindingTrigger.cs | 40 ++++ src/Avalonia.Base/Data/TypedBinding`1.cs | 38 +--- src/Avalonia.Base/Data/TypedBinding`2.cs | 31 +-- .../Parsers/ExpressionChainVisitorTests.cs | 2 +- .../TypedBindingExpressionTests_Indexer.cs | 6 +- 11 files changed, 416 insertions(+), 237 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyBindingTrigger.cs create mode 100644 src/Avalonia.Base/Data/Core/Plugins/InccBindingTrigger.cs create mode 100644 src/Avalonia.Base/Data/Core/Plugins/InpcBindingTrigger.cs create mode 100644 src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs 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);