Skip to content

Commit

Permalink
Added experimental typed bindings.
Browse files Browse the repository at this point in the history
Ported over from AvaloniaUI/Avalonia#5510.
  • Loading branch information
grokys committed Feb 19, 2021
1 parent a175171 commit 0e5b245
Show file tree
Hide file tree
Showing 6 changed files with 748 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

#nullable enable

namespace Avalonia.Data.Core.Parsers
{
public class ExpressionChainVisitor<TIn> : ExpressionVisitor
{
private readonly LambdaExpression _rootExpression;
private List<Func<TIn, object>> _links = new List<Func<TIn, object>>();

public ExpressionChainVisitor(LambdaExpression expression)
{
_rootExpression = expression;
}

public static Func<TIn, object>[] Build<TOut>(Expression<Func<TIn, TOut>> expression)
{
var visitor = new ExpressionChainVisitor<TIn>(expression);
visitor.Visit(expression);
visitor._links.Reverse();
return visitor._links.ToArray();
}

protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression?.GetType().IsValueType == false)
{
var link = Expression.Lambda<Func<TIn, object>>(node.Expression, _rootExpression.Parameters);
_links.Add(link.Compile());
}

return base.VisitMember(node);
}

protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Object?.GetType().IsValueType == false)
{
var link = Expression.Lambda<Func<TIn, object>>(node.Object, _rootExpression.Parameters);
_links.Add(link.Compile());
}

return base.VisitMethodCall(node);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Reactive;
using Avalonia.Utilities;

#nullable enable

namespace Avalonia.Experimental.Data.Core
{
/// <summary>
/// A binding expression which uses delegates to read and write a bound value.
/// </summary>
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <remarks>
/// A <see cref="TypedBindingExpression{TIn, TOut}"/> represents a typed binding which has been
/// instantiated on an object.
/// </remarks>
public class TypedBindingExpression<TIn, TOut> : LightweightObservableBase<BindingValue<TOut>>,
ISubject<BindingValue<TOut>>,
IDescription
where TIn : class
{
private readonly IObservable<TIn> _rootSource;
private readonly Func<TIn, TOut> _read;
private readonly Action<TIn, TOut>? _write;
private readonly Link[]? _chain;
private readonly Optional<TOut> _fallbackValue;
private IDisposable? _rootSourceSubsciption;
private WeakReference<TIn>? _root;
private bool _rootHasFired;
private int _publishCount;

public TypedBindingExpression(
IObservable<TIn> root,
Func<TIn, TOut> read,
Action<TIn, TOut>? write,
Func<TIn, object>[] links,
Optional<TOut> fallbackValue)
{
_rootSource = root ?? throw new ArgumentNullException(nameof(root));
_read = read;
_write = write;
_fallbackValue = fallbackValue;

if (links != null)
{
_chain = new Link[links.Length];

for (var i = 0; i < links.Length; ++i)
{
_chain[i] = new Link(links[i]);
}
}
}

public string Description => "TODO";

public void OnNext(BindingValue<TOut> value)
{
if (value.HasValue &&
_write is object &&
_root is object &&
_root.TryGetTarget(out var root))
{
try
{
var c = _publishCount;
_write.Invoke(root, value.Value);
if (_publishCount == c)
PublishValue();
}
catch
{
PublishValue();
}
}
}

void IObserver<BindingValue<TOut>>.OnCompleted()
{
}

void IObserver<BindingValue<TOut>>.OnError(Exception error)
{
}

protected override void Initialize()
{
_rootHasFired = false;
_rootSourceSubsciption = _rootSource.Subscribe(RootChanged);
}

protected override void Deinitialize()
{
StopListeningToChain(0);
_rootSourceSubsciption?.Dispose();
_rootSourceSubsciption = null;
}

protected override void Subscribed(IObserver<BindingValue<TOut>> observer, bool first)
{
// If this is the first subscription, `Initialize()` will have run which will
// already have produced a value.
if (!first)
{
var result = GetResult();

if (result.HasValue)
{
observer.OnNext(result.Value);
}
}
}

private void RootChanged(TIn value)
{
_root = new WeakReference<TIn>(value);
_rootHasFired = true;
StopListeningToChain(0);
ListenToChain(0);
PublishValue();
}

private void ListenToChain(int from)
{
if (_chain != 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.
}
}
}

private void StopListeningToChain(int from)
{
if (_chain != null && _root != null && _root.TryGetTarget(out _))
{
for (var i = from; i < _chain.Length; ++i)
{
var link = _chain[i];

if (link.Value is object && link.Value.TryGetTarget(out var o))
{
UnsubscribeToChanges(o);
}
}
}
}

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

private BindingValue<TOut>? GetResult()
{
if (_root != null && _root.TryGetTarget(out var root))
{
try
{
var value = _read(root);
return new BindingValue<TOut>(value);
}
catch (Exception e)
{
return BindingValue<TOut>.BindingError(e, _fallbackValue);
}
}
else if (_rootHasFired)
{
return BindingValue<TOut>.BindingError(new NullReferenceException(), _fallbackValue);
}
else
{
return null;
}
}

private void PublishValue()
{
var result = GetResult();

if (result.HasValue)
{
unchecked
{ ++_publishCount; }
PublishNext(result.Value);
}
}

private int ChainIndexOf(object o)
{
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)
{
var index = ChainIndexOf(sender);

if (index != -1)
{
StopListeningToChain(index);
ListenToChain(index);
}

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)
{
Eval = eval;
Value = null;
}

public Func<TIn, object> Eval;
public WeakReference<object>? Value;
}
}
}
Loading

0 comments on commit 0e5b245

Please sign in to comment.