Browse Source

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.
feature/typed-binding-2
Steven Kirk 4 years ago
parent
commit
74228d6bff
  1. 53
      src/Avalonia.Base/Data/Core/Parsers/ExpressionChainVisitor.cs
  2. 116
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyBindingTrigger.cs
  3. 66
      src/Avalonia.Base/Data/Core/Plugins/InccBindingTrigger.cs
  4. 89
      src/Avalonia.Base/Data/Core/Plugins/InpcBindingTrigger.cs
  5. 16
      src/Avalonia.Base/Data/Core/TypedBindingExpression.cs
  6. 196
      src/Avalonia.Base/Data/Core/TypedBindingExpression`2.cs
  7. 40
      src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs
  8. 38
      src/Avalonia.Base/Data/TypedBinding`1.cs
  9. 31
      src/Avalonia.Base/Data/TypedBinding`2.cs
  10. 2
      tests/Avalonia.Base.UnitTests/Data/Core/Parsers/ExpressionChainVisitorTests.cs
  11. 6
      tests/Avalonia.Base.UnitTests/Data/Core/TypedBindingExpressionTests_Indexer.cs

53
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<TIn> : ExpressionVisitor
internal class ExpressionChainVisitor<TIn> : ExpressionVisitor
{
private readonly LambdaExpression _rootExpression;
private readonly List<Func<TIn, object>> _links = new();
private readonly List<TypedBindingTrigger<TIn>> _triggers = new();
private Expression? _head;
public ExpressionChainVisitor(LambdaExpression expression)
@ -15,11 +17,32 @@ namespace Avalonia.Data.Core.Parsers
_rootExpression = expression;
}
public static Func<TIn, object>[] Build<TOut>(Expression<Func<TIn, TOut>> expression)
public static TypedBindingTrigger<TIn>[] BuildTriggers<TOut>(Expression<Func<TIn, TOut>> expression)
{
var visitor = new ExpressionChainVisitor<TIn>(expression);
visitor.Visit(expression);
return visitor._links.ToArray();
return visitor._triggers.ToArray();
}
public static Action<TIn, TOut> BuildWriteExpression<TOut>(Expression<Func<TIn, TOut>> 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<Action<TIn, TOut>>(
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<Func<TIn, object>>(node.Expression, _rootExpression.Parameters);
_links.Add(link.Compile());
var i = _triggers.Count;
var trigger = AvaloniaPropertyBindingTrigger<TIn>.TryCreate(i, node, _rootExpression) ??
InpcBindingTrigger<TIn>.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<Func<TIn, object>>(node.Object, _rootExpression.Parameters);
_links.Add(link.Compile());
var i = _triggers.Count;
var trigger = InccBindingTrigger<TIn>.TryCreate(i, node, _rootExpression) ??
AvaloniaPropertyBindingTrigger<TIn>.TryCreate(i, node, _rootExpression) ??
InpcBindingTrigger<TIn>.TryCreate(i, node, _rootExpression);
if (trigger is not null)
_triggers.Add(trigger);
_head = node;
}

116
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<TIn> : TypedBindingTrigger<TIn>,
IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>
{
private readonly Func<TIn, object?> _read;
private readonly AvaloniaProperty _property;
private WeakReference<AvaloniaObject?>? _source;
public AvaloniaPropertyBindingTrigger(
int index,
Func<TIn, object?> read,
AvaloniaProperty property)
: base(index)
{
_read = read;
_property = property;
}
internal static TypedBindingTrigger<TIn>? 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<Func<TIn, object>>(node.Expression!, rootExpression.Parameters);
var read = lambda.Compile();
return new AvaloniaPropertyBindingTrigger<TIn>(index, read, property);
}
internal static TypedBindingTrigger<TIn>? 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<Func<TIn, object>>(node.Object!, rootExpression.Parameters);
var read = lambda.Compile();
return new AvaloniaPropertyBindingTrigger<TIn>(index, read, property);
}
void IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>.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;
}
}
}

66
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<TIn> : TypedBindingTrigger<TIn>,
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private readonly Func<TIn, object?> _read;
private WeakReference<INotifyCollectionChanged?>? _source;
public InccBindingTrigger(
int index,
Func<TIn, object?> read)
: base(index)
{
_read = read;
}
internal static TypedBindingTrigger<TIn>? 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<Func<TIn, object>>(node.Object!, rootExpression.Parameters);
var read = lambda.Compile();
return new InccBindingTrigger<TIn>(index, read);
}
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.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);
}
}
}

89
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<TIn> : TypedBindingTrigger<TIn>,
IWeakEventSubscriber<PropertyChangedEventArgs>
{
private readonly Func<TIn, object?> _read;
private readonly string _propertyName;
private WeakReference<INotifyPropertyChanged?>? _source;
public InpcBindingTrigger(
int index,
Func<TIn, object?> read,
string propertyName)
: base(index)
{
_read = read;
_propertyName = propertyName;
}
internal static TypedBindingTrigger<TIn>? 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<Func<TIn, object>>(node.Expression!, rootExpression.Parameters);
var read = lambda.Compile();
return new InpcBindingTrigger<TIn>(index, read, member.Name);
}
internal static TypedBindingTrigger<TIn>? 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<Func<TIn, object>>(node.Object!, rootExpression.Parameters);
var read = lambda.Compile();
return new InpcBindingTrigger<TIn>(index, read, CommonPropertyNames.IndexerName);
}
void IWeakEventSubscriber<PropertyChangedEventArgs>.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);
}
}
}

16
src/Avalonia.Base/Data/Core/TypedBindingExpression.cs

@ -20,7 +20,7 @@ namespace Avalonia.Data.Core
new Single<TIn>(root),
read.Compile(),
null,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -34,7 +34,7 @@ namespace Avalonia.Data.Core
root,
read.Compile(),
null,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -51,7 +51,7 @@ namespace Avalonia.Data.Core
new Single<TIn>(root),
x => convert(compiledRead(x)),
null,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -68,7 +68,7 @@ namespace Avalonia.Data.Core
root,
x => convert(compiledRead(x)),
null,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -83,7 +83,7 @@ namespace Avalonia.Data.Core
new Single<TIn>(root),
read.Compile(),
write,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -98,7 +98,7 @@ namespace Avalonia.Data.Core
root,
read.Compile(),
write,
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -117,7 +117,7 @@ namespace Avalonia.Data.Core
new Single<TIn>(root),
x => convert(compiledRead(x)),
(o, v) => write(o, convertBack(v)),
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}
@ -136,7 +136,7 @@ namespace Avalonia.Data.Core
root,
x => convert(compiledRead(x)),
(o, v) => write(o, convertBack(v)),
ExpressionChainVisitor<TIn>.Build(read),
ExpressionChainVisitor<TIn>.BuildTriggers(read),
fallbackValue);
}

196
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<TIn?> _rootSource;
private readonly Func<TIn, TOut>? _read;
private readonly Action<TIn, TOut>? _write;
private readonly Link[]? _chain;
private readonly TypedBindingTrigger<TIn>[]? _triggers;
private readonly Optional<TOut> _fallbackValue;
private readonly Action<int> _triggerFired;
private IDisposable? _rootSourceSubsciption;
private WeakReference<TIn>? _root;
private Flags _flags;
private bool _initialized;
private bool _rootHasFired;
private bool _listening;
private int _publishCount;
public TypedBindingExpression(
internal TypedBindingExpression(
IObservable<TIn?> root,
Func<TIn, TOut>? read,
Action<TIn, TOut>? write,
Func<TIn, object>[]? links,
TypedBindingTrigger<TIn>[]? triggers,
Optional<TOut> 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<BindingValue<TOut>> observer, bool first)
@ -139,7 +131,7 @@ namespace Avalonia.Data.Core
private void RootChanged(TIn? value)
{
_root = value is null ? null : new WeakReference<TIn>(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<object>(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<IAvaloniaObject, AvaloniaPropertyChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
ao,
nameof(ao.PropertyChanged),
ChainPropertyChanged);
result |= true;
}
else if (o is INotifyPropertyChanged inpc)
{
WeakEventHandlerManager.Subscribe<INotifyPropertyChanged, PropertyChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
inpc,
nameof(inpc.PropertyChanged),
ChainPropertyChanged);
result |= true;
}
if (o is INotifyCollectionChanged incc)
{
WeakEventHandlerManager.Subscribe<INotifyCollectionChanged, NotifyCollectionChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
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<AvaloniaPropertyChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
ao,
nameof(ao.PropertyChanged),
ChainPropertyChanged);
}
else if (o is INotifyPropertyChanged inpc)
{
WeakEventHandlerManager.Unsubscribe<PropertyChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
inpc,
nameof(inpc.PropertyChanged),
ChainPropertyChanged);
}
if (o is INotifyCollectionChanged incc)
{
WeakEventHandlerManager.Unsubscribe<NotifyCollectionChangedEventArgs, TypedBindingExpression<TIn, TOut>>(
incc,
nameof(incc.CollectionChanged),
ChainCollectionChanged);
}
_listening = false;
}
private BindingValue<TOut>? GetResult()
@ -286,7 +179,7 @@ namespace Avalonia.Data.Core
return BindingValue<TOut>.BindingError(e, _fallbackValue);
}
}
else if ((_flags & Flags.RootHasFired) != 0)
else if (_rootHasFired)
{
return BindingValue<TOut>.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<TIn, object> eval)

40
src/Avalonia.Base/Data/Core/TypedBindingTrigger.cs

@ -0,0 +1,40 @@
using System;
namespace Avalonia.Data.Core
{
internal abstract class TypedBindingTrigger<TIn>
{
private readonly int _index;
private Action<int>? _changed;
public TypedBindingTrigger(int index) => _index = index;
public bool Subscribe(TIn root, Action<int> 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();
}
}

38
src/Avalonia.Base/Data/TypedBinding`1.cs

@ -21,7 +21,7 @@ namespace Avalonia.Data
{
Read = read.Compile(),
Write = write,
Links = ExpressionChainVisitor<TIn>.Build(read),
ReadTriggers = ExpressionChainVisitor<TIn>.BuildTriggers(read),
Mode = BindingMode.Default,
};
}
@ -31,43 +31,17 @@ namespace Avalonia.Data
return new TypedBinding<TIn, TOut>
{
Read = read.Compile(),
Links = ExpressionChainVisitor<TIn>.Build(read),
ReadTriggers = ExpressionChainVisitor<TIn>.BuildTriggers(read),
};
}
public static TypedBinding<TIn, TOut> TwoWay<TOut>(Expression<Func<TIn, TOut>> 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<TIn>.Build(expression);
Action<TIn, TOut> 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<TIn, TOut>
{
Read = expression.Compile(),
Write = write,
Links = links,
Write = ExpressionChainVisitor<TIn>.BuildWriteExpression(expression),
ReadTriggers = ExpressionChainVisitor<TIn>.BuildTriggers(expression),
Mode = BindingMode.TwoWay,
};
}
@ -80,7 +54,7 @@ namespace Avalonia.Data
{
Read = read.Compile(),
Write = write,
Links = ExpressionChainVisitor<TIn>.Build(read),
ReadTriggers = ExpressionChainVisitor<TIn>.BuildTriggers(read),
Mode = BindingMode.TwoWay,
};
}
@ -90,7 +64,7 @@ namespace Avalonia.Data
return new TypedBinding<TIn, TOut>
{
Read = read.Compile(),
Links = ExpressionChainVisitor<TIn>.Build(read),
ReadTriggers = ExpressionChainVisitor<TIn>.BuildTriggers(read),
Mode = BindingMode.OneTime,
};
}

31
src/Avalonia.Base/Data/TypedBinding`2.cs

@ -13,14 +13,15 @@ namespace Avalonia.Data
/// <typeparam name="TOut">The binding output.</typeparam>
/// <remarks>
/// <see cref="TypedBinding{TIn, TOut}"/> represents a strongly-typed binding as opposed to
/// <see cref="Binding"/> 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:
///
/// - <see cref="Read"/> reads the value given a binding input
/// - <see cref="Write"/> writes a value given a binding input
/// - <see cref="Links"/> holds a collection of delegates which when passed a binding input
/// return each object traversed by <see cref="Read"/>. 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.
/// - <see cref="ReadTriggers"/> holds a collection of objects which listen for changes in each
/// object traversed by <see cref="Read"/>. 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
/// <see cref="Bind(IAvaloniaObject, DirectPropertyBase{TOut})"/> or
@ -34,32 +35,32 @@ namespace Avalonia.Data
/// <summary>
/// Gets or sets the read function.
/// </summary>
public Func<TIn, TOut>? Read { get; set; }
internal Func<TIn, TOut>? Read { get; set; }
/// <summary>
/// Gets or sets the write function.
/// </summary>
public Action<TIn, TOut>? Write { get; set; }
internal Action<TIn, TOut>? Write { get; set; }
/// <summary>
/// Gets or sets the links in the binding chain.
/// Gets or sets the read triggers.
/// </summary>
public Func<TIn, object>[]? Links { get; set; }
internal TypedBindingTrigger<TIn>[]? ReadTriggers { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
internal BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
internal BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public Optional<TOut> FallbackValue { get; set; }
internal Optional<TOut> FallbackValue { get; set; }
/// <summary>
/// Gets or sets the source for the binding.
@ -67,7 +68,7 @@ namespace Avalonia.Data
/// <remarks>
/// If unset the source is the target control's <see cref="StyledElement.DataContext"/> property.
/// </remarks>
public Optional<TIn> Source { get; set; }
internal Optional<TIn> Source { get; set; }
/// <summary>
/// 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<TIn, TOut>(source, Read, Write, Links, fallback);
return new TypedBindingExpression<TIn, TOut>(source, Read, Write, ReadTriggers, fallback);
}
private BindingMode GetMode(IAvaloniaObject target, AvaloniaProperty property)

2
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<Class1>.Build(x => data.PrependHello(x.Foo));
var result = ExpressionChainVisitor<Class1>.BuildTriggers(x => data.PrependHello(x.Foo));
Assert.Equal(1, result.Length);
}

6
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);

Loading…
Cancel
Save