Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(feeds): Entity tracking in list feeds #759

Merged
merged 3 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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