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 :
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 :
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 :
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 :
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 :
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) :
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▲
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▲
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 :
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 :