Skip to content

Commit

Permalink
Merge pull request #759 from unoplatform/dev/dr/lfEntityTracking
Browse files Browse the repository at this point in the history
feat(feeds): Entity tracking in list feeds
  • Loading branch information
dr1rrb authored Sep 27, 2022
2 parents 7fd55f3 + 8f534ed commit 31142a1
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 77 deletions.
3 changes: 3 additions & 0 deletions src/Uno.Extensions.Reactive.Testing/ConstraintParts/Items.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public static ItemsChanged Remove<T>(int at, IEnumerable<T> items)
public static ItemsChanged Replace<T>(int at, IEnumerable<T> oldItems, IEnumerable<T> newItems)
=> ItemsChanged.Replace(at, oldItems, newItems);

public static ItemsChanged Replace<T>(int at, T oldItem, T newItem)
=> ItemsChanged.Replace(at, oldItem, newItem);

public static ItemsChanged Move<T>(int from, int to, params T[] items)
=> ItemsChanged.Move(from, to, items);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static ItemsChanged Remove<T>(int index, IEnumerable<T> items)
public static ItemsChanged Replace<T>(int index, IEnumerable<T> oldItems, IEnumerable<T> newItems)
=> new(RichNotifyCollectionChangedEventArgs.ReplaceSome(oldItems.ToList(), newItems.ToList(), index));

public static ItemsChanged Replace<T>(int index, T oldItem, T newItem)
=> new(RichNotifyCollectionChangedEventArgs.Replace(oldItem, newItem, index));

public static ItemsChanged Move<T>(int oldIndex, int newIndex, params T[] items)
=> new(RichNotifyCollectionChangedEventArgs.MoveSome(items.ToList(), oldIndex, newIndex));

Expand All @@ -42,9 +45,16 @@ public static ItemsChanged Reset<T>(IEnumerable<T> oldItems, IEnumerable<T> newI
public static ItemsChanged Reset<T>(IEnumerable<T> newItems)
=> new(RichNotifyCollectionChangedEventArgs.Reset(null, newItems.ToList()));

public static ItemsChanged operator &(ItemsChanged left, ItemsChanged right)
=> new(left._expectedArgs.Concat(right._expectedArgs).ToImmutableList());

internal ItemsChanged(params RichNotifyCollectionChangedEventArgs[] expectedArgs)
=> _expectedArgs = expectedArgs.ToImmutableList();

internal ItemsChanged(IImmutableList<RichNotifyCollectionChangedEventArgs> expectedArgs)
=> _expectedArgs = expectedArgs.ToImmutableList();


/// <inheritdoc />
public override void Assert(ChangeCollection actual)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.Extensions.Equality;
using Uno.Extensions.Reactive.Operators;
using Uno.Extensions.Reactive.Testing;

namespace Uno.Extensions.Reactive.Tests.Operators;
Expand Down Expand Up @@ -36,4 +40,104 @@ await result.Should().BeAsync(r => r
.Message(Changed.Data, Data.None, Error.No, Progress.Final)
);
}

[TestMethod]
public async Task When_KeyEquatableNoComparerAndUpdate_Then_TrackItemsUsingKeyEquality()
{
var original = new MyKeyedRecord[] { new(1, 1), new(2, 1), new(3, 1), new(4, 1) };
var updated = new MyKeyedRecord[] { new(1, 1), new(2, 2), new(4, 1), new(5, 1) };

async IAsyncEnumerable<IImmutableList<MyKeyedRecord>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}

var source = Feed.AsyncEnumerable(GetSource);
var sut = new FeedToListFeedAdapter<MyKeyedRecord>(source);
var result = sut.Record();

await result.Should().BeAsync(r => r
.Message(m => m
.Current(Items.Some(original), Error.No, Progress.Final)
.Changed(Items.Reset(original)))
.Message(m => m
.Current(Items.Some(updated), Error.No, Progress.Final)
.Changed(Items.Replace(1, new MyKeyedRecord(2,1), new MyKeyedRecord(2,2))
& Items.Remove(2, new MyKeyedRecord(3,1))
& Items.Add(3, new MyKeyedRecord(5,1))))
);
}

[TestMethod]
public async Task When_NotKeyEquatableNoComparerAndUpdate_Then_TrackItemsUsingKeyEquality()
{
var original = new MyNotKeyedRecord[] { new(1, 1), new(2, 1), new(3, 1), new(4, 1) };
var updated = new MyNotKeyedRecord[] { new(1, 1), new(2, 2), new(4, 1), new(5, 1) };

async IAsyncEnumerable<IImmutableList<MyNotKeyedRecord>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}

var source = Feed.AsyncEnumerable(GetSource);
var sut = new FeedToListFeedAdapter<MyNotKeyedRecord>(source);
var result = sut.Record();

await result.Should().BeAsync(r => r
.Message(m => m
.Current(Items.Some(original), Error.No, Progress.Final)
.Changed(Items.Reset(original)))
.Message(m => m
.Current(Items.Some(updated), Error.No, Progress.Final)
.Changed(Items.Remove(1, new MyNotKeyedRecord(2, 1), new MyNotKeyedRecord(3, 1))
& Items.Add(1, new MyNotKeyedRecord(2, 2))
& Items.Add(3, new MyNotKeyedRecord(5, 1))))
);
}

[TestMethod]
public async Task When_ClassNoComparerAndUpdate_Then_TrackItemsUsingKeyEquality()
{
var original = new MyNotKeyedClass[] { new(1, 1), new(2, 1), new(3, 1), new(4, 1) };
var updated = new MyNotKeyedClass[] { new(1, 1), new(2, 2), new(4, 1), new(5, 1) };

async IAsyncEnumerable<IImmutableList<MyNotKeyedClass>> GetSource([EnumeratorCancellation] CancellationToken ct = default)
{
yield return original.ToImmutableList();
yield return updated.ToImmutableList();
}

var source = Feed.AsyncEnumerable(GetSource);
var sut = new FeedToListFeedAdapter<MyNotKeyedClass>(source);
var result = sut.Record();

await result.Should().BeAsync(r => r
.Message(m => m
.Current(Items.Some(original), Error.No, Progress.Final)
.Changed(Items.Reset(original)))
.Message(m => m
.Current(Items.Some(updated), Error.No, Progress.Final)
.Changed(Items.Remove(0, original)
& Items.Add(0, updated)))
);
}

public partial record MyKeyedRecord(int Id, int Version);

[ImplicitKeyEquality(IsEnabled = false)]
public partial record MyNotKeyedRecord(int Id, int Version);

public partial class MyNotKeyedClass
{
public MyNotKeyedClass(int id, int version)
{
Id = id;
Version = version;
}

public int Id { get; init; }
public int Version { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public BindableListFeed(string propertyName, IListFeed<T> source, SourceContext
PropertyName = propertyName;

_state = ctx.GetOrCreateListState(source);
_items = CreateBindableCollection(ctx);
_items = CreateBindableCollection(_state, ctx);
}

/// <inheritdoc />
Expand Down Expand Up @@ -78,15 +78,17 @@ ValueTask IState<IImmutableList<T>>.UpdateMessage(Action<MessageBuilder<IImmutab
=> _state.UpdateMessage(updater, ct);


private BindableCollection CreateBindableCollection(SourceContext ctx)
private static BindableCollection CreateBindableCollection(IListState<T> state, SourceContext ctx)
{
var currentCount = 0;
var pageTokens = new TokenSetAwaiter<PageToken>();

var requests = new RequestSource();
var pagination = new PaginationService(LoadMore);
var services = new SingletonServiceProvider(pagination);
var collection = BindableCollection.Create<T>(services: services);
var collection = BindableCollection.Create(
services: services,
itemComparer: ListFeed<T>.DefaultComparer);

if (ctx.Token.CanBeCanceled)
{
Expand All @@ -98,7 +100,7 @@ private BindableCollection CreateBindableCollection(SourceContext ctx)
// Note: we have to listen for collection changes on bindable _items to update the state.
// https://github.com/unoplatform/uno.extensions/issues/370

_state.GetSource(ctx.CreateChild(requests), ctx.Token).ForEachAsync(
state.GetSource(ctx.CreateChild(requests), ctx.Token).ForEachAsync(
msg =>
{
if (ctx.Token.IsCancellationRequested)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data;
using Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Facets;
using Uno.Extensions.Reactive.Bindings.Collections.Services;
using Uno.Extensions.Reactive.Collections;
using Uno.Extensions.Reactive.Dispatching;
using Uno.Extensions.Reactive.Utils;
using ISchedulersProvider = Uno.Extensions.Reactive.Dispatching.DispatcherHelper.FindDispatcher;
Expand All @@ -32,21 +33,7 @@ internal sealed partial class BindableCollection : ICollectionView, INotifyColle
/// Creates a new instance of a <see cref="BindableCollection"/>.
/// </summary>
/// <param name="initial">The initial items in the collection.</param>
/// <param name="itemComparer">
/// Comparer used to detect multiple versions of the **same entity (T)**, or null to use default.
/// <remarks>Usually this should only compare the ID of the entities in order to properly track the changes made on an entity.</remarks>
/// <remarks>For better performance, prefer provide null instead of <see cref="EqualityComparer{T}.Default"/> (cf. MapObservableCollection).</remarks>
/// </param>
/// <param name="itemVersionComparer">
/// Comparer used to detect multiple instance of the **same version** of the **same entity (T)**, or null to rely only on the <paramref name="itemComparer"/> (not recommanded).
/// <remarks>
/// This comparer will determine if two instances of the same entity (which was considered as equals by the <paramref name="itemComparer"/>),
/// are effectively equals or not (i.e. same version or not).
/// <br />
/// * If **Equals**: it's 2 **instances** of the **same version** of the **same entity** (all properties are equals), so we don't have to raise a <see cref="NotifyCollectionChangedAction.Replace"/>.<br />
/// * If **NOT Equals**: it's 2 **distinct versions** of the **same entity** (not all properties are equals) and we have to raise a 'Replace' to re-evaluate those properties.
/// </remarks>
/// </param>
/// <param name="itemComparer">Comparer used to track items.</param>
/// <param name="schedulersProvider">Schedulers provider to use to handle concurrency.</param>
/// <param name="services">A set of services that the collection can use (cf. Remarks)</param>
/// <param name="resetThreshold">Threshold on which the a single reset is raised instead of multiple collection changes.</param>
Expand All @@ -55,16 +42,12 @@ internal sealed partial class BindableCollection : ICollectionView, INotifyColle
/// </remarks>
internal static BindableCollection Create<T>(
IObservableCollection<T>? initial = null,
IEqualityComparer<T>? itemComparer = null,
IEqualityComparer<T>? itemVersionComparer = null,
ItemComparer<T> itemComparer = default,
ISchedulersProvider? schedulersProvider = null,
IServiceProvider? services = null,
int resetThreshold = DataStructure.DefaultResetThreshold)
{
var dataStructure = new DataStructure
(
(itemComparer?.ToEqualityComparer(), itemVersionComparer?.ToEqualityComparer())
)
var dataStructure = new DataStructure((ItemComparer)itemComparer)
{
ResetThreshold = resetThreshold
};
Expand All @@ -77,16 +60,12 @@ internal static BindableCollection Create<T>(
/// </summary>
internal static BindableCollection CreateUntyped(
IObservableCollection? initial = null,
IEqualityComparer? itemComparer = null,
IEqualityComparer? itemVersionComparer = null,
ItemComparer itemComparer = default,
ISchedulersProvider? schedulersProvider = null,
int resetThreshold = DataStructure.DefaultResetThreshold
)
{
var dataStructure = new DataStructure
(
(itemComparer, itemVersionComparer)
)
var dataStructure = new DataStructure(itemComparer)
{
ResetThreshold = resetThreshold
};
Expand All @@ -96,19 +75,13 @@ internal static BindableCollection CreateUntyped(

internal static BindableCollection CreateGrouped<TKey, T>(
IObservableCollection<TKey> initial,
IEqualityComparer<TKey>? keyComparer,
IEqualityComparer<TKey>? keyVersionComparer,
IEqualityComparer<T>? itemComparer = null,
IEqualityComparer<T>? itemVersionComparer = null,
ItemComparer<TKey> keyComparer,
ItemComparer<T> itemComparer = default,
ISchedulersProvider? schedulersProvider = null,
int resetThreshold = DataStructure.DefaultResetThreshold)
where TKey : IObservableGroup<T>
{
var dataStructure = new DataStructure
(
(keyComparer?.ToEqualityComparer(), keyVersionComparer?.ToEqualityComparer()),
(itemComparer?.ToEqualityComparer(), itemVersionComparer?.ToEqualityComparer())
)
var dataStructure = new DataStructure((ItemComparer)keyComparer, (ItemComparer)itemComparer)
{
ResetThreshold = resetThreshold
};
Expand All @@ -118,19 +91,13 @@ internal static BindableCollection CreateGrouped<TKey, T>(

internal static BindableCollection CreateGrouped<TGroup>(
IObservableCollection initial,
IEqualityComparer<TGroup>? groupComparer,
IEqualityComparer<TGroup>? groupVersionComparer,
IEqualityComparer? itemComparer,
IEqualityComparer? itemVersionComparer,
ItemComparer<TGroup> groupComparer,
ItemComparer itemComparer,
ISchedulersProvider? schedulersProvider = null,
int resetThreshold = DataStructure.DefaultResetThreshold)
where TGroup : IObservableGroup
{
var dataStructure = new DataStructure
(
(groupComparer?.ToEqualityComparer(), groupVersionComparer?.ToEqualityComparer()),
(itemComparer, itemVersionComparer)
)
var dataStructure = new DataStructure((ItemComparer)groupComparer, itemComparer)
{
ResetThreshold = resetThreshold
};
Expand All @@ -140,18 +107,12 @@ internal static BindableCollection CreateGrouped<TGroup>(

internal static BindableCollection CreateUntypedGrouped(
IObservableCollection initial,
IEqualityComparer? groupComparer,
IEqualityComparer? groupVersionComparer,
IEqualityComparer? itemComparer,
IEqualityComparer? itemVersionComparer,
ItemComparer groupComparer,
ItemComparer itemComparer,
ISchedulersProvider? schedulersProvider = null,
int resetThreshold = DataStructure.DefaultResetThreshold)
{
var dataStructure = new DataStructure
(
(groupComparer, groupVersionComparer),
(itemComparer, itemVersionComparer)
)
var dataStructure = new DataStructure(groupComparer, itemComparer)
{
ResetThreshold = resetThreshold
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ internal class DataStructure : IBindableCollectionDataStructure
{
public const int DefaultResetThreshold = 5;

private readonly (IEqualityComparer? itemComparer, IEqualityComparer? itemVersionComparer)[] _comparersStructure;
private readonly ItemComparer[] _comparersStructure;

public DataStructure(params (IEqualityComparer? itemComparer, IEqualityComparer? itemVersionComparer)[] comparersStructure)
public DataStructure(params ItemComparer[] comparersStructure)
{
_comparersStructure = comparersStructure;
}
Expand All @@ -33,7 +33,7 @@ public IBindableCollectionDataLayerStrategy GetLayer(uint level)
}

var comparers = _comparersStructure[level];
var tracker = new CollectionAnalyzer(new ItemComparer(comparers.itemComparer, comparers.itemVersionComparer));
var tracker = new CollectionAnalyzer(comparers);

if (level + 1 == _comparersStructure.Length)
{
Expand Down
Loading

0 comments on commit 31142a1

Please sign in to comment.