New binding mechanism: Expression tracking
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using TerminalUI.Controls;
|
||||
using TerminalUI.ExpressionTrackers;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult>
|
||||
public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult>, IDisposable
|
||||
{
|
||||
private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
|
||||
private IView<TDataContext> _dataSourceView;
|
||||
@@ -15,6 +16,7 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
private readonly Func<TExpressionResult, TResult> _converter;
|
||||
private readonly TResult? _fallbackValue;
|
||||
private IDisposableCollection? _propertySourceDisposableCollection;
|
||||
private readonly string _parameterName;
|
||||
|
||||
public Binding(
|
||||
IView<TDataContext> dataSourceView,
|
||||
@@ -23,7 +25,7 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
PropertyInfo targetProperty,
|
||||
Func<TExpressionResult, TResult> converter,
|
||||
TResult? fallbackValue = default
|
||||
) : base(() => dataSourceView.DataContext, dataSourceExpression)
|
||||
) : base(dataSourceExpression)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataSourceView);
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
@@ -37,14 +39,14 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
_converter = converter;
|
||||
_fallbackValue = fallbackValue;
|
||||
|
||||
UpdateTrackers();
|
||||
_parameterName = dataSourceExpression.Parameters[0].Name!;
|
||||
Parameters.SetValue(_parameterName, dataSourceView.DataContext);
|
||||
|
||||
|
||||
dataSourceView.PropertyChanged += View_PropertyChanged;
|
||||
UpdateTargetProperty();
|
||||
Update(true);
|
||||
|
||||
AddToSourceDisposables(propertySource);
|
||||
|
||||
dataSourceView.AddDisposable(this);
|
||||
}
|
||||
|
||||
private void AddToSourceDisposables(object? propertySource)
|
||||
@@ -60,18 +62,23 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
{
|
||||
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
|
||||
|
||||
UpdateTrackers();
|
||||
UpdateTargetProperty();
|
||||
Parameters.SetValue(_parameterName, _dataSourceView.DataContext);
|
||||
Update(true);
|
||||
}
|
||||
|
||||
protected override void Update(string propertyPath) => UpdateTargetProperty();
|
||||
|
||||
private void UpdateTargetProperty()
|
||||
protected override void Update(bool couldCompute)
|
||||
{
|
||||
TResult value;
|
||||
TResult? value;
|
||||
try
|
||||
{
|
||||
value = _converter(_dataContextMapper(_dataSourceView.DataContext));
|
||||
if (couldCompute)
|
||||
{
|
||||
value = _converter(_dataContextMapper(_dataSourceView.DataContext));
|
||||
}
|
||||
else
|
||||
{
|
||||
value = _fallbackValue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -87,9 +94,9 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
public void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
//base.Dispose();
|
||||
_propertySourceDisposableCollection?.RemoveDisposable(this);
|
||||
_dataSourceView.RemoveDisposable(this);
|
||||
_dataSourceView.PropertyChanged -= View_PropertyChanged;
|
||||
|
||||
@@ -27,11 +27,11 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
|
||||
|
||||
public TextBlock()
|
||||
{
|
||||
this.Bind(
|
||||
/*this.Bind(
|
||||
this,
|
||||
dc => dc == null ? string.Empty : dc.ToString(),
|
||||
tb => tb.Text
|
||||
);
|
||||
);*/
|
||||
|
||||
RerenderProperties.Add(nameof(Text));
|
||||
RerenderProperties.Add(nameof(TextAlignment));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
@@ -32,14 +33,15 @@ public class EventLoop : IEventLoop
|
||||
{
|
||||
foreach (var action in _permanentQueue)
|
||||
{
|
||||
try
|
||||
{
|
||||
/*try
|
||||
{*/
|
||||
action();
|
||||
}
|
||||
/*}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Fail(e.Message);
|
||||
_logger.LogError(e, "Error while processing action in permanent queue");
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs
Normal file
37
src/Library/TerminalUI/ExpressionTrackers/BinaryTracker.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public class BinaryTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly IExpressionTracker _leftExpressionTracker;
|
||||
private readonly IExpressionTracker _rightExpressionTracker;
|
||||
private readonly Func<object?, object?, object?> _computer;
|
||||
|
||||
public BinaryTracker(
|
||||
BinaryExpression binaryExpression,
|
||||
IExpressionTracker leftExpressionTracker,
|
||||
IExpressionTracker rightExpressionTracker)
|
||||
{
|
||||
_leftExpressionTracker = leftExpressionTracker;
|
||||
_rightExpressionTracker = rightExpressionTracker;
|
||||
ArgumentNullException.ThrowIfNull(leftExpressionTracker);
|
||||
ArgumentNullException.ThrowIfNull(rightExpressionTracker);
|
||||
|
||||
SubscribeToValueChanges = false;
|
||||
SubscribeToTracker(leftExpressionTracker);
|
||||
SubscribeToTracker(rightExpressionTracker);
|
||||
|
||||
_computer = binaryExpression.NodeType switch
|
||||
{
|
||||
ExpressionType.Equal => (v1, v2) => Equals(v1, v2),
|
||||
ExpressionType.NotEqual => (v1, v2) => !Equals(v1, v2),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
protected override object? ComputeValue()
|
||||
=> _computer(_leftExpressionTracker.GetValue(), _rightExpressionTracker.GetValue());
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public class ConditionalTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly IExpressionTracker _testExpressionTracker;
|
||||
private readonly IExpressionTracker _ifTrueExpressionTracker;
|
||||
private readonly IExpressionTracker _ifFalseExpressionTracker;
|
||||
|
||||
public ConditionalTracker(
|
||||
ConditionalExpression conditionalExpression,
|
||||
IExpressionTracker testExpressionTracker,
|
||||
IExpressionTracker ifTrueExpressionTracker,
|
||||
IExpressionTracker ifFalseExpressionTracker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(conditionalExpression);
|
||||
ArgumentNullException.ThrowIfNull(testExpressionTracker);
|
||||
ArgumentNullException.ThrowIfNull(ifTrueExpressionTracker);
|
||||
|
||||
SubscribeToValueChanges = false;
|
||||
|
||||
_testExpressionTracker = testExpressionTracker;
|
||||
_ifTrueExpressionTracker = ifTrueExpressionTracker;
|
||||
_ifFalseExpressionTracker = ifFalseExpressionTracker;
|
||||
|
||||
SubscribeToTracker(testExpressionTracker);
|
||||
SubscribeToTracker(ifTrueExpressionTracker);
|
||||
SubscribeToTracker(ifFalseExpressionTracker);
|
||||
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
protected override object? ComputeValue()
|
||||
{
|
||||
var testValue = _testExpressionTracker.GetValue();
|
||||
return testValue switch
|
||||
{
|
||||
true => _ifTrueExpressionTracker.GetValue(),
|
||||
false => _ifFalseExpressionTracker.GetValue(),
|
||||
_ => throw new NotSupportedException($"Conditional expression must evaluate to a boolean value, but {testValue} ({testValue.GetType().Name}) is not that.")
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs
Normal file
13
src/Library/TerminalUI/ExpressionTrackers/ConstantTracker.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public class ConstantTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly object? _value;
|
||||
|
||||
public ConstantTracker(object? value)
|
||||
{
|
||||
_value = value;
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
protected override object? ComputeValue() => _value;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public class ExpressionParameterTrackerCollection
|
||||
{
|
||||
private readonly Dictionary<string, object?> _values = new();
|
||||
|
||||
public ReadOnlyDictionary<string, object?> Values => new(_values);
|
||||
public event Action<string, object?>? ValueChanged;
|
||||
|
||||
public void SetValue(string name, object? value)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||
|
||||
_values[name] = value;
|
||||
ValueChanged?.Invoke(name, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public abstract class ExpressionTrackerBase : IExpressionTracker
|
||||
{
|
||||
private object? _currentValue;
|
||||
public List<string> TrackedPropertyNames { get; } = new();
|
||||
|
||||
protected bool SubscribeToValueChanges { get; set; } = true;
|
||||
|
||||
public event Action<string>? PropertyChanged;
|
||||
public event Action<bool>? Update;
|
||||
public object? GetValue() => _currentValue;
|
||||
protected abstract object? ComputeValue();
|
||||
|
||||
protected void SubscribeToTracker(IExpressionTracker? expressionTracker)
|
||||
{
|
||||
if (expressionTracker is null) return;
|
||||
expressionTracker.Update += UpdateValueAndChangeTrackers;
|
||||
}
|
||||
|
||||
protected void UpdateValueAndChangeTrackers() => UpdateValueAndChangeTrackers(true);
|
||||
|
||||
private void UpdateValueAndChangeTrackers(bool couldCompute)
|
||||
{
|
||||
if (SubscribeToValueChanges)
|
||||
{
|
||||
if (_currentValue is INotifyPropertyChanged oldNotifyPropertyChanged)
|
||||
oldNotifyPropertyChanged.PropertyChanged -= OnPropertyChanged;
|
||||
if (_currentValue is INotifyCollectionChanged collectionChanged)
|
||||
collectionChanged.CollectionChanged -= OnCollectionChanged;
|
||||
}
|
||||
|
||||
var useNull = false;
|
||||
try
|
||||
{
|
||||
if (couldCompute)
|
||||
{
|
||||
_currentValue = ComputeValue();
|
||||
|
||||
if (SubscribeToValueChanges)
|
||||
{
|
||||
if (_currentValue is INotifyPropertyChanged notifyPropertyChanged)
|
||||
notifyPropertyChanged.PropertyChanged += OnPropertyChanged;
|
||||
if (_currentValue is INotifyCollectionChanged collectionChanged)
|
||||
collectionChanged.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
Update?.Invoke(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
useNull = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
useNull = true;
|
||||
}
|
||||
|
||||
if (useNull)
|
||||
{
|
||||
_currentValue = null;
|
||||
Update?.Invoke(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
=> UpdateValueAndChangeTrackers();
|
||||
|
||||
private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is null) return;
|
||||
|
||||
if (TrackedPropertyNames.Contains(e.PropertyName))
|
||||
{
|
||||
PropertyChanged?.Invoke(e.PropertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public interface IExpressionTracker
|
||||
{
|
||||
List<string> TrackedPropertyNames { get; }
|
||||
event Action<string>? PropertyChanged;
|
||||
event Action<bool>? Update;
|
||||
object? GetValue();
|
||||
}
|
||||
82
src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs
Normal file
82
src/Library/TerminalUI/ExpressionTrackers/MemberTracker.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public sealed class MemberTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly IExpressionTracker? _parentTracker;
|
||||
private readonly string _memberName;
|
||||
private readonly Func<object?> _valueProvider;
|
||||
|
||||
public MemberTracker(MemberExpression memberExpression, IExpressionTracker? parentTracker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(memberExpression);
|
||||
_parentTracker = parentTracker;
|
||||
|
||||
if (parentTracker is not null)
|
||||
{
|
||||
parentTracker.PropertyChanged += propertyName =>
|
||||
{
|
||||
if (propertyName == _memberName)
|
||||
{
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (memberExpression.Member is PropertyInfo propertyInfo)
|
||||
{
|
||||
_memberName = propertyInfo.Name;
|
||||
parentTracker?.TrackedPropertyNames.Add(propertyInfo.Name);
|
||||
|
||||
if (propertyInfo.GetMethod is { } getMethod)
|
||||
{
|
||||
_valueProvider = () => CallPropertyInfo(propertyInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Try to get value of a property without a getter: {propertyInfo.Name} in {propertyInfo.DeclaringType?.Name ?? "<null>"}."
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (memberExpression.Member is FieldInfo fieldInfo)
|
||||
{
|
||||
_memberName = fieldInfo.Name;
|
||||
parentTracker?.TrackedPropertyNames.Add(fieldInfo.Name);
|
||||
|
||||
_valueProvider = () => CallFieldInfo(fieldInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Could not determine source type of expression {memberExpression} with parent {parentTracker}");
|
||||
}
|
||||
|
||||
SubscribeToTracker(parentTracker);
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
private object? CallPropertyInfo(PropertyInfo propertyInfo)
|
||||
{
|
||||
var obj = _parentTracker?.GetValue();
|
||||
if (obj is null && !propertyInfo.GetMethod!.IsStatic) return null;
|
||||
|
||||
return propertyInfo.GetValue(_parentTracker?.GetValue());
|
||||
}
|
||||
|
||||
private object? CallFieldInfo(FieldInfo fieldInfo)
|
||||
{
|
||||
var obj = _parentTracker?.GetValue();
|
||||
if (obj is null && !fieldInfo.IsStatic) return null;
|
||||
|
||||
return fieldInfo.GetValue(_parentTracker?.GetValue());
|
||||
}
|
||||
|
||||
protected override object? ComputeValue()
|
||||
{
|
||||
var v = _valueProvider();
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public sealed class MethodCallTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly MethodInfo _method;
|
||||
private readonly IExpressionTracker? _objectTracker;
|
||||
private readonly List<IExpressionTracker> _argumentTrackers;
|
||||
|
||||
public MethodCallTracker(
|
||||
MethodCallExpression methodCallExpression,
|
||||
IExpressionTracker? objectTracker,
|
||||
IEnumerable<IExpressionTracker> argumentTrackers)
|
||||
{
|
||||
_method = methodCallExpression.Method;
|
||||
_objectTracker = objectTracker;
|
||||
_argumentTrackers = argumentTrackers.ToList();
|
||||
|
||||
if (objectTracker is not null)
|
||||
{
|
||||
SubscribeToTracker(objectTracker);
|
||||
}
|
||||
|
||||
foreach (var expressionTracker in _argumentTrackers)
|
||||
{
|
||||
SubscribeToTracker(expressionTracker);
|
||||
}
|
||||
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
protected override object? ComputeValue()
|
||||
{
|
||||
var obj = _objectTracker?.GetValue();
|
||||
if (obj is null && !_method.IsStatic) return null;
|
||||
return _method.Invoke(obj, _argumentTrackers.Select(t => t.GetValue()).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public sealed class ParameterTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly ExpressionParameterTrackerCollection _trackerCollection;
|
||||
private readonly string _parameterName;
|
||||
|
||||
public ParameterTracker(
|
||||
ParameterExpression parameterExpression,
|
||||
ExpressionParameterTrackerCollection trackerCollection,
|
||||
string parameterName)
|
||||
{
|
||||
_trackerCollection = trackerCollection;
|
||||
_parameterName = parameterName;
|
||||
|
||||
trackerCollection.ValueChanged += TrackerCollectionOnValueChanged;
|
||||
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
private void TrackerCollectionOnValueChanged(string parameterName, object? newValue)
|
||||
=> UpdateValueAndChangeTrackers();
|
||||
|
||||
protected override object? ComputeValue()
|
||||
{
|
||||
_trackerCollection.Values.TryGetValue(_parameterName, out var v);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
46
src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs
Normal file
46
src/Library/TerminalUI/ExpressionTrackers/UnaryTracker.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace TerminalUI.ExpressionTrackers;
|
||||
|
||||
public class UnaryTracker : ExpressionTrackerBase
|
||||
{
|
||||
private readonly IExpressionTracker _operandTracker;
|
||||
private readonly Func<object?, object?> _operator;
|
||||
|
||||
public UnaryTracker(UnaryExpression unaryExpression, IExpressionTracker operandTracker)
|
||||
{
|
||||
_operandTracker = operandTracker;
|
||||
ArgumentNullException.ThrowIfNull(unaryExpression);
|
||||
ArgumentNullException.ThrowIfNull(operandTracker);
|
||||
|
||||
SubscribeToValueChanges = false;
|
||||
|
||||
_operator = unaryExpression.NodeType switch
|
||||
{
|
||||
ExpressionType.Negate => Negate,
|
||||
ExpressionType.Not => o => o is bool b ? !b : null,
|
||||
ExpressionType.Convert => o => o,
|
||||
_ => throw new NotSupportedException($"Unary expression of type {unaryExpression.NodeType} is not supported.")
|
||||
};
|
||||
|
||||
SubscribeToTracker(operandTracker);
|
||||
|
||||
UpdateValueAndChangeTrackers();
|
||||
}
|
||||
|
||||
private static object? Negate(object? source)
|
||||
{
|
||||
if (source is null) return null;
|
||||
return source switch
|
||||
{
|
||||
int i => -i,
|
||||
long l => -l,
|
||||
float f => -f,
|
||||
double d => -d,
|
||||
decimal d => -d,
|
||||
_ => throw new NotSupportedException($"Unary negation is not supported for type {source.GetType().Name}.")
|
||||
};
|
||||
}
|
||||
|
||||
protected override object? ComputeValue() => _operator(_operandTracker.GetValue());
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using TerminalUI.Controls;
|
||||
|
||||
namespace TerminalUI.Extensions;
|
||||
|
||||
public static class Binding
|
||||
public static class BindingExtensions
|
||||
{
|
||||
public static Binding<TDataContext, TResult, TResult> Bind<TView, TDataContext, TResult>(
|
||||
this TView targetView,
|
||||
@@ -28,7 +28,7 @@ public static class ViewExtensions
|
||||
public static TItem WithPropertyChangedHandler<TItem, TExpressionResult>(
|
||||
this TItem dataSource,
|
||||
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
|
||||
Action<string, bool, TExpressionResult> handler)
|
||||
Action<bool, TExpressionResult> handler)
|
||||
{
|
||||
new PropertyChangedHandler<TItem, TExpressionResult>
|
||||
(
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public interface IPropertyChangeTracker : IDisposable
|
||||
{
|
||||
string Name { get; }
|
||||
string Path { get; }
|
||||
Dictionary<string, IPropertyChangeTracker> Children { get; }
|
||||
}
|
||||
|
||||
public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Path { get; }
|
||||
public Dictionary<string, IPropertyChangeTracker> Children { get; } = new();
|
||||
|
||||
protected PropertyChangeTrackerBase(string name, string path)
|
||||
{
|
||||
Name = name;
|
||||
Path = path;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
foreach (var propertyChangeTracker in Children.Values)
|
||||
{
|
||||
propertyChangeTracker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PropertyChangeTracker : PropertyChangeTrackerBase
|
||||
{
|
||||
private readonly PropertyTrackTreeItem _propertyTrackTreeItem;
|
||||
private readonly INotifyPropertyChanged _target;
|
||||
private readonly IEnumerable<string> _propertiesToListen;
|
||||
private readonly Action<string> _updateBinding;
|
||||
|
||||
public PropertyChangeTracker(
|
||||
string name,
|
||||
string path,
|
||||
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||
INotifyPropertyChanged target,
|
||||
IEnumerable<string> propertiesToListen,
|
||||
Action<string> updateBinding) : base(name, path)
|
||||
{
|
||||
_propertyTrackTreeItem = propertyTrackTreeItem;
|
||||
_target = target;
|
||||
_propertiesToListen = propertiesToListen;
|
||||
_updateBinding = updateBinding;
|
||||
target.PropertyChanged += Target_PropertyChanged;
|
||||
}
|
||||
|
||||
private void Target_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
var propertyName = e.PropertyName;
|
||||
if (propertyName is null || !_propertiesToListen.Contains(propertyName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Children.Remove(propertyName);
|
||||
|
||||
var newChild = PropertyChangeHelper.CreatePropertyTracker(
|
||||
Path,
|
||||
_propertyTrackTreeItem.Children[propertyName],
|
||||
_target.GetType().GetProperty(propertyName)?.GetValue(_target),
|
||||
_updateBinding
|
||||
);
|
||||
|
||||
if (newChild is not null)
|
||||
{
|
||||
Children.Add(propertyName, newChild);
|
||||
}
|
||||
|
||||
_updateBinding(propertyName);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_target.PropertyChanged -= Target_PropertyChanged;
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase
|
||||
{
|
||||
public NonSubscriberPropertyChangeTracker(string name, string path) : base(name, path)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class PropertyTrackTreeItem
|
||||
{
|
||||
public string Name { get; }
|
||||
public Dictionary<string, PropertyTrackTreeItem> Children { get; } = new();
|
||||
|
||||
public PropertyTrackTreeItem(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PropertyChangeHelper
|
||||
{
|
||||
internal static IPropertyChangeTracker? CreatePropertyTracker(
|
||||
string? path,
|
||||
PropertyTrackTreeItem propertyTrackTreeItem,
|
||||
object? obj,
|
||||
Action<string> updateBinding
|
||||
)
|
||||
{
|
||||
if (obj is null) return null;
|
||||
|
||||
path = path is null ? propertyTrackTreeItem.Name : path + "." + propertyTrackTreeItem.Name;
|
||||
|
||||
IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged
|
||||
? new PropertyChangeTracker(
|
||||
propertyTrackTreeItem.Name,
|
||||
path,
|
||||
propertyTrackTreeItem,
|
||||
notifyPropertyChanged,
|
||||
propertyTrackTreeItem.Children.Keys,
|
||||
updateBinding
|
||||
)
|
||||
: new NonSubscriberPropertyChangeTracker(
|
||||
propertyTrackTreeItem.Name,
|
||||
path);
|
||||
|
||||
foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children)
|
||||
{
|
||||
var childTracker = CreatePropertyTracker(
|
||||
path,
|
||||
trackerTreeItem,
|
||||
obj.GetType().GetProperty(propertyName)?.GetValue(obj),
|
||||
updateBinding
|
||||
);
|
||||
|
||||
if (childTracker is not null)
|
||||
{
|
||||
tracker.Children.Add(propertyName, childTracker);
|
||||
}
|
||||
}
|
||||
|
||||
return tracker;
|
||||
}
|
||||
}
|
||||
@@ -2,44 +2,48 @@
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>, IDisposable
|
||||
public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>
|
||||
{
|
||||
private readonly TItem _dataSource;
|
||||
private readonly Action<string, bool, TExpressionResult?> _handler;
|
||||
private readonly PropertyTrackTreeItem? _propertyTrackTreeItem;
|
||||
private readonly Action<bool, TExpressionResult?> _handler;
|
||||
private readonly Func<TItem, TExpressionResult> _propertyValueGenerator;
|
||||
|
||||
public PropertyChangedHandler(
|
||||
TItem dataSource,
|
||||
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
|
||||
Action<string, bool, TExpressionResult?> handler
|
||||
) : base(() => dataSource, dataSourceExpression)
|
||||
Action<bool, TExpressionResult?> handler
|
||||
) : base(dataSourceExpression!)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_handler = handler;
|
||||
ArgumentNullException.ThrowIfNull(dataSource);
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
_dataSource = dataSource;
|
||||
_handler = handler;
|
||||
|
||||
Parameters.SetValue(dataSourceExpression.Parameters[0].Name!, dataSource);
|
||||
|
||||
_propertyTrackTreeItem = CreateTrackingTree(dataSourceExpression);
|
||||
_propertyValueGenerator = dataSourceExpression.Compile();
|
||||
UpdateTrackers();
|
||||
Update(true);
|
||||
}
|
||||
|
||||
protected override void Update(string propertyPath)
|
||||
protected override void Update(bool couldCompute)
|
||||
{
|
||||
TExpressionResult? value = default;
|
||||
var parsed = false;
|
||||
|
||||
try
|
||||
{
|
||||
value = _propertyValueGenerator(_dataSource);
|
||||
parsed = true;
|
||||
if (couldCompute)
|
||||
{
|
||||
value = _propertyValueGenerator(_dataSource);
|
||||
parsed = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
_handler(propertyPath, parsed, value);
|
||||
_handler(parsed, value);
|
||||
}
|
||||
}
|
||||
@@ -1,145 +1,92 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using TerminalUI.ExpressionTrackers;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public abstract class PropertyTrackerBase<TSource, TExpressionResult> : IDisposable
|
||||
public abstract class PropertyTrackerBase<TSource, TExpressionResult>
|
||||
{
|
||||
private readonly Func<TSource?> _source;
|
||||
protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; }
|
||||
protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; }
|
||||
private readonly IExpressionTracker _tracker;
|
||||
protected ExpressionParameterTrackerCollection Parameters { get; } = new();
|
||||
|
||||
protected PropertyTrackerBase(
|
||||
Func<TSource?> source,
|
||||
Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
|
||||
protected PropertyTrackerBase(Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataSourceExpression);
|
||||
|
||||
_source = source;
|
||||
PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression);
|
||||
_tracker = FindReactiveProperties(dataSourceExpression.Body, Parameters);
|
||||
_tracker.Update += Update;
|
||||
}
|
||||
|
||||
protected PropertyTrackTreeItem? CreateTrackingTree(Expression<Func<TSource?, TExpressionResult>> dataContextExpression)
|
||||
private IExpressionTracker FindReactiveProperties(Expression? expression, ExpressionParameterTrackerCollection parameters)
|
||||
{
|
||||
var properties = new List<string>();
|
||||
FindReactiveProperties(dataContextExpression, properties);
|
||||
|
||||
if (properties.Count > 0)
|
||||
if (expression is ConditionalExpression conditionalExpression)
|
||||
{
|
||||
var rootItem = new PropertyTrackTreeItem(null!);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var pathParts = property.Split('.');
|
||||
var currentItem = rootItem;
|
||||
for (var i = 0; i < pathParts.Length; i++)
|
||||
{
|
||||
if (!currentItem.Children.TryGetValue(pathParts[i], out var child))
|
||||
{
|
||||
child = new PropertyTrackTreeItem(pathParts[i]);
|
||||
currentItem.Children.Add(pathParts[i], child);
|
||||
}
|
||||
var testTracker = FindReactiveProperties(conditionalExpression.Test, parameters);
|
||||
var trueTracker = FindReactiveProperties(conditionalExpression.IfTrue, parameters);
|
||||
var falseTracker = FindReactiveProperties(conditionalExpression.IfFalse, parameters);
|
||||
|
||||
currentItem = child;
|
||||
}
|
||||
}
|
||||
|
||||
return rootItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? FindReactiveProperties(Expression? expression, List<string> properties)
|
||||
{
|
||||
if (expression is null) return "";
|
||||
|
||||
if (expression is LambdaExpression lambdaExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties));
|
||||
}
|
||||
else if (expression is ConditionalExpression conditionalExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties));
|
||||
SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties));
|
||||
return new ConditionalTracker(
|
||||
conditionalExpression,
|
||||
testTracker,
|
||||
trueTracker,
|
||||
falseTracker);
|
||||
}
|
||||
else if (expression is MemberExpression memberExpression)
|
||||
{
|
||||
IExpressionTracker? parentExpressionTracker = null;
|
||||
if (memberExpression.Expression is not null)
|
||||
{
|
||||
FindReactiveProperties(memberExpression.Expression, properties);
|
||||
|
||||
if (FindReactiveProperties(memberExpression.Expression, properties) is { } path
|
||||
&& memberExpression.Member is PropertyInfo dataContextPropertyInfo)
|
||||
{
|
||||
path += "." + memberExpression.Member.Name;
|
||||
return path;
|
||||
}
|
||||
parentExpressionTracker = FindReactiveProperties(memberExpression.Expression, parameters);
|
||||
}
|
||||
|
||||
return new MemberTracker(memberExpression, parentExpressionTracker);
|
||||
}
|
||||
else if (expression is MethodCallExpression methodCallExpression)
|
||||
{
|
||||
if (methodCallExpression.Object is
|
||||
{
|
||||
NodeType:
|
||||
not ExpressionType.Parameter
|
||||
and not ExpressionType.Constant
|
||||
} methodObject)
|
||||
IExpressionTracker? objectTracker = null;
|
||||
if (methodCallExpression.Object is { } methodObject)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(methodObject, properties));
|
||||
objectTracker = FindReactiveProperties(methodObject, parameters);
|
||||
}
|
||||
|
||||
var argumentTrackers = new List<IExpressionTracker>(methodCallExpression.Arguments.Count);
|
||||
foreach (var argument in methodCallExpression.Arguments)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(argument, properties));
|
||||
var argumentTracker = FindReactiveProperties(argument, parameters);
|
||||
argumentTrackers.Add(argumentTracker);
|
||||
}
|
||||
|
||||
return new MethodCallTracker(methodCallExpression, objectTracker, argumentTrackers);
|
||||
}
|
||||
else if (expression is BinaryExpression binaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties));
|
||||
SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties));
|
||||
var leftTracker = FindReactiveProperties(binaryExpression.Left, parameters);
|
||||
var rightTracker = FindReactiveProperties(binaryExpression.Right, parameters);
|
||||
|
||||
return new BinaryTracker(binaryExpression, leftTracker, rightTracker);
|
||||
}
|
||||
else if (expression is UnaryExpression unaryExpression)
|
||||
{
|
||||
return FindReactiveProperties(unaryExpression.Operand, properties);
|
||||
var operandTracker = FindReactiveProperties(unaryExpression.Operand, parameters);
|
||||
return new UnaryTracker(unaryExpression, operandTracker);
|
||||
}
|
||||
else if (expression is ParameterExpression parameterExpression)
|
||||
{
|
||||
if (parameterExpression.Type == typeof(TSource))
|
||||
if (parameterExpression.Name is { } name)
|
||||
{
|
||||
return "";
|
||||
return new ParameterTracker(parameterExpression, parameters, name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
void SavePropertyPath(string? path)
|
||||
else if (expression is ConstantExpression constantExpression)
|
||||
{
|
||||
if (path is null) return;
|
||||
path = path.TrimStart('.');
|
||||
properties.Add(path);
|
||||
return new ConstantTracker(constantExpression.Value);
|
||||
}
|
||||
/*else if (expression is not ConstantExpression)
|
||||
{
|
||||
Debug.Assert(false, "Unknown expression type " + expression.GetType());
|
||||
}*/
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
protected void UpdateTrackers()
|
||||
{
|
||||
if (PropertyChangeTracker is not null)
|
||||
{
|
||||
PropertyChangeTracker.Dispose();
|
||||
}
|
||||
|
||||
if (PropertyTrackTreeItem is not null)
|
||||
{
|
||||
PropertyChangeTracker = PropertyChangeHelper.CreatePropertyTracker(
|
||||
null,
|
||||
PropertyTrackTreeItem,
|
||||
_source(),
|
||||
Update
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void Update(string propertyPath);
|
||||
|
||||
public virtual void Dispose() => PropertyChangeTracker?.Dispose();
|
||||
protected abstract void Update(bool couldCompute);
|
||||
}
|
||||
Reference in New Issue
Block a user