Skip to content

Commit 048fe04

Browse files
committed
Merge branch 'main' into winui
2 parents 275c33d + e348f61 commit 048fe04

File tree

4 files changed

+468
-3
lines changed

4 files changed

+468
-3
lines changed

CommunityToolkit.WinUI/IncrementalLoadingCollection/IncrementalLoadingCollection.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class IncrementalLoadingCollection<TSource, IType> : ObservableCollection
3030
ISupportIncrementalLoading
3131
where TSource : IIncrementalSource<IType>
3232
{
33+
private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
34+
3335
/// <summary>
3436
/// Gets or sets an <see cref="Action"/> that is called when a retrieval operation begins.
3537
/// </summary>
@@ -226,7 +228,23 @@ public Task RefreshAsync()
226228
/// </returns>
227229
protected virtual async Task<IEnumerable<IType>> LoadDataAsync(CancellationToken cancellationToken)
228230
{
229-
var result = await Source.GetPagedItemsAsync(CurrentPageIndex++, ItemsPerPage, cancellationToken);
231+
var result = await Source.GetPagedItemsAsync(CurrentPageIndex, ItemsPerPage, cancellationToken)
232+
.ContinueWith(
233+
t =>
234+
{
235+
if(t.IsFaulted)
236+
{
237+
throw t.Exception;
238+
}
239+
240+
if (t.IsCompletedSuccessfully)
241+
{
242+
CurrentPageIndex += 1;
243+
}
244+
245+
return t.Result;
246+
}, cancellationToken);
247+
230248
return result;
231249
}
232250

@@ -235,6 +253,9 @@ private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, Cancellat
235253
uint resultCount = 0;
236254
_cancellationToken = cancellationToken;
237255

256+
// TODO (2021.05.05): Make use common AsyncMutex class.
257+
// AsyncMutex is located at Microsoft.Toolkit.Uwp.UI.Media/Extensions/System.Threading.Tasks/AsyncMutex.cs at the time of this note.
258+
await _mutex.WaitAsync();
238259
try
239260
{
240261
if (!_cancellationToken.IsCancellationRequested)
@@ -278,6 +299,8 @@ private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, Cancellat
278299
_refreshOnLoad = false;
279300
await RefreshAsync();
280301
}
302+
303+
_mutex.Release();
281304
}
282305

283306
return new LoadMoreItemsResult { Count = resultCount };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Toolkit.Collections;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace UnitTests.UI
13+
{
14+
public class DataSource<T> : IIncrementalSource<T>
15+
{
16+
private readonly IEnumerable<T> items;
17+
private readonly Queue<PageOperation> pageRequestOperations;
18+
19+
public delegate IEnumerable<T> PageOperation(IEnumerable<T> page);
20+
21+
public DataSource(IEnumerable<T> items, IEnumerable<PageOperation> pageOps)
22+
: this(items, new Queue<PageOperation>(pageOps))
23+
{
24+
}
25+
26+
public DataSource(IEnumerable<T> items, params PageOperation[] pageOps)
27+
: this(items, new Queue<PageOperation>(pageOps))
28+
{
29+
}
30+
31+
public DataSource(IEnumerable<T> items, Queue<PageOperation> pageOps = default)
32+
{
33+
this.items = items ?? throw new ArgumentNullException(nameof(items));
34+
this.pageRequestOperations = pageOps ?? new Queue<PageOperation>();
35+
}
36+
37+
public static PageOperation MakeDelayOp(int delay)
38+
=> new (page =>
39+
{
40+
Thread.Sleep(delay);
41+
return page;
42+
});
43+
44+
public static IEnumerable<T> ThrowException(IEnumerable<T> page) => throw new Exception();
45+
46+
public static IEnumerable<T> PassThrough(IEnumerable<T> page) => page;
47+
48+
public async Task<IEnumerable<T>> GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default)
49+
{
50+
// Gets items from the collection according to pageIndex and pageSize parameters.
51+
var result = (from p in items
52+
select p).Skip(pageIndex * pageSize).Take(pageSize);
53+
54+
return this.pageRequestOperations.TryDequeue(out var op)
55+
? await Task.Factory.StartNew(new Func<object, IEnumerable<T>>(o => op(o as IEnumerable<T>)), state: result)
56+
: result;
57+
}
58+
}
59+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Toolkit.Uwp;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting;
12+
13+
namespace UnitTests.UI
14+
{
15+
[TestClass]
16+
public class Test_IncrementalLoadingCollection
17+
{
18+
private const int PageSize = 20;
19+
private const int Pages = 5;
20+
21+
private static readonly DataSource<int>.PageOperation[] FailPassSequence
22+
= new DataSource<int>.PageOperation[]
23+
{
24+
DataSource<int>.ThrowException, DataSource<int>.PassThrough,
25+
DataSource<int>.ThrowException, DataSource<int>.PassThrough,
26+
DataSource<int>.ThrowException, DataSource<int>.PassThrough,
27+
DataSource<int>.ThrowException, DataSource<int>.PassThrough,
28+
DataSource<int>.ThrowException, DataSource<int>.PassThrough,
29+
};
30+
31+
private static readonly int[] AllData
32+
= Enumerable.Range(0, Pages * PageSize).ToArray();
33+
34+
[DataRow]
35+
[DataRow(2500, 1000, 1000, 1000, 1000)]
36+
[TestMethod]
37+
public async Task Requests(params int[] pageDelays)
38+
{
39+
var source = new DataSource<int>(AllData, pageDelays.Select(DataSource<int>.MakeDelayOp));
40+
var collection = new IncrementalLoadingCollection<DataSource<int>, int>(source, PageSize);
41+
42+
for (int pageNum = 1; pageNum <= Pages; pageNum++)
43+
{
44+
await collection.LoadMoreItemsAsync(0);
45+
CollectionAssert.AreEqual(Enumerable.Range(0, PageSize * pageNum).ToArray(), collection);
46+
}
47+
}
48+
49+
[DataRow]
50+
[DataRow(2500, 1000, 1000, 1000, 1000)]
51+
[TestMethod]
52+
public async Task RequestsAsync(params int[] pageDelays)
53+
{
54+
var source = new DataSource<int>(AllData, pageDelays.Select(DataSource<int>.MakeDelayOp));
55+
var collection = new IncrementalLoadingCollection<DataSource<int>, int>(source, PageSize);
56+
57+
var requests = new List<Task>();
58+
59+
for (int pageNum = 1; pageNum <= Pages; pageNum++)
60+
{
61+
requests.Add(collection.LoadMoreItemsAsync(0).AsTask()
62+
.ContinueWith(t => Assert.IsTrue(t.IsCompletedSuccessfully)));
63+
}
64+
65+
await Task.WhenAll(requests);
66+
67+
CollectionAssert.AreEqual(AllData, collection);
68+
}
69+
70+
[TestMethod]
71+
public async Task FirstRequestFails()
72+
{
73+
var source = new DataSource<int>(AllData, DataSource<int>.ThrowException);
74+
var collection = new IncrementalLoadingCollection<DataSource<int>, int>(source, PageSize);
75+
76+
await Assert.ThrowsExceptionAsync<AggregateException>(collection.LoadMoreItemsAsync(0).AsTask);
77+
78+
Assert.IsTrue(!collection.Any());
79+
80+
var requests = new List<Task>();
81+
82+
for (int pageNum = 1; pageNum <= Pages; pageNum++)
83+
{
84+
requests.Add(collection.LoadMoreItemsAsync(0).AsTask()
85+
.ContinueWith(t => Assert.IsTrue(t.IsCompletedSuccessfully)));
86+
}
87+
88+
await Task.WhenAll(requests);
89+
90+
CollectionAssert.AreEqual(AllData, collection);
91+
}
92+
93+
[TestMethod]
94+
public async Task EveryOtherRequestFails()
95+
{
96+
var source = new DataSource<int>(AllData, FailPassSequence);
97+
var collection = new IncrementalLoadingCollection<DataSource<int>, int>(source, PageSize);
98+
99+
var willFail = true;
100+
for (int submitedRequests = 0; submitedRequests < Pages * 2; submitedRequests++)
101+
{
102+
if (willFail)
103+
{
104+
await Assert.ThrowsExceptionAsync<AggregateException>(collection.LoadMoreItemsAsync(0).AsTask);
105+
}
106+
else
107+
{
108+
await collection.LoadMoreItemsAsync(0);
109+
}
110+
111+
willFail = !willFail;
112+
}
113+
114+
CollectionAssert.AreEqual(AllData, collection);
115+
}
116+
117+
[TestMethod]
118+
public async Task EveryOtherRequestFailsAsync()
119+
{
120+
var source = new DataSource<int>(AllData, FailPassSequence);
121+
var collection = new IncrementalLoadingCollection<DataSource<int>, int>(source, PageSize);
122+
123+
var requests = new List<Task>();
124+
125+
var willFail = true;
126+
for (int submitedRequests = 0; submitedRequests < Pages * 2; submitedRequests++)
127+
{
128+
if (willFail)
129+
{
130+
requests.Add(Assert.ThrowsExceptionAsync<AggregateException>(collection.LoadMoreItemsAsync(0).AsTask));
131+
}
132+
else
133+
{
134+
requests.Add(collection.LoadMoreItemsAsync(0).AsTask().ContinueWith(t => Assert.IsTrue(t.IsCompletedSuccessfully)));
135+
}
136+
137+
willFail = !willFail;
138+
}
139+
140+
await Task.WhenAll(requests);
141+
142+
CollectionAssert.AreEqual(AllData, collection);
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)