Chargement d’un large volume de données : Gestion de ISupportIncrementalLoading

 

Téléchargez le code source : https://aka.ms/q4mity

Dans un précédant billet https://aka.ms/islchg , j’expliquais qu’il est possible de charger les données en arrière plan en évitant de figer l’interface utilisateur, précepte fort du développement Windows 8.

L’idée de ce billet c’est d’aller un peu plus loin et de fournir une collection générique en C# (IncrementalLoadingCollection) , qui charge les données de manière incrémentale comme son nom l’indique afin de permettre un défilement d’un large volume de données de manière fast & fluid.

Pour ce faire, il faut que la classe implémente entre autre l’interface ISupportIncrementalLoading, qui combinée avec les attributs DataFetchSize et IncrementalLoadingThreshold permettent aux contrôles GridView ou ListView, ou tout autres contrôles qui implémentent ListViewBase de charger séquentiellement des données .

Il faut également que cette classe implémente IList et INofifyCollectionChanged. Mais afin de simplifier ce billet, dans notre exemple nous hériterons directement de la classe ObservableCollection<T> , (Dans le code fournit à cette adresse https://aka.ms/q4mity, vous retrouverez un exemple qui implémente ces deux dernières interfaces)

 

Code Snippet

  1. public class IncrementalLoadingCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading

 

Le constructeur de notre classe prend comme paramètre un délégué, qui sera exécuté afin de retrouver un sous ensemble des données. Il prend comme paramètre un CancellationToken afin de pouvoir arrêter le chargement, et une liste de type ObservableCollection encapsulée dans une Task

Code Snippet

  1. //delegate which populate the next items        
  2.         Func<CancellationToken, uint, Task<ObservableCollection<T>>> _func;

Code Snippet

  1. public IncrementalLoadingCollection(Func<CancellationToken, uint, Task<ObservableCollection<T>>> func, uint maxItems)
  2.         {
  3.             _func = func;
  4.             if (maxItems == 0) //Infinite
  5.             {
  6.                 _isInfinite = true;
  7.             }
  8.             else
  9.             {
  10.                 _maxItems = maxItems;
  11.                 _isInfinite = false;
  12.             }
  13.         }

 

Gestion de ISupportIncrementalLoading

Pour gérer cette interface, il faut implémenter deux méthodes :

HasMoreItems, qui comme sont nom l’indique est invoquée par le contrôle, pour savoir si il doit déclencher le chargement de plus d’éléments.

Voici un exemple de cette méthode qui gère un nombre d’éléments fini/ou infini, et qui permet d’arrêter le chargement si un arrêt est demandé.

Code Snippet

  1. public bool HasMoreItems
  2.         {
  3.             get
  4.             {
  5.                 if (_cts.IsCancellationRequested)
  6.                     return false;
  7.  
  8.                 if (_isInfinite)
  9.                 {
  10.                     return true;
  11.                 }
  12.                 return this.Count < _maxItems;
  13.             }
  14.         }

LoadMoreItemsAsync, est invoquée par le contrôle qui passe comme paramètre la variable count, et qui est calculé en fonction de plusieurs facteurs.

Tout d’abord, le contrôle à besoin de connaitre le nombre d’objets visuels à afficher et à virtualiser.. Pour cela il exécute toujours  une 1ere fois la méthode LoadMoreItemsAsync avec un count=1 

Ensuite, pour un objet qui fera 480*680, sur un écran 15” de résolution 1920*1080, le nombre d’objet affiché (Visible à l’écran) sera de 4 et le nombre virtualisé de 20, comme sur l’image suivante : count =24
picture1

Les appels successifs seront dans notre cas tous avec un count=24.

Maintenant il est possible d’outrepasser cette valeur en utilisant les attributs du contrôle DataFetchSize et/ou IncrementalLoadingThreshold.

Par exemple sur la même configuration , un DataFetchSize=1, donne une virtualisation de count=16

un DataFetchSize=5 donne une virtualisation de count=40, et ainsi de suite…

L’attribut IncrementalLoadingThreshold, quand à lui indiquera au contrôle d’appeler X fois la méthode LoadMoreItemsAsync lors du 1er chargement des données.

Par exemple sur la même configuration, un DataFetchSize=5 et IncrementalLoadingThreshold=10 donne toujours un count=40 mais exécuté 3 fois d’affiler soit 120 éléments virtualisés. Ensuite lors du défilement, le contrôle redemandera des données avec un incrément de 40.

C’est à vous de jouer sur ces différentes valeurs pour trouver le meilleur compromis. (Dans l’exemple fournit vous pourrez jouer avec des curseurs pour constater les différences de chargement comme illustré sur la figure suivante : )

apbar

Détaillons maintenant un peu la méthode LoadMoreItemsAsync

Vous pouvez constater qu’elle retourne une Interface IAsyncOperation<LoadMoreItemsResult>. Pour créer cette interface j’utilise ici la méthode AsyncInfo.Run qui m’est fournit dans l’espace de nom  System.Runtime.InteropServices.WindowsRuntime, en lui passant comme paramètre une expression Lambda, qui elle même appel une méthode interne InternalLoadMoreItemsAsync()

Cette dernière à pour but de calculer si besoin est le nombre (numberOfItemsTogenerate) de données à charger en fonction de la variable count.

Puis d’exécuter le délégué (_func) qui est passé au constructeur de la classe afin de générer une liste intermédiaire  de données. C’est ce délégué que vous aurez à implémenter afin de retrouver de manière incrémentale les données, comme nous le verrons plus tard.

La liste intermédiaire est en fin agrégée avec la liste courante qui constituera notre liste finale.

Code Snippet

  1. public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
  2.         {                       
  3.             return AsyncInfo.Run((cts) => InternalLoadMoreItemsAsync(cts, count));                        
  4.         }      
  5.         async Task<LoadMoreItemsResult> InternalLoadMoreItemsAsync(CancellationToken cts, uint count)
  6.         {
  7.            
  8.             ObservableCollection<T> intermediate = null;          
  9.             _cts = cts;     
  10.                 var baseIndex = this.Count;
  11.                 uint numberOfitemsTogenerate = 0;
  12.  
  13.                 if (!_isInfinite)
  14.                 {
  15.                     if (baseIndex + count < _maxItems)
  16.                     {
  17.                         numberOfitemsTogenerate = count;
  18.  
  19.                     }
  20.                     else
  21.                     {
  22.                         //take the last items
  23.                         numberOfitemsTogenerate = _maxItems - (uint)(baseIndex);
  24.                     }
  25.  
  26.                 }
  27.                 else
  28.                 {
  29.                     numberOfitemsTogenerate = count;
  30.                 }                                 
  31.                 intermediate = await _func(cts, numberOfitemsTogenerate);              
  32.                 if (intermediate.Count == 0) //no more items stop the incremental loading
  33.                 {
  34.                     _maxItems = (uint)this.Count;                    
  35.                     _isInfinite = false;
  36.                 }
  37.                 else
  38.                 {
  39.                     intermediate.AddTo<T>(this);
  40.                 }                   
  41.                 return new LoadMoreItemsResult { Count = (uint)intermediate.Count };                   
  42.         }             

Utilisation de la classe IncrementalCollection<T>

pour utiliser cette classe il vous suffira de créer votre modèle de données qui sera lié à votre XAML. par exemple :

Code Snippet

  1. public class DataModel : BindableBase
  2.     {
  3.         private Uri _uri;
  4.         public Uri UriPath
  5.         {
  6.             get { return _uri; }
  7.             set { this.SetProperty(ref _uri, value); }
  8.  
  9.         }             
  10.         private String _title;
  11.         public String Title
  12.         {
  13.             get { return _title; }
  14.             set { this.SetProperty(ref _title, value); }
  15.  
  16.         }
  17.  
  18.         //Use to show the download progress
  19.         private int _progress;
  20.         public int Progress
  21.         {
  22.             get { return _progress; }
  23.             set { this.SetProperty(ref _progress, value); }
  24.  
  25.         }
  26.         //Flag to know if the picture come from a remote source        
  27.         private Boolean _isRemote;
  28.         public Boolean IsRemote
  29.         {
  30.             get { return _isRemote; }
  31.             set { this.SetProperty(ref _isRemote, value); }
  32.  
  33.         }
  34.         private String _toolTip;
  35.         public String ToolTip
  36.         {
  37.             get { return _toolTip; }
  38.             set { this.SetProperty(ref _toolTip, value); }
  39.  
  40.         }               
  41.     }

 

Puis d’implémenter l’algorithme qui retrouve les données.

Dans notre exemple, j’ai copié 9561 images sur un blob Azure dont l’adresse est https://devosaure.blob.core.windows.net/images/

Je passe au constructeur de la classe IncrementalCollection, une expression Lanbda qui implémente un algorithme très simple et basé sur le nom des images qui va de UrzaGatherer (1).Jpg à UrzaGatherer (9561).jpg. J’incrémente une variable pageNumber, qui me permet de retrouver les images en fonction du nombre count passé en paramètre.

puis je construit une liste intermédiaire que je retourne et qui sera utilisé par la méthode LoadMoreItemsAsync. N’oubliez pas que , que c’est cette dernière qui exécutera cette lambda.

 

Code Snippet

  1. private void GetAzureBlobImagesAsync()
  2. {
  3.  
  4.     uint pageNumber = 1;
  5.     _data = new IncrementalLoadingCollection<DataModel>((CancellationToken cts, uint count) =>
  6.     {
  7.  
  8.         return Task.Run<ObservableCollection<DataModel>>(() =>
  9.         {
  10.             //***************************************
  11.             //Your code start here                
  12.             ObservableCollection<DataModel> intermediateList = new ObservableCollection<DataModel>();
  13.  
  14.             for (uint i = pageNumber; i < pageNumber + count; i++)
  15.             {
  16.                 String FileName = String.Format("UrzaGatherer ({0}).jpg", i.ToString());
  17.                 String RemotePath = String.Format(@"https://devosaure.blob.core.windows.net/images/{0}", FileName);
  18.                 DataModel item = new DataModel();
  19.                 item.IsRemote = true;
  20.                 item.Title = FileName;
  21.                 item.UriPath = new Uri(RemotePath);
  22.                 intermediateList.Add(item);
  23.             }
  24.             pageNumber += count;
  25.             return intermediateList;
  26.             //*************and finish here**************************      
  27.         });
  28.  
  29.     }, 9651);
  30.  
  31.     _data.CollectionChanged += data_CollectionChanged;
  32.     itemsGridViewIncremental.DataContext = _data;          
  33. }

Vous retrouverez dans le code source fournit (https://aka.ms/q4mity), un exemple autour des APIS Bing (Vous devrez alors fournir votre propre APPID (https://ssl.bing.com/webmaster/developers/createapp.aspx)

Ainsi qu’un code qui permet de charger des fichiers images en local. Pour ce dernier exercice vous devrez copier des fichiers images dans le répertoire suivant de l’application :C:\Users\ [Votre nom d’utilisateur] \AppData\Local\Packages\ericvIncremental_nwdczr7kjtdqt\LocalState

 

Pour exécuter le code fournit, il suffit après le chargement de l’application, cliquez droit sur la souris pour faire apparaitre l’Appbar qui contient les commandes a exécuter.

Vous noterez qu’il est possible de débrayer le chargement incrémental en cliquant sur la case à cocher  Virtualisation, qui positionne par liaison de donnée l’attribut IncrementalLoadingTrigger = IncrementalLoadingTrigger.None

Dans ce dernier cas, le contrôle n’exécute pas la méthode LoadMoreItemsAsync(), c’est à vous de le faire explicitement. Ce scénario peut être intéressant, lorsque vous souhaitez vous même gérer le chargement incrémentale.

 

Eric Vernié