New reactive core WIP
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public class CollectionRepeaterProperty<TCollection, TItem> : DeclarativePropertyBase<TCollection>
|
||||
where TCollection : IList<TItem>, INotifyCollectionChanged
|
||||
{
|
||||
private TCollection? _currentCollection;
|
||||
|
||||
public CollectionRepeaterProperty(IDeclarativeProperty<TCollection?> from)
|
||||
{
|
||||
_currentCollection = from.Value;
|
||||
if (from.Value is { } value)
|
||||
{
|
||||
value.CollectionChanged += HandleCollectionChanged;
|
||||
}
|
||||
AddDisposable(from.Subscribe(Handle));
|
||||
}
|
||||
|
||||
private void HandleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
var t = Task.Run(async () => await NotifySubscribersAsync(Value));
|
||||
t.Wait();
|
||||
}
|
||||
|
||||
private async Task Handle(TCollection? collection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_currentCollection is { } currentCollection)
|
||||
{
|
||||
currentCollection.CollectionChanged -= HandleCollectionChanged;
|
||||
}
|
||||
if (collection is {} newCollection)
|
||||
{
|
||||
//newCollection.CollectionChanged -= HandleCollectionChanged;
|
||||
newCollection.CollectionChanged += HandleCollectionChanged;
|
||||
}
|
||||
|
||||
_currentCollection = collection;
|
||||
|
||||
await SetNewValueAsync(collection, cancellationToken);
|
||||
}
|
||||
}
|
||||
36
src/Library/DeclarativeProperty/CombineLatestProperty.cs
Normal file
36
src/Library/DeclarativeProperty/CombineLatestProperty.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class CombineLatestProperty<T1, T2, TResult> : DeclarativePropertyBase<TResult>
|
||||
{
|
||||
private readonly Func<T1?, T2?, Task<TResult>> _func;
|
||||
private T1? _value1;
|
||||
private T2? _value2;
|
||||
|
||||
public CombineLatestProperty(IDeclarativeProperty<T1> prop1, IDeclarativeProperty<T2> prop2, Func<T1?, T2?, Task<TResult>> func)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prop1);
|
||||
ArgumentNullException.ThrowIfNull(prop2);
|
||||
|
||||
_func = func;
|
||||
|
||||
_value1 = prop1.Value is null ? default : prop1.Value;
|
||||
_value2 = prop2.Value is null ? default : prop2.Value;
|
||||
|
||||
prop1.Subscribe(async (value1, token) =>
|
||||
{
|
||||
_value1 = value1;
|
||||
await UpdateAsync(token);
|
||||
});
|
||||
prop2.Subscribe(async (value2, token) =>
|
||||
{
|
||||
_value2 = value2;
|
||||
await UpdateAsync(token);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task UpdateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _func(_value1, _value2);
|
||||
await SetNewValueAsync(result, cancellationToken);
|
||||
}
|
||||
}
|
||||
52
src/Library/DeclarativeProperty/DebounceProperty.cs
Normal file
52
src/Library/DeclarativeProperty/DebounceProperty.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class DebounceProperty<T> : TimingPropertyBase<T>
|
||||
{
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
private bool _isActive;
|
||||
private DateTime _startTime;
|
||||
public bool ResetTimer { get; init; }
|
||||
public TimeSpan WaitInterval { get; init; } = TimeSpan.FromMilliseconds(1);
|
||||
|
||||
public DebounceProperty(
|
||||
IDeclarativeProperty<T> from,
|
||||
TimeSpan interval,
|
||||
Action<T?>? setValueHook = null) : base(from, interval, setValueHook)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_debounceCts?.Cancel();
|
||||
var newTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_debounceCts = newTokenSource;
|
||||
|
||||
var newToken = newTokenSource.Token;
|
||||
|
||||
if (!_isActive || ResetTimer)
|
||||
{
|
||||
_isActive = true;
|
||||
_startTime = DateTime.Now;
|
||||
}
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (DateTime.Now - _startTime < Interval)
|
||||
{
|
||||
await Task.Delay(WaitInterval, newToken);
|
||||
}
|
||||
|
||||
WithLock(() => { _isActive = false; });
|
||||
|
||||
await FireAsync(next, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
25
src/Library/DeclarativeProperty/DeclarativeProperty.cs
Normal file
25
src/Library/DeclarativeProperty/DeclarativeProperty.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public static class DeclarativePropertyHelpers
|
||||
{
|
||||
public static CombineLatestProperty<T1, T2, TResult> CombineLatest<T1, T2, TResult>(
|
||||
IDeclarativeProperty<T1> prop1,
|
||||
IDeclarativeProperty<T2> prop2,
|
||||
Func<T1, T2, Task<TResult>> func)
|
||||
=> new(prop1, prop2, func);
|
||||
}
|
||||
|
||||
public sealed class DeclarativeProperty<T> : DeclarativePropertyBase<T>
|
||||
{
|
||||
public DeclarativeProperty(Action<T?>? setValueHook = null) : base(setValueHook)
|
||||
{
|
||||
}
|
||||
|
||||
public DeclarativeProperty(T initialValue, Action<T?>? setValueHook = null) : base(initialValue, setValueHook)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task SetValue(T newValue, CancellationToken cancellationToken = default)
|
||||
=> await SetNewValueAsync(newValue, cancellationToken);
|
||||
}
|
||||
13
src/Library/DeclarativeProperty/DeclarativeProperty.csproj
Normal file
13
src/Library/DeclarativeProperty/DeclarativeProperty.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ObservableComputations" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
143
src/Library/DeclarativeProperty/DeclarativePropertyBase.cs
Normal file
143
src/Library/DeclarativeProperty/DeclarativePropertyBase.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public abstract class DeclarativePropertyBase<T> : IDeclarativeProperty<T>
|
||||
{
|
||||
private readonly List<Func<T?, CancellationToken, Task>> _subscribers = new();
|
||||
private readonly Action<T?>? _setValueHook;
|
||||
private readonly List<IDisposable> _disposables = new();
|
||||
private readonly List<Func<IDeclarativeProperty<T>, T, IDisposable?>> _subscribeTriggers = new();
|
||||
private readonly List<Action<IDeclarativeProperty<T>, T>> _unsubscribeTriggers = new();
|
||||
private readonly List<IDisposable> _triggerDisposables = new();
|
||||
private readonly object _triggerLock = new();
|
||||
|
||||
private T? _value;
|
||||
|
||||
public T? Value
|
||||
{
|
||||
get => _value;
|
||||
set => _setValueHook?.Invoke(value);
|
||||
}
|
||||
|
||||
protected DeclarativePropertyBase(Action<T?>? setValueHook = null)
|
||||
{
|
||||
_setValueHook = setValueHook;
|
||||
}
|
||||
|
||||
protected DeclarativePropertyBase(T? initialValue, Action<T?>? setValueHook = null)
|
||||
{
|
||||
_setValueHook = setValueHook;
|
||||
_value = initialValue;
|
||||
}
|
||||
|
||||
|
||||
protected async Task NotifySubscribersAsync(T? newValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var subscribers = _subscribers.ToList();
|
||||
foreach (var handler in subscribers)
|
||||
{
|
||||
await handler(newValue, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
public IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange)
|
||||
{
|
||||
_subscribers.Add(onChange);
|
||||
onChange(_value, default);
|
||||
|
||||
return new Unsubscriber<T>(this, onChange);
|
||||
}
|
||||
|
||||
public void Unsubscribe(Func<T, CancellationToken, Task> onChange) => _subscribers.Remove(onChange);
|
||||
|
||||
public IDeclarativeProperty<T> RegisterTrigger(
|
||||
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
||||
Action<IDeclarativeProperty<T>, T>? triggerUnsubscribe = null)
|
||||
{
|
||||
lock (_triggerLock)
|
||||
{
|
||||
if (Value != null)
|
||||
{
|
||||
var disposable = triggerSubscribe(this, Value);
|
||||
if (disposable != null) _triggerDisposables.Add(disposable);
|
||||
}
|
||||
|
||||
_subscribeTriggers.Add(triggerSubscribe);
|
||||
if (triggerUnsubscribe != null) _unsubscribeTriggers.Add(triggerUnsubscribe);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable IObservable<T>.Subscribe(IObserver<T> observer)
|
||||
=> Subscribe((v, _) =>
|
||||
{
|
||||
observer.OnNext(v);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
protected async Task SetNewValueAsync(T? newValue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!(Value?.Equals(newValue) ?? false))
|
||||
{
|
||||
lock (_triggerLock)
|
||||
{
|
||||
if (_value != null)
|
||||
{
|
||||
foreach (var unsubscribeTrigger in _unsubscribeTriggers)
|
||||
{
|
||||
unsubscribeTrigger(this, _value);
|
||||
}
|
||||
|
||||
foreach (var triggerDisposable in _triggerDisposables)
|
||||
{
|
||||
triggerDisposable.Dispose();
|
||||
}
|
||||
|
||||
_triggerDisposables.Clear();
|
||||
|
||||
if(cancellationToken.IsCancellationRequested) return;
|
||||
}
|
||||
|
||||
_value = newValue;
|
||||
|
||||
if (_value != null)
|
||||
{
|
||||
foreach (var subscribeTrigger in _subscribeTriggers)
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested) return;
|
||||
|
||||
var disposable = subscribeTrigger(this, _value);
|
||||
if (disposable != null) _triggerDisposables.Add(disposable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Value));
|
||||
}
|
||||
|
||||
await NotifySubscribersAsync(newValue, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReFireAsync()
|
||||
=> await SetNewValueAsync(Value);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var disposable in _disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_subscribers.Clear();
|
||||
}
|
||||
|
||||
protected void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using ObservableComputations;
|
||||
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public static class DeclarativePropertyExtensions
|
||||
{
|
||||
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, TimeSpan interval, bool resetTimer = false)
|
||||
=> new DebounceProperty<T>(from, interval){ResetTimer = resetTimer};
|
||||
|
||||
public static IDeclarativeProperty<T> DistinctUntilChanged<T>(this IDeclarativeProperty<T> from)
|
||||
=> new DistinctUntilChangedProperty<T>(from);
|
||||
|
||||
public static IDeclarativeProperty<TTo?> Map<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, CancellationToken, Task<TTo?>> mapper)
|
||||
=> new MapProperty<TFrom?, TTo?>(mapper, from);
|
||||
|
||||
public static IDeclarativeProperty<TTo?> Map<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, TTo?> mapper)
|
||||
=> new MapProperty<TFrom?, TTo?>((next, _) => Task.FromResult(mapper(next)), from);
|
||||
|
||||
public static async Task<IDeclarativeProperty<TTo?>> MapAsync<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, CancellationToken, Task<TTo?>> mapper)
|
||||
=> await MapProperty<TFrom?, TTo?>.CreateAsync(mapper, from);
|
||||
|
||||
public static async Task<IDeclarativeProperty<TTo?>> MapAsync<TFrom, TTo>(this IDeclarativeProperty<TFrom?> from, Func<TFrom?, TTo?> mapper)
|
||||
=> await MapProperty<TFrom?, TTo?>.CreateAsync((next, _) => Task.FromResult(mapper(next)), from);
|
||||
|
||||
public static IDisposable Subscribe<T>(this IDeclarativeProperty<T> property, Action<T?, CancellationToken> onChange)
|
||||
=> property.Subscribe((value, token) =>
|
||||
{
|
||||
onChange(value, token);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
public static IDisposable Subscribe<T>(this IDeclarativeProperty<T> property, Action<T?> onChange)
|
||||
=> property.Subscribe((value, _) =>
|
||||
{
|
||||
onChange(value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
public static IDeclarativeProperty<T> Extract<T>(
|
||||
this IDeclarativeProperty<ReadOnlyObservableCollection<T>> from,
|
||||
Func<IList<T>?, T?> extractor
|
||||
)
|
||||
=> new ExtractorProperty<T>(from, extractor);
|
||||
|
||||
public static IDeclarativeProperty<T> Extract<T>(
|
||||
this IDeclarativeProperty<ObservableCollection<T>> from,
|
||||
Func<IList<T>?, T?> extractor
|
||||
)
|
||||
=> new ExtractorProperty<T>(from, extractor);
|
||||
|
||||
public static IDeclarativeProperty<T> Extract<T>(
|
||||
this IDeclarativeProperty<CollectionComputing<T>> from,
|
||||
Func<IList<T>?, T?> extractor
|
||||
)
|
||||
=> new ExtractorProperty<T>(from, extractor);
|
||||
|
||||
public static IDeclarativeProperty<TCollection?> Watch<TCollection, TItem>(
|
||||
this IDeclarativeProperty<TCollection?> collection)
|
||||
where TCollection : IList<TItem>, INotifyCollectionChanged
|
||||
=> new CollectionRepeaterProperty<TCollection?, TItem>(collection);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class DistinctUntilChangedProperty<T> : DeclarativePropertyBase<T>
|
||||
{
|
||||
private readonly Func<T?, T?, bool>? _comparer;
|
||||
private bool _firstFire = true;
|
||||
|
||||
public DistinctUntilChangedProperty(IDeclarativeProperty<T> from, Func<T?, T?, bool>? comparer = null) : base(from.Value)
|
||||
{
|
||||
_comparer = comparer;
|
||||
AddDisposable(from.Subscribe(Handle));
|
||||
}
|
||||
|
||||
async Task Handle(T? next, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_comparer is { } comparer)
|
||||
{
|
||||
if (comparer(Value, next))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if ((next is null && Value is null && !_firstFire)
|
||||
|| (Value?.Equals(next) ?? false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_firstFire = false;
|
||||
await SetNewValueAsync(next, cancellationToken);
|
||||
}
|
||||
}
|
||||
174
src/Library/DeclarativeProperty/ExtractorProperty.cs
Normal file
174
src/Library/DeclarativeProperty/ExtractorProperty.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using ObservableComputations;
|
||||
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class ExtractorProperty<T> : DeclarativePropertyBase<T>
|
||||
{
|
||||
private interface ICollectionWrapper : IDisposable
|
||||
{
|
||||
IList<T> Collection { get; }
|
||||
}
|
||||
|
||||
private interface ICollectionWrapper<in TCollection> : ICollectionWrapper
|
||||
where TCollection : IList<T>, INotifyCollectionChanged
|
||||
{
|
||||
static abstract ICollectionWrapper<TCollection> Create(
|
||||
TCollection collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
);
|
||||
}
|
||||
|
||||
private class ObservableCollectionWrapper : ICollectionWrapper<ObservableCollection<T>>
|
||||
{
|
||||
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||
private readonly ObservableCollection<T> _collection;
|
||||
|
||||
public ObservableCollectionWrapper(
|
||||
ObservableCollection<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
)
|
||||
{
|
||||
_collection = collection;
|
||||
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||
|
||||
_collection.CollectionChanged += _handler;
|
||||
}
|
||||
|
||||
public IList<T> Collection => _collection;
|
||||
|
||||
public void Dispose() => _collection.CollectionChanged -= _handler;
|
||||
|
||||
|
||||
public static ICollectionWrapper<ObservableCollection<T>> Create(
|
||||
ObservableCollection<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
) => new ObservableCollectionWrapper(collection, handler);
|
||||
}
|
||||
|
||||
private class ReadOnlyObservableCollectionWrapper : ICollectionWrapper<ReadOnlyObservableCollection<T>>
|
||||
{
|
||||
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||
private readonly ReadOnlyObservableCollection<T> _collection;
|
||||
|
||||
public ReadOnlyObservableCollectionWrapper(
|
||||
ReadOnlyObservableCollection<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
)
|
||||
{
|
||||
_collection = collection;
|
||||
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||
|
||||
((INotifyCollectionChanged) _collection).CollectionChanged += _handler;
|
||||
}
|
||||
|
||||
public IList<T> Collection => _collection;
|
||||
|
||||
public void Dispose() => ((INotifyCollectionChanged) _collection).CollectionChanged -= _handler;
|
||||
|
||||
|
||||
public static ICollectionWrapper<ReadOnlyObservableCollection<T>> Create(
|
||||
ReadOnlyObservableCollection<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
) => new ReadOnlyObservableCollectionWrapper(collection, handler);
|
||||
}
|
||||
|
||||
private class CollectionComputingWrapper : ICollectionWrapper<CollectionComputing<T>>
|
||||
{
|
||||
private readonly OcConsumer _consumer;
|
||||
private readonly NotifyCollectionChangedEventHandler _handler;
|
||||
private readonly CollectionComputing<T> _collection;
|
||||
|
||||
public CollectionComputingWrapper(
|
||||
CollectionComputing<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
)
|
||||
{
|
||||
_collection = collection;
|
||||
_handler = new NotifyCollectionChangedEventHandler(handler);
|
||||
|
||||
((INotifyCollectionChanged) _collection).CollectionChanged += _handler;
|
||||
|
||||
collection.For(_consumer = new OcConsumer());
|
||||
}
|
||||
|
||||
public IList<T> Collection => _collection;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
((INotifyCollectionChanged) _collection).CollectionChanged -= _handler;
|
||||
_consumer.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public static ICollectionWrapper<CollectionComputing<T>> Create(
|
||||
CollectionComputing<T> collection,
|
||||
Action<object?, NotifyCollectionChangedEventArgs> handler
|
||||
) => new CollectionComputingWrapper(collection, handler);
|
||||
}
|
||||
|
||||
private readonly Func<IList<T>?, T?> _extractor;
|
||||
private ICollectionWrapper? _collectionWrapper;
|
||||
|
||||
public ExtractorProperty(
|
||||
IDeclarativeProperty<ObservableCollection<T>> from,
|
||||
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||
{
|
||||
_extractor = extractor;
|
||||
_collectionWrapper = from.Value is null
|
||||
? null
|
||||
: new ObservableCollectionWrapper(from.Value, CollectionUpdated);
|
||||
|
||||
AddDisposable(from.Subscribe(SetValue<ObservableCollectionWrapper, ObservableCollection<T>>));
|
||||
}
|
||||
|
||||
public ExtractorProperty(
|
||||
IDeclarativeProperty<ReadOnlyObservableCollection<T>> from,
|
||||
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||
{
|
||||
_extractor = extractor;
|
||||
_collectionWrapper = from.Value is null
|
||||
? null
|
||||
: new ReadOnlyObservableCollectionWrapper(from.Value, CollectionUpdated);
|
||||
|
||||
AddDisposable(from.Subscribe(SetValue<ReadOnlyObservableCollectionWrapper, ReadOnlyObservableCollection<T>>));
|
||||
}
|
||||
|
||||
public ExtractorProperty(
|
||||
IDeclarativeProperty<CollectionComputing<T>> from,
|
||||
Func<IList<T>?, T?> extractor) : base(extractor(from.Value))
|
||||
{
|
||||
_extractor = extractor;
|
||||
_collectionWrapper = from.Value is null
|
||||
? null
|
||||
: new CollectionComputingWrapper(from.Value, CollectionUpdated);
|
||||
|
||||
AddDisposable(from.Subscribe(SetValue<CollectionComputingWrapper, CollectionComputing<T>>));
|
||||
}
|
||||
|
||||
private void CollectionUpdated(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
=> Task.Run(async () => await Fire(_collectionWrapper?.Collection)).Wait();
|
||||
|
||||
private async Task SetValue<TWrapper, TCollection>(TCollection? next, CancellationToken cancellationToken = default)
|
||||
where TCollection : IList<T>, INotifyCollectionChanged
|
||||
where TWrapper : ICollectionWrapper<TCollection>
|
||||
{
|
||||
_collectionWrapper?.Dispose();
|
||||
|
||||
_collectionWrapper = next is null
|
||||
? null
|
||||
: TWrapper.Create(next, CollectionUpdated);
|
||||
|
||||
await Fire(next, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task Fire(IList<T>? items, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var newValue = items is null
|
||||
? default
|
||||
: _extractor(items);
|
||||
|
||||
await SetNewValueAsync(newValue, cancellationToken);
|
||||
}
|
||||
}
|
||||
34
src/Library/DeclarativeProperty/FilterProperty.cs
Normal file
34
src/Library/DeclarativeProperty/FilterProperty.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class FilterProperty<T> : DeclarativePropertyBase<T>
|
||||
{
|
||||
private readonly Func<T?, Task<bool>> _filter;
|
||||
|
||||
public FilterProperty(
|
||||
Func<T?, Task<bool>> filter,
|
||||
IDeclarativeProperty<T> from,
|
||||
Action<T?>? setValueHook = null) : base(setValueHook)
|
||||
{
|
||||
_filter = filter;
|
||||
AddDisposable(from.Subscribe(SetValue));
|
||||
}
|
||||
|
||||
private async Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (await _filter(next))
|
||||
{
|
||||
await SetNewValueAsync(next, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<FilterProperty<T>> CreateAsync(
|
||||
Func<T?, Task<bool>> filter,
|
||||
IDeclarativeProperty<T> from,
|
||||
Action<T?>? setValueHook = null)
|
||||
{
|
||||
var prop = new FilterProperty<T>(filter, from, setValueHook);
|
||||
await prop.SetValue(from.Value);
|
||||
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
17
src/Library/DeclarativeProperty/IDeclarativeProperty.cs
Normal file
17
src/Library/DeclarativeProperty/IDeclarativeProperty.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public interface IDeclarativeProperty<T> : INotifyPropertyChanged, IDisposable, IObservable<T>
|
||||
{
|
||||
T? Value { get; set; }
|
||||
IDisposable Subscribe(Func<T?, CancellationToken, Task> onChange);
|
||||
void Unsubscribe(Func<T?, CancellationToken, Task> onChange);
|
||||
|
||||
IDeclarativeProperty<T> RegisterTrigger(
|
||||
Func<IDeclarativeProperty<T>, T, IDisposable?> triggerSubscribe,
|
||||
Action<IDeclarativeProperty<T>, T>? triggerUnsubscribe = null
|
||||
);
|
||||
|
||||
Task ReFireAsync();
|
||||
}
|
||||
32
src/Library/DeclarativeProperty/MapProperty.cs
Normal file
32
src/Library/DeclarativeProperty/MapProperty.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class MapProperty<TFrom, TTo> : DeclarativePropertyBase<TTo>
|
||||
{
|
||||
private readonly Func<TFrom?, CancellationToken, Task<TTo?>> _mapper;
|
||||
|
||||
public MapProperty(
|
||||
Func<TFrom?, CancellationToken, Task<TTo?>> mapper,
|
||||
IDeclarativeProperty<TFrom?> from,
|
||||
Action<TTo?>? setValueHook = null) : base(setValueHook)
|
||||
{
|
||||
_mapper = mapper;
|
||||
AddDisposable(from.Subscribe(SetValue));
|
||||
}
|
||||
|
||||
private async Task SetValue(TFrom? next, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var newValue = await _mapper(next, cancellationToken);
|
||||
await SetNewValueAsync(newValue, cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<MapProperty<TFrom?, TTo?>> CreateAsync(
|
||||
Func<TFrom?, CancellationToken, Task<TTo?>> mapper,
|
||||
IDeclarativeProperty<TFrom?> from,
|
||||
Action<TTo?>? setValueHook = null)
|
||||
{
|
||||
var prop = new MapProperty<TFrom?, TTo?>(mapper, from, setValueHook);
|
||||
await prop.SetValue(from.Value);
|
||||
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
24
src/Library/DeclarativeProperty/SwitchProperty.cs
Normal file
24
src/Library/DeclarativeProperty/SwitchProperty.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public sealed class SwitchProperty<TItem> : DeclarativePropertyBase<TItem>
|
||||
{
|
||||
private IDisposable? _innerSubscription;
|
||||
|
||||
public SwitchProperty(IDeclarativeProperty<IDeclarativeProperty<TItem?>?> from) : base(from.Value is null ? default : from.Value.Value)
|
||||
{
|
||||
AddDisposable(from.Subscribe(HandleStreamChange));
|
||||
_innerSubscription = from.Value?.Subscribe(HandleInnerValueChange);
|
||||
}
|
||||
|
||||
private async Task HandleStreamChange(IDeclarativeProperty<TItem?>? next, CancellationToken token)
|
||||
{
|
||||
_innerSubscription?.Dispose();
|
||||
_innerSubscription = next?.Subscribe(HandleInnerValueChange);
|
||||
|
||||
|
||||
await SetNewValueAsync(next is null ? default : next.Value, token);
|
||||
}
|
||||
|
||||
private async Task HandleInnerValueChange(TItem? next, CancellationToken token)
|
||||
=> await SetNewValueAsync(next, token);
|
||||
}
|
||||
58
src/Library/DeclarativeProperty/ThrottleProperty.cs
Normal file
58
src/Library/DeclarativeProperty/ThrottleProperty.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public class ThrottleProperty<T> : TimingPropertyBase<T>
|
||||
{
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
private DateTime _lastFired;
|
||||
|
||||
public ThrottleProperty(
|
||||
IDeclarativeProperty<T> from,
|
||||
TimeSpan interval,
|
||||
Action<T?>? setValueHook = null) : base(from, interval, setValueHook)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task SetValue(T? next, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_debounceCts?.Cancel();
|
||||
if (DateTime.Now - _lastFired > Interval)
|
||||
{
|
||||
_lastFired = DateTime.Now;
|
||||
// Note: Recursive chains can happen. Awaiting this can cause a deadlock.
|
||||
Task.Run(async () => await FireAsync(next, cancellationToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
_debounceCts = new();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(Interval, _debounceCts.Token);
|
||||
await FireIfNeededAsync(
|
||||
next,
|
||||
() => { _lastFired = DateTime.Now; },
|
||||
_debounceCts.Token, cancellationToken
|
||||
);
|
||||
/*var shouldFire = WithLock(() =>
|
||||
{
|
||||
if (_debounceCts.Token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
_lastFired = DateTime.Now;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!shouldFire) return;
|
||||
|
||||
await Fire(next, cancellationToken);*/
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
85
src/Library/DeclarativeProperty/TimingPropertyBase.cs
Normal file
85
src/Library/DeclarativeProperty/TimingPropertyBase.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
public abstract class TimingPropertyBase<T> : DeclarativePropertyBase<T>
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
protected TimeSpan Interval { get; }
|
||||
|
||||
protected TimingPropertyBase(
|
||||
IDeclarativeProperty<T> from,
|
||||
TimeSpan interval,
|
||||
Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
|
||||
{
|
||||
Interval = interval;
|
||||
AddDisposable(from.Subscribe(SetValueInternal));
|
||||
}
|
||||
|
||||
private async Task SetValueInternal(T? next, CancellationToken cancellationToken = default)
|
||||
=> await WithLockAsync(async () => await SetValue(next, cancellationToken), cancellationToken);
|
||||
|
||||
|
||||
protected void WithLock(Action action)
|
||||
{
|
||||
try
|
||||
{
|
||||
_semaphore.Wait();
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected TResult WithLock<TResult>(Func<TResult> func)
|
||||
{
|
||||
try
|
||||
{
|
||||
_semaphore.Wait();
|
||||
return func();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task WithLockAsync(Func<Task> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken);
|
||||
await action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task SetValue(T? next, CancellationToken cancellationToken = default);
|
||||
|
||||
protected async Task FireIfNeededAsync(
|
||||
T? next,
|
||||
Action cleanup,
|
||||
CancellationToken timingCancellationToken = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.Delay(Interval, timingCancellationToken);
|
||||
var shouldFire = WithLock(() =>
|
||||
{
|
||||
if (timingCancellationToken.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
cleanup();
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!shouldFire) return;
|
||||
|
||||
await FireAsync(next, cancellationToken);
|
||||
}
|
||||
|
||||
protected async Task FireAsync(T? next, CancellationToken cancellationToken = default)
|
||||
=> await SetNewValueAsync(next, cancellationToken);
|
||||
}
|
||||
14
src/Library/DeclarativeProperty/Unsubscriber.cs
Normal file
14
src/Library/DeclarativeProperty/Unsubscriber.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace DeclarativeProperty;
|
||||
|
||||
internal sealed class Unsubscriber<T> : IDisposable
|
||||
{
|
||||
private readonly IDeclarativeProperty<T> _owner;
|
||||
private readonly Func<T?, CancellationToken, Task> _onChange;
|
||||
|
||||
public Unsubscriber(IDeclarativeProperty<T> owner, Func<T?, CancellationToken, Task> onChange)
|
||||
{
|
||||
_owner = owner;
|
||||
_onChange = onChange;
|
||||
}
|
||||
public void Dispose() => _owner.Unsubscribe(_onChange);
|
||||
}
|
||||
13
src/Library/Defer/Defer.cs
Normal file
13
src/Library/Defer/Defer.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace System;
|
||||
|
||||
public static class DeferTools
|
||||
{
|
||||
public static IDisposable Defer(Action action) => new DeferDisposable(action);
|
||||
}
|
||||
|
||||
internal readonly struct DeferDisposable : IDisposable
|
||||
{
|
||||
readonly Action _action;
|
||||
public DeferDisposable(Action action) => _action = action;
|
||||
public void Dispose() => _action();
|
||||
}
|
||||
10
src/Library/Defer/Defer.csproj
Normal file
10
src/Library/Defer/Defer.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>System</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user