Skip to content

Commit f807adb

Browse files
committed
added features to AsyncLoadingCollection for a possible integration with Windows Community Toolkit
Updated Readme Updated vulnerable nuget package
1 parent 00881f2 commit f807adb

File tree

3 files changed

+88
-28
lines changed

3 files changed

+88
-28
lines changed

MsGraphSamples.WinUI/Helpers/AsyncLoadingCollection.cs

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,76 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.ObjectModel;
5+
using System.ComponentModel;
56
using Microsoft.UI.Xaml.Data;
67
using Windows.Foundation;
78

89
namespace MsGraphSamples.WinUI.Helpers;
910

10-
public class AsyncLoadingCollection<T>(IAsyncEnumerable<T> source, uint itemsPerPage = 25) : ObservableCollection<T>, ISupportIncrementalLoading
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="AsyncLoadingCollection{T}"/> class.
13+
/// </summary>
14+
/// <param name="source">The source of items to load asynchronously.</param>
15+
/// <param name="maxItemsPerPage">The maximum number of items to load per page.</param>
16+
/// <param name="OnStartLoading">The action to invoke when loading starts.</param>
17+
/// <param name="OnEndLoading">The action to invoke when loading ends.</param>
18+
/// <param name="OnError">The action to invoke when an error occurs during loading.</param>
19+
/// <param name="cancellationToken">The cancellation token to cancel the loading operation.</param>
20+
public class AsyncLoadingCollection<T>(
21+
IAsyncEnumerable<T> source,
22+
uint defaultItemsPerPage = 25,
23+
CancellationToken cancellationToken = default)
24+
: ObservableCollection<T>, ISupportIncrementalLoading
1125
{
12-
private IAsyncEnumerator<T>? _asyncEnumerator = source.GetAsyncEnumerator();
26+
private IAsyncEnumerator<T>? _asyncEnumerator = source
27+
.GetAsyncEnumerator()
28+
.WithCancellation(cancellationToken);
29+
1330
private readonly SemaphoreSlim _mutex = new(1, 1);
31+
public Action? OnStartLoading { get; set; }
32+
public Action? OnEndLoading { get; set; }
33+
public Action<Exception>? OnError { get; set; }
1434

1535
public bool HasMoreItems => _asyncEnumerator != null;
1636

17-
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count = 0) =>
18-
LoadMoreItemsAsync(count == 0 ? itemsPerPage : count, default)
37+
private bool _isLoading;
38+
/// <summary>
39+
/// Gets a value indicating whether new items are being loaded.
40+
/// </summary>
41+
public bool IsLoading
42+
{
43+
get => _isLoading;
44+
45+
private set
46+
{
47+
if (value == _isLoading)
48+
return;
49+
50+
_isLoading = value;
51+
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsLoading)));
52+
53+
if (_isLoading)
54+
OnStartLoading?.Invoke();
55+
else
56+
OnEndLoading?.Invoke();
57+
}
58+
}
59+
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) =>
60+
LoadMoreItemsAsyncInternal(count)
1961
.AsAsyncOperation();
2062

21-
private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, CancellationToken cancellationToken)
63+
private async Task<LoadMoreItemsResult> LoadMoreItemsAsyncInternal(uint count)
2264
{
2365
await _mutex.WaitAsync(cancellationToken);
2466

25-
if (cancellationToken.IsCancellationRequested || !HasMoreItems)
26-
return new LoadMoreItemsResult(0);
27-
2867
uint itemsLoaded = 0;
29-
var itemsToLoad = Math.Min(itemsPerPage, count);
68+
IsLoading = true;
3069

3170
try
3271
{
33-
while (itemsLoaded < itemsToLoad)
72+
while (itemsLoaded < count && HasMoreItems)
3473
{
35-
if (await _asyncEnumerator!.MoveNextAsync(cancellationToken).ConfigureAwait(false))
74+
if (await _asyncEnumerator!.MoveNextAsync().ConfigureAwait(false))
3675
{
3776
Add(_asyncEnumerator!.Current);
3877
itemsLoaded++;
@@ -42,7 +81,6 @@ private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, Cancellat
4281
// Dispose the enumerator when we're done
4382
await _asyncEnumerator!.DisposeAsync();
4483
_asyncEnumerator = null;
45-
break;
4684
}
4785
}
4886
}
@@ -52,17 +90,39 @@ private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, Cancellat
5290
await _asyncEnumerator!.DisposeAsync();
5391
_asyncEnumerator = null;
5492
}
55-
catch (Exception)
93+
catch (Exception ex)
5694
{
95+
OnError?.Invoke(ex);
5796
await _asyncEnumerator!.DisposeAsync();
5897
_asyncEnumerator = null;
5998
throw;
6099
}
61100
finally
62101
{
102+
IsLoading = false;
63103
_mutex.Release();
64104
}
65105

66106
return new LoadMoreItemsResult(itemsLoaded);
67107
}
108+
109+
/// <summary>
110+
/// Clears the collection and triggers/forces a reload of the first page
111+
/// </summary>
112+
/// <returns>
113+
/// An object of the <see cref="LoadMoreItemsAsync(uint)"/> that specifies how many items have been actually retrieved.
114+
/// </returns>
115+
public async Task<LoadMoreItemsResult> RefreshAsync()
116+
{
117+
await _mutex.WaitAsync(cancellationToken);
118+
119+
Clear();
120+
_asyncEnumerator = source
121+
.GetAsyncEnumerator()
122+
.WithCancellation(cancellationToken);
123+
124+
_mutex.Release();
125+
126+
return await LoadMoreItemsAsync(defaultItemsPerPage);
127+
}
68128
}

MsGraphSamples.WinUI/ViewModels/MainViewModel.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial class MainViewModel(
2020
IAsyncEnumerableGraphDataService graphDataService,
2121
IDialogService dialogService) : ObservableRecipient
2222
{
23-
private readonly ushort pageSize = 25;
23+
public ushort PageSize { get; set; } = 25;
2424
private readonly Stopwatch _stopWatch = new();
2525
public long ElapsedMs => _stopWatch.ElapsedMilliseconds;
2626

@@ -125,11 +125,11 @@ private Task Load()
125125
return IsBusyWrapper(SelectedEntity switch
126126
{
127127
//"Users" => _graphDataService.GetUsersInBatch(SplittedSelect, pageSize),
128-
"Users" => graphDataService.GetUsers(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
129-
"Groups" => graphDataService.GetGroups(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
130-
"Applications" => graphDataService.GetApplications(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
131-
"ServicePrincipals" => graphDataService.GetServicePrincipals(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
132-
"Devices" => graphDataService.GetDevices(SplittedSelect, Filter, SplittedOrderBy, Search, pageSize),
128+
"Users" => graphDataService.GetUsers(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
129+
"Groups" => graphDataService.GetGroups(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
130+
"Applications" => graphDataService.GetApplications(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
131+
"ServicePrincipals" => graphDataService.GetServicePrincipals(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
132+
"Devices" => graphDataService.GetDevices(SplittedSelect, Filter, SplittedOrderBy, Search, PageSize),
133133
_ => throw new NotImplementedException("Can't find selected entity")
134134
});
135135
}
@@ -144,11 +144,11 @@ public Task DrillDown()
144144

145145
return IsBusyWrapper(SelectedEntity switch
146146
{
147-
"Users" => graphDataService.GetTransitiveMemberOfAsGroups(SelectedObject.Id!, SplittedSelect, pageSize),
148-
"Groups" => graphDataService.GetTransitiveMembersAsUsers(SelectedObject.Id!, SplittedSelect, pageSize),
149-
"Applications" => graphDataService.GetApplicationOwnersAsUsers(SelectedObject.Id!, SplittedSelect, pageSize),
150-
"ServicePrincipals" => graphDataService.GetServicePrincipalOwnersAsUsers(SelectedObject.Id!, SplittedSelect, pageSize),
151-
"Devices" => graphDataService.GetDeviceOwnersAsUsers(SelectedObject.Id!, SplittedSelect, pageSize),
147+
"Users" => graphDataService.GetTransitiveMemberOfAsGroups(SelectedObject.Id!, SplittedSelect, PageSize),
148+
"Groups" => graphDataService.GetTransitiveMembersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
149+
"Applications" => graphDataService.GetApplicationOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
150+
"ServicePrincipals" => graphDataService.GetServicePrincipalOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
151+
"Devices" => graphDataService.GetDeviceOwnersAsUsers(SelectedObject.Id!, SplittedSelect, PageSize),
152152
_ => throw new NotImplementedException("Can't find selected entity")
153153
});
154154
}
@@ -199,10 +199,10 @@ private async Task IsBusyWrapper(IAsyncEnumerable<DirectoryObject> directoryObje
199199
// Sending message to generate DataGridColumns according to the selected properties
200200
await GetPropertiesAndSortDirection(directoryObjects);
201201

202-
DirectoryObjects = new(directoryObjects, pageSize);
202+
DirectoryObjects = new(directoryObjects, PageSize);
203203

204204
// Trigger load due to bug https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3584
205-
await DirectoryObjects.LoadMoreItemsAsync();
205+
await DirectoryObjects.RefreshAsync();
206206

207207
SelectedEntity = DirectoryObjects.FirstOrDefault() switch
208208
{

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The main code is in [AsyncEnumerableGraphDataService.cs](MsGraphSamples.Services
5353

5454
## Prerequisites
5555

56-
- Either [Visual Studio (>v16.8)](https://aka.ms/vsdownload) *or* [Visual Studio Code](https://code.visualstudio.com/) with [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet/7.0) and [C# for Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
56+
- Either [Visual Studio (>v16.8)](https://aka.ms/vsdownload) *or* [Visual Studio Code](https://code.visualstudio.com/) with [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) and [C# for Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
5757
- A Microsoft Entra ID tenant. For more information, see [How to get an Microsoft Entra ID tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/)
5858
- A user account in your Microsoft Entra ID tenant. This sample will not work with a personal Microsoft account (formerly Windows Live account). Therefore, if you signed in to the [Azure portal](https://portal.azure.com) with a Microsoft account and have never created a user account in your directory before, you need to do that now.
5959

@@ -91,7 +91,7 @@ or download and extract the repository .zip file.
9191
This application use the [.NET Core Secret Manager](https://docs.microsoft.com/aspnet/core/security/app-secrets) to store the **ClientId**.
9292
To add the **ClientId** created on step 1 of registration:
9393

94-
1. Open a **Developer Command Prompt** or an **Integrated Terminal** and locate the `dotnet-aad-query-sample\MSGraphSamples.WPF\` directory.
94+
1. Open a **Developer Command Prompt** or an **Integrated Terminal** and locate the `dotnet-aad-query-sample\MsGraphSamples.Services\` directory.
9595
1. Type `dotnet user-secrets set "clientId" "<YOUR CLIENT ID>"`
9696

9797
## Run the sample

0 commit comments

Comments
 (0)