Mobile paged requests using IAsyncEnumerable
One of the best new features in c# 8 is the #IAsyncEnumerable as it provide an asynchronous lazy behavior which will solve a lot of problems.
In this article we will explore how we can use the IAsyncEnumerable to handle the paged request with a very clean declarative programming style.
As an example lets make a UWP app that get Sam Smith Albums from Last.fm and show it in a GridView.
I will use the last.fm c# sdk https://github.com/inflatablefriends/lastfm which contains a nice albums search method
_client.Artist.GetTopTracksAsync(string ArtistName, bool AutoCorrection, int page, int pageSize)Now lets install 2 additional nuget packages AsyncEnumerableExtensions and AsyncEnumerableExtensions.Standard .
Now lets Build Our Main View Model
public class MainPageViewModel{private LastfmClient _client;public MainPageViewModel(){_client = new LastfmClient("b471c30029ba983849723e5120e7504c", "24c4739698eebb92f792b118095dbdff");Artists = GetArtists().ToIncrementalLoadingCollection();}public ISupportIncrementalLoading Artists { get; }private IAsyncEnumerable<AlbumModel> GetArtists(){return AsyncEnumerableExtensions.Standard.AsyncEnumerableBuilder.FromPaged((i, i1) =>_client.Artist.GetTopTracksAsync("Sam Smith", true, i, i1), tracks => tracks.Select(t =>new AlbumModel(){Name = t.Name,Id = t.Id,Image = t.Images.Large.ToString(),Duration = t.Duration}), (i, tracks) => tracks.TotalItems > i, (tracks, i) => tracks.Count() + i,tracks =>{var pagenumber = (tracks?.Page ?? 0) + 1;return pagenumber;},tracks => 20);}}
The magic happens in 2 places
The first one is GetArtists Function
private IAsyncEnumerable<AlbumModel> GetArtists(){return AsyncEnumerableExtensions.Standard.AsyncEnumerableBuilder.FromPaged((i, i1) =>_client.Artist.GetTopTracksAsync("Sam Smith", true, i, i1), tracks => tracks.Select(t =>new AlbumModel(){Name = t.Name,Id = t.Id,Image = t.Images.Large.ToString(),Duration = t.Duration}), (i, tracks) => tracks.TotalItems > i, (tracks, i) => tracks.Count() + i,tracks =>{var pagenumber = (tracks?.Page ?? 0) + 1;return pagenumber;},tracks => 20);}
Lets check AsyncEnumerableBuilder.FromPaged method parameters
1- a delegate that take page number and page size as input and return the actual data.
2- a mapping function that map the paged response returned from the server to a domain model that can be used by the app.
3- a predicate that take the paged response returned from the server and the current number of downloaded items as input and check if there is more pages or not.
4- a function that take the paged response returned from the server and the current number of downloaded items as input and return the new total number of items usually a simple sum.
5- a function that map take the paged response returned from the server and return next page number.
6- a function that map take the paged response returned from the server and return next page size.
The AsyncEnumerableBuilder.FromPaged use this 6 methods to create IAsyncEnumerable.
The Second Part of the magic is in
Artists = GetArtists().ToIncrementalLoadingCollection();The ToIncrementalLoadingCollection return an ISupportIncrementalLoading which can be used for lazy data binding in the view.
for better understanding lets check this runtime sequence
1- The initial call ToIncrementalLoadingCollection trigger a request for the first page and show it in the listview.
2- When the user scroll to the end of the listview , the list view ask the ISupportIncrementalLoadingCollection for more data.
3- The ISupportIncrementalLoadingCollection ask the IAsyncEnumerable for more data.
4- The IAsyncEnumerable check if any remaining pages , if yes he will load the data of the next page and pass it to the ISupportIncrementalLoadingCollection to show it in the list.
No more magic :D Here is the xaml binding and the page.xaml.cs , it is a normal binding
<GridView ItemsSource="{x:Bind ViewModel.Artists}"><GridView.ItemTemplate><DataTemplate x:DataType="local:AlbumModel"><Grid Background="Black" Height="100" Width="100"><Image Source="{x:Bind Image}" Margin="0,0,0,30" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" /><TextBlock Text="{x:Bind Name}" VerticalAlignment="Bottom" Foreground="White"/></Grid></DataTemplate></GridView.ItemTemplate></GridView>public sealed partial class MainPage : Page{public MainPage(){this.InitializeComponent();this.ViewModel = new MainPageViewModel();}public MainPageViewModel ViewModel { get; set; }}
Screenshot
