Implémenter une pagination Visualforce dans Salesforce.com

La pagination est un outil très pratique lorsqu'il s'agit d'afficher beaucoup de données à l'écran, cela permet à l'utilisateur de ne pas se perdre devant une montagne d'informations.

Cet article a pour but de citer celles disponibles et d'aider à choisir laquelle utiliser en prenant en compte le contexte d'une application.

2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Lorsque l'on développe une page VisualForce pour y afficher une liste de données, il peut devenir intéressant d'utiliser une pagination afin que l'utilisateur ne voie pas trop de données en même temps et soit perdu.

Cependant, il existe plusieurs types de pagination avec pour chacun, ses avantages et ses inconvénients. Il est donc nécessaire de les connaître pour déterminer lequel est le plus adapté à un contexte.

Cet article a pour but d'initier à la pagination des pages VisualForce en décrivant les différents types de pagination. Nous retrouvons pour cela l'OffSet, le standardSetController ou encore le queryMore.

Avant d'aller plus loin dans cet article, je vous propose quelques rappels sur les termes techniques qui vont être utilisés :

  • Salesforce.com est un CRM (Customer Relationship Manager) orienté SaaS (Software as a Service) et PaaS (Platform as a Service) permettant la gestion des relations clients d'une entreprise ;
  • Apex, est un langage de programmation côté serveur permettant de modifier la logique métier et le traitement des données ;
  • SOQL (Salesforce Object Query Language) est le langage permettant d'effectuer des requêtes dans la base de données de Salesforce.com.

II. Prérequis

Avant toute chose, vous devez disposer d'une instance afin de pouvoir suivre ce tutoriel. Si ce n'est pas le cas, je vous invite à vous rendre à cette adresse. Salesforce met à disposition plusieurs types d'instances qui proposent différentes fonctionnalités et donc à différents prix (prix par licence utilisateur et par mois). La seule instance que vous pourrez généreusement obtenir de la part de Salesforce est celle de développement (celle que je vous propose via le lien ci-dessus) qui, comme son nom l'indique, sert à tester et développer des services. Je vous laisse juger par vous-même la grille tarifaire sur les diverses instances que propose le CRM.

Vous aurez également besoin d'un environnement de développement pour écrire votre code. Vous pouvez utiliser celui fourni par Salesforce ou alors vous procurer celui compatible avec Eclipse et qui s'appelle Force.com IDE (je l'utiliserai tout au long de ce tutoriel). Pour ce dernier, rendez-vous sur l'Eclipse MarketPlace et installez-le.

III. Implémenter la pagination

Comme indiqué plus haut dans l'introduction, il existe plusieurs types de pagination, avec pour chacun ses avantages ainsi que ses inconvénients.

Cette partie de l'article vise à vous les présenter un par un pour que vous puissiez vous faire votre propre opinion sur le sujet.

III-A. Visualforce StandardSetController

Selon moi, c'est la meilleure méthode de pagination lorsqu'il n'y a pas énormément d'enregistrements à afficher à l'utilisateur.

Il s‘agit d'un outil très puissant puisque finalement, le code est assez simple et le serveur retourne uniquement les enregistrements à afficher sur la page (donc si nous n'affichons que 10 enregistrements par page, nous n'en retournons que 10 et non 20, 100, 1000…), ce qui permet de réduire fortement la taille du view state.

. Avantages :

  • traite les données côté serveur et ne retourne que les enregistrements à afficher, ce qui permet de réduire considérablement le view state de la page Visualforce et améliore ainsi ses performances ;
  • permet de gérer une pagination avec plus de 10 000 enregistrements ;
  • inclut de base des fonctionnalités telles que le « suivant », « précédent », « première », « dernière », et encore d'autres méthodes pouvant aider à simplifier l'implémentation de la pagination ;
  • permet de naviguer facilement d'une page à une autre ;
  • stocke en mémoire l'intégralité des enregistrements côté serveur pour des raisons de performances, cela a donc pour conséquence que si un enregistrement est modifié pendant qu'il est lu dans la page Visualforce ou que l'utilisateur consulte un autre enregistrement pour revenir dessus, les modifications ne seront pas prises en compte côté Visualforce et les anciennes données seront affichées.

. Inconvénient :

  • peut seulement être utilisé avec de l'Apex (et non via les API SOAP ou REST par exemple).

. Exemple d'utilisation :

Il y a deux façons d'initier un objet StandardSetController, soit via une liste de sObjects, soit via une requête.

Voici la méthode via une liste de sObjects :

 
Sélectionnez
1.
2.
List<account> accountList = [SELECT Name FROM Account LIMIT 20];
ApexPages.StandardSetController ssc = new ApexPages.StandardSetController(accountList);

Et la méthode via une requête :

 
Sélectionnez
1.
ApexPages.StandardSetController ssc = new ApexPages.StandardSetController(Database.getQueryLocator([SELECT Name,CloseDate FROM Opportunity]));

Ces lignes sont à déclarer en tant qu'attributs de votre classe. Vous avez ensuite le choix de leur affecter directement leur valeur comme montré au-dessus ou de simplement les déclarer puis de leur affecter une valeur dans le constructeur (en appelant une méthode ou directement en dur, mais c'est moins joli).

Ce qui peut ressembler à ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
Public class MyClass{

    ApexPages.StandardSetController ssc;

    public MyClass(){
        this.ssc = new ApexPages.StandardSetController(this.getOpportunities());
    }

    public List<Opportunity> getOpportunities(){
        return [
            SELECT Id, Name, CloseDate
            FROM Opportunity
        ];
    }
}

III-B. Offset

Cette méthode est utilisée avec une requête SOQL. Elle sert entre autres à requêter uniquement la liste des enregistrements que l'utilisateur va consulter sur la page où il se trouve sans stocker en mémoire les enregistrements.

Cela signifie que si l'utilisateur se trouve sur la deuxième page et qu'il a une pagination avec 25 enregistrements par page, seuls le 26e enregistrement jusqu'au 50e seront chargés. Cela a également pour conséquence qu'à chaque changement de page, une requête SOQL côté contrôleur sera exécutée.

. Avantages :

  • requête SOQL exécutée via les API SOAP ou REST ou encore de l'APEX ;
  • permet de ne pas avoir à stocker l'ensemble des enregistrements en mémoire, mais uniquement ceux qui seront affichés sur la page consultée, la page VisualForce est donc chargée plus rapidement.

. Inconvénients :

  • la clause OFFSET ne supporte pas plus de 2 000 enregistrements, cela signifie qu'il n'est pas capable d'aller requêter les 25 enregistrements qui suivent les 2 000 premiers. Vous ne pourrez donc pas requêter les 25 enregistrements suivant le 2 001e, la condition OFFSET 2001 retournera une erreur (QueryException : Maximum SOQL offset allowed is 2000) ;
  • aucun cache n'est disponible, cela signifie qu'à chaque changement de page, une requête SOQL est nécessaire pour charger la liste des enregistrements ;
  • si vous êtes sur la deuxième page et que vous cliquez sur la troisième page pour ensuite retourner sur la seconde, vos enregistrements peuvent avoir été modifiés par un autre utilisateur entre temps à cause du chargement via SOQL. Cela paraît comme un inconvénient dans le sens où cela impose un chargement de données côté contrôleur, donc un temps d'attente pour l'utilisateur et une diminution des performances.

. Exemple d'utilisation :

Prenons l'exemple d'un utilisateur utilisant une application affichant 25 comptes par page et se trouvant sur la première page, la requête SOQL ressemble à ceci :

 
Sélectionnez
1.
2.
3.
4.
SELECT Id, Name, BillingStreet, BillingPostalCode, BillingCity, BillingCountry
FROM Account
LIMIT 25
OFFSET 0

Maintenant, l'utilisateur souhaite accéder à la deuxième page, la requête SOQL ressemble alors à celle-ci :

 
Sélectionnez
1.
2.
3.
4.
SELECT Id, Name, BillingStreet, BillingPostalCode, BillingCity, BillingCountry
FROM Account
LIMIT 25
OFFSET 25

Pour être même plus précis, lorsque l'on utilise OFFSET dans la deuxième requête du dessus, nous demandons également les enregistrements qui précèdent, mais ils sont supprimés avant de les retourner. Cela signifie que nous demandons 50 enregistrements pour n'en retourner que 25.

Cette requête est à simplement inclure dans une méthode qui retourne les enregistrements que l'on souhaite afficher. Il n'y a rien de particulier pour mettre en place l'OFFSET.

Ce qui est à prendre en compte est que la valeur de la condition OFFSET sera certainement une variable (OFFSET :myVar) puisque quand l'utilisateur va cliquer sur des boutons tels que « Suivant » ou « Précédent », la requête va différer.

III-C. API query et queryMore

L'API query et queryMore sont à utiliser lors d'une grande quantité de données, cette quantité dépend de votre condition WHERE.

Pour des raisons de performances, ces données retournées par le contrôleur sont stockées dans le cache du client pendant une durée d'inactivité maximum de 15 minutes, vous obtiendrez ensuite une erreur lors de l'appel du queryMore.

. Avantages :

  • peut retourner une grande quantité de données à l'application ;
  • retrouve des données plus rapidement qu'un autre outil de pagination.

. Inconvénients :

  • suivant l'implémentation de l'application, l'ensemble des données peut être transmis au client dès son chargement et allonger le temps de chargement de la page Visualforce ;
  • le localisateur de requête a une durée de vie de 15 minutes d'inactivité, une erreur est alors ensuite retournée lors de l'appel du queryMore ;
  • 10 curseurs de requêtes peuvent être ouverts à un même moment.

. Exemple d'utilisation :

Nous souhaitons retourner tous les comptes ouverts vivant dans l'état de Californie (en Amérique, you know what I mean) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
QueryResult qResult = null;
try{
  
  String soqlQuery = "SELECT Name, StageName, Amount, Account.Name, Account.Named_Account__c FROM Opportunity where Account.BillingState = 'California' and Status__c = 'Open'";
  
  qResult = binding.query(soqlQuery);
  
  Boolean done = false;
  
  if (qResult.size > 0){
     while (!done){

        if (qResult.done){
           done = true;
        }else{
           qResult = binding.queryMore(qResult.queryLocator);
        }
     }
  // Implémentation de la pagination
  }
}

Les lignes présentes ci-dessus sont à intégrer dans une méthode (en les arrangeant à votre façon) qui sera appelée par le constructeur de votre contrôleur ou par la méthode appelée lors du clic sur le bouton « Suivant » ou « Précédent ».

IV. Exemple d'une pagination Visualforce

Maintenant que nous avons étudié plusieurs formes de paginations, rien ne vaut un exemple concret avec une utilisation complète (contrôleur et page Visualforce).

IV-A. Le scénario

Pas besoin d'avoir un contexte complexe, il nous faut juste un exemple où l'on doit afficher des données à l'utilisateur.

Pour cela, nous partirons sur le principe que lors de la création d'un compte, nous allons effectuer une recherche sur les enregistrements déjà existants en base pour les proposer et ainsi éviter de créer des doublons.

IV-B. Quelle pagination utiliser ?

Sachant que nous allons utiliser un formulaire (pour le nom, le code postal et la ville du client), nous pouvons penser que la quantité de données chargées ne devraient pas être trop importantes.

L'utilisateur ne devrait donc pas avoir besoin de se déplacer dans beaucoup de pages, nous allons par conséquent opter pour la solution du standardSetController.

IV-C. Le contrôleur

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
public with sharing class VFC01_CreateAccount {
    
    public AccountForm accountForm{get; set;}
    public List<Account> accountList{get; set;}
    public Integer size{get; set;}
    public Integer noOfRecords{get; set;} 
    public ApexPages.StandardSetController setCon {
        get {
            if(setCon == NULL){                
                this.initCon(new List<Account>());
            }
            
            return setCon;
        }
        set;
    }
    
    public VFC01_CreateAccount(){
      this.accountForm = new AccountForm();
      this.accountList = new List<Account>();   
      this.size = 10;   
    }
    
    /** Retourne les comptes répondant à la recherche du formulaire **/
    public void search(){
      String soqlQuery = 'SELECT Id, Name, Phone, BillingStreet, BillingPostalCode, BillingCity, BillingCountry FROM Account';
      Boolean isWhere = false;
      
      if(!String.IsBlank(accountForm.Name)){
        isWhere = true;
        
        soqlQuery += ' WHERE Name = :accountForm.Name'; 
      }
      
      if(!String.IsBlank(accountForm.PostalCode)){
        
        if(!isWhere){
          isWhere = true;
          soqlQuery += ' WHERE'; 
        }else{
          soqlQuery += ' AND';
        }
        
        soqlQuery += ' BillingPostalCode = \'' + accountForm.PostalCode + '\'';
      }
      
      if(!String.IsBlank(accountForm.City)){
        
        if(!isWhere){
          soqlQuery += ' WHERE';
        }else{
          soqlQuery += ' AND';
        }
        
        soqlQuery += ' BillingCity = \'' + accountForm.City + '\'';
      }
      
      this.initCon(Database.query(soqlQuery));
    }
    
    public List<Account> getAccounts(){
      return (List<Account>) setCon.getRecords();
    }
    
    public void initCon(List<Account> accountsToInit){
      setCon = new ApexPages.StandardSetController(accountsToInit);
        setCon.setPageSize(size);  
        noOfRecords = setCon.getResultSize();
    }
    
    public class AccountForm{
      
      public String Name{get; set;}
      public String PostalCode{get; set;}
      public String City{get; set;}       
    }
}

Ce qu'il faut retenir de ce contrôleur est l'utilisation d'un objet de type ApexPage.StandardSetController pour la pagination dans lequel nous allons lui transmettre notre liste de comptes à afficher grâce à la méthode initCon.

Cet objet va également nous servir pour nous déplacer de page en page, mais nous verrons cela dans la partie suivante.

IV-D. La page Visualforce

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
<apex:page controller="VFC01_CreateAccount" tabStyle="Account">

  <apex:form id="theForm">
    
    <apex:pageBlock id="thePageBlock">
    
      <apex:pageBlockButtons location="top">
        <apex:commandButton value="Rechercher" action="{!search}" />
      </apex:pageBlockButtons>
    
      <apex:pageBlockSection title="Recherche des comptes">
        <apex:inputText value="{!accountForm.Name}" label="Nom" />
        <apex:inputText value="{!accountForm.PostalCode}" label="Code postal" />
        <apex:inputText value="{!accountForm.City}" label="Ville" />
      </apex:pageBlockSection>
    
      <apex:pageBlockSection title="Comptes similaires">
        <apex:pageBlockTable value="{!accounts}" var="anAccount">
                     
                    <apex:column headerValue="Nom">
                        <apex:outputLink value="/{!anAccount.Id}" target="_blank">{!anAccount.Name}</apex:outputLink>
                    </apex:column>
                    
                    <apex:column headerValue="Téléphone">
                        <apex:outputField value="{!anAccount.Phone}"/>
                    </apex:column>
                     
                    <apex:column headerValue="Adresse">
                        <apex:outputField value="{!anAccount.BillingStreet}"/>
                    </apex:column>
                     
                    <apex:column headerValue="Code Postal">
                        <apex:outputField value="{!anAccount.BillingPostalCode}"/>
                    </apex:column>
                     
                    <apex:column headerValue="Ville">
                        <apex:outputField value="{!anAccount.BillingCity}"/>
                    </apex:column>
                     
                    <apex:column headerValue="Pays">
                        <apex:outputField value="{!anAccount.BillingCountry}"/>
                    </apex:column>
                </apex:pageBlockTable>
                
                <br/>
                
                <apex:panelGrid columns="6"> 
                 
                  <apex:commandLink status="fetchStatus" reRender="thePageBlock" value="Première page" action="{!setCon.first}" title="Première page" /> 
            <apex:commandLink status="fetchStatus" reRender="thePageBlock" value="Page précédente" action="{!setCon.previous}" title="Page précédente" /> 
            
            <apex:outputText >
                    {!(setCon.pageNumber * size) + 1 - size} - {!IF((setCon.pageNumber * size) > noOfRecords, noOfRecords, (setCon.pageNumber * size))} de {!noOfRecords}
                  </apex:outputText>            
            
            <apex:commandLink status="fetchStatus" reRender="thePageBlock" value="Page suivante" action="{!setCon.next}" title="Page suivante" /> 
            <apex:commandLink status="fetchStatus" reRender="thePageBlock" value="Dernière page" action="{!setCon.last}" title="Dernière page" />
                         
                  <apex:outputPanel >                      
                      <apex:actionStatus id="fetchStatus" >
                          <apex:facet name="start" >
                            <img src="/img/loading.gif" />                    
                          </apex:facet>
                      </apex:actionStatus>
                  </apex:outputPanel> 
    
              </apex:panelGrid>
                
      </apex:pageBlockSection>
    
    </apex:pageBlock>
    
  </apex:form>

</apex:page>

Comme dit dans le chapitre précédent, nous utilisons notre objet de type ApexPages.StandardSetController avec ses méthodes first, previous, next et last pour se déplacer facilement de page en page.

Sinon, rien d'exceptionnel, nous avons un formulaire pour restreindre les enregistrements affichés à l'utilisateur et la liste des enregistrements retournés après validation du formulaire.

IV-E. Rendu visuel

Nous venons de développer le contrôleur ainsi que la page Visualforce, mais à quoi peut ressembler notre page Visualforce ?

Eh bien à cela :

Image non disponible

V. Conclusion

Dans cet article, vous avez été initié à la pagination des pages VisualForce au travers des différentes possibilités qui vous sont offertes dans le CRM.

Il n'y en a pas une meilleure que les autres, cela dépend principalement de la quantité de données à afficher et de quelle manière vous souhaitez les afficher (stockées en mémoire ou non).

Pour vous aider à résumer, je vous propose ce résumé (tiré de l'article Paginating Data for Force.com Applications) :

  • Offset : à utiliser lorsque les utilisateurs ne se déplacent généralement pas après les premières pages de résultats ou lorsqu'il y a peu de résultats à afficher à l'écran de l'utilisateur ;
  • StandardSetController : implémenter cette pagination sur la majorité des applications web sur la plate-forme Force.com excepté les applications mobiles et celles dont les utilisateurs ne se déplaceront pas après la première page ;
  • API query et queryMore : bonne solution au niveau des performances lorsque vous retournez une grande quantité de données.

VI. Ressources utiles

VII. Remerciements

Je tiens à remercier tous ceux qui m'ont aidé à la réalisation de cet article :

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Aurélien Laval. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.