Apache Adobe Flex TutorialTutoriaux Adobe Flex & AIR en Français

24juil/1014

DataFilterLib – Pagination de données filtrées (ArrayCollection avec filterFunction)

On m'a posé une question très intéressante sur la DataFilterLib:

Cedric
Bonjour moi j'aurais une petite question :
Comment intégrer les fonctionnalités de filtrage sur une dataGrid dont les données sont paginées (à l'aide du paginateur disponible sur ce site)?

En effet, la DataFilterLib permet de filtrer des données côté client selon plusieurs critères, de manière déclarative (MXML) ou dynamique (AS). Elle permet, pour simplifier, de donner plusieurs filterFunction à un ArrayCollection.

Et pour la pagination (côté client), le plus simple et le plus effectif est d'utiliser aussi une filterFunction sur une ICollectionView, un ArrayCollection par exemple. La question est alors, comment combiner les deux?

Application de test

Voilà le code utilisé pour tester l'application. Le dataProvider se trouve dans un fichier à part pour faciliter la lecture:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%"
                paddingRight="20" xmlns:filter="com.fnicollet.datafilter.filter.*"
                xmlns:pagination="pagination.*">
  <mx:Script>
    <![CDATA[
      import pagination.event.PaginateEvent;
      import com.fnicollet.datafilter.filter.DataFilterType;
      import com.fnicollet.datafilter.filter.DataFilterSingleValueOperator;

      [Bindable]
      private var _startIdx:int = 0;
      [Bindable]
      private var _endIdx:int = 999999999;

      private function pageChangeHandler(e:PaginateEvent):void {
        var page:int = e.index;
        _startIdx = (page * e.itemsPerPage) + 1;
        _endIdx = Math.min((_startIdx + e.itemsPerPage - 1), e.itemsTotal);
        //txt.text = "Results: " + String(_startIdx) + " - " + String(_endIdx) + " of " + e.itemsTotal;
      }
    ]]>
  </mx:Script>

  <mx:Script source="/data/StateData.as"/>
  <filter:DataFilterSet id="filterSet" data="{statesData}">
    <filter:dataFilterParameters>
      <filter:DataFilterParameters id="simpleParam" filterType="{DataFilterType.SINGLE_VALUE}"
                                   filterKeys="state"
                                   filterOperator="{DataFilterSingleValueOperator.LIKE}"
                                   filterValues="{stateInput.text}"/>
    </filter:dataFilterParameters>
  </filter:DataFilterSet>

  <mx:Label text="Filter By State Name (Contains)" fontSize="14" fontWeight="bold"/>
  <mx:TextInput id="stateInput"/>
  <mx:Label text="Pagination over the filtered elements" fontSize="14" fontWeight="bold"/>
  <pagination:PageSelector id="paginator" itemsPerPage="6" rangeCount="6"
                           itemsTotal="{statesData.length}" selectedIndex="0"
                           pageChange="pageChangeHandler(event)"/>
  <mx:Label text="Filtered Data" fontSize="14" fontWeight="bold"/>
  <mx:DataGrid rowCount="7" dataProvider="{statesData}" width="100%">
    <mx:columns>
      <mx:DataGridColumn dataField="state" headerText="State Name"/>
      <mx:DataGridColumn dataField="sales" headerText="Sales"/>
      <mx:DataGridColumn dataField="employees" headerText="Number of Employees"/>
      <mx:DataGridColumn dataField="population" headerText="Population"/>
    </mx:columns>
  </mx:DataGrid>

  <mx:Label text="Unfiltered Data" fontSize="14" fontWeight="bold"/>
  <mx:DataGrid rowCount="8" dataProvider="{statesData.source}" width="100%">
    <mx:columns>
      <mx:DataGridColumn dataField="state" headerText="State Name"/>
      <mx:DataGridColumn dataField="sales" headerText="Sales"/>
      <mx:DataGridColumn dataField="employees" headerText="Number of Employees"/>
      <mx:DataGridColumn dataField="population" headerText="Population"/>
    </mx:columns>
  </mx:DataGrid>

</mx:Application>

Un composant générique de pagination est intégré et met seulement à jour 2 variables Bindable. Il n'effectue encore aucun filtrage:

Composant Flex – Interface de Pagination générique

Premier test: Ajouter un nouveau filtre pour la pagination

Dans un premier temps, la première idée que j'ai eu fut de simplement ajouter un nouveau filtre, se comportant comme un filtre de type Interval, dont l'intervalle irait entre l'index de mon premier élément de page et le dernier élément.

Pour créer ce nouveau filtre, qui n'existe pas par défaut, il suffit de créer une classe qui hérite de "DataFilterInterval": DataFilterIntervalPagination. Pour l'ajouter en tant que filtre, il suffit d'utiliser la technique que j'expose dans cet article:

DataFilterLib – Utilisation de filtres personnalisés

Pour une utilisation dans une application, il suffit ensuite de donner:

<filter:DataFilterParameters filterClass="{DataFilterIntervalPagination}"
 id="intervalPagination" filterValues="{[_startIdx, _endIdx]}"/>

Et déjà, là, premier obstacle, la méthode de filtrage de base. Pour simplifier, la DataFilterLib boucle sur la méthode "apply" de chaque filtre dont la signature est la suiavnte:

public function apply(item:Object):Boolean {

On prend ensuite chaque résultat et si au moins un d'entre eux est false, l'objet est filtré. Pour les filtres sur valeur et sur intervalle, on cherche à comparer une certaine propriété de l'item (donnée par le paramètre "filterKeys"). En pseudo-code, on a :

public function apply(item:Object):Boolean {
 var value:String = item[filterKey];
 if (value == filterValue){
  return true;
 }
 return false;
}

On fait ici la distinction par rapport à une propriété de l'objet. Seulement dans notre cas, on ne veut pas comparer une des propriétés de l'objet mais bien la position de l'objet dans le dataProvider. Si son index est dans la page courante on garde l'objet, sinon on le filtre.

Le problème devient ici que l'on ne connait pas le dataProvider dans l'objet contenant la méthode "apply". Mais bon, comme on est encore en expérimentation, on peut se permettre de faire (Attention, uniquement pour tester, ne jamais laisser du code comme cela :P ):

    override public function apply(item:Object):Boolean {
      var _dataProvider:ArrayCollection = Application.application.statesData;
      if (!_dataProvider) {
        return true;
      }
      var itemIdx:int = _dataProvider.getItemIndex(item);
      if (itemIdx == -1) {
        return false;
      }
      var result:Boolean = (itemIdx >= min && itemIdx <= max);
      return applyConstraints(result);
    }

Le résultat obtenu est assez étrange mais peut être expliqué si on regarde bien ce qui se passe. On obtient bien une page de résultat mais l'interface de pagination a disparu car elle a déterminé que l'on avait une seule page (donc aucun besoin d'afficher les autres pages).

Si on regarde le code du composant de pagination, on a :

  <pagination:PageSelector id="paginator" itemsPerPage="6" rangeCount="6"
                           itemsTotal="{statesData.length}" selectedIndex="0"
                           pageChange="pageChangeHandler(event)"/>

Le problème vient en fait du itemsTotal="{statesData.length}". Lorsque notre ArrayCollection est filtrée, la "length" renvoyée par celle-ci est celle qui est filtrée, dans notre cas, la taille d'une page. Dans tous les cas, on aura une seule page, quoi qu'il arrive.

Pour conclure, le problème vient du fait qu'on a ajouté un filtre pour la pagination. Il faut en fait que la pagination filtre la donnée déjà filtrée mais qu'elle n'altère pas celle-ci.

Deuxième test: Réaliser une copie de la donnée

On vient de le voir, la première méthode échoue car la pagination doit filtrer la donnée pre-filtrée sans altérer la donnée de base. On va donc travailler sur une copie de la donnée filtrée.

Comme la donnée est filtrée une première fois de manière dynamique (à chaque frappe de l'utilisateur), il faut que la copie soit elle aussi mise à jour. Pour cela, on va détecter les changements sur la données de base grâce à l'évènement CollectionEvent.COLLECTION_CHANGE et réaliser une copie:

[Bindable]
  public var statesDataCopy:ArrayCollection = null;

  private function onCreationComplete():void {
    statesData.addEventListener(CollectionEvent.COLLECTION_CHANGE, onCollectionChange);
    copyCollection();
  }

  private function onCollectionChange(event:CollectionEvent):void {
    copyCollection();
  }

  private function copyCollection():void {
    var arrCopy:Array = ObjectUtil.copy(statesData.toArray()) as Array;
    statesDataCopy = new ArrayCollection(arrCopy);
    statesDataCopy.refresh();
  }

Comme on l'a vu plus haut, on peut réaliser la pagination avec un simple filtre hérite de la DataFilterLib alors autant l'utiliser à fond. On va donc créer un deuxième DataFilterSet (puisqu'il y a un deuxième filtrage):

  <filter:DataFilterSet id="filterSet2" data="{statesDataCopy}">
    <filter:dataFilterParameters>
      <filter:DataFilterParameters filterClass="{DataFilterIntervalPagination}"
                                   id="intervalPagination" filterValues="{[_startIdx, _endIdx]}"/>
    </filter:dataFilterParameters>
  </filter:DataFilterSet>

Pour l'affichage final, on va prendre la copie "statesDataCopy":

<mx:DataGrid rowCount="7" dataProvider="{statesDataCopy}" width="100%">

Et cela fonctionne! On a bien la pagination sur les éléments filtrés comme prévu.

On fait le ménage avant de partir

Ne jamais crier victoire trop rapidement, on a vu un peu plus haut qu'on a laissé un raccourci un peu trop facile dans le code. Celui-ci allait chercher le dataProvider directement dans l'application, créant un couplage inutile:

override public function apply(item:Object):Boolean {
      var _dataProvider:ArrayCollection = Application.application.statesData;
...

Pour résoudre ce problème, on va encore utiliser notre ami l'héritage. Comme l'objet DataFilterSet connait à la fois le dataProvider et ses DataFilter, autant en profiter. On va donc créer une classe DataFilterSetPagination héritant de DataFilterSet. Dans cette classe, on surcharge la méthode set data(value:ArrayCollection):void pour injecter le dataProvider dans nos filtres:

package com.fnicollet.datafilter.filter {
  import mx.collections.ArrayCollection;

  public class DataFilterSetPagination extends DataFilterSet {

    public function DataFilterSetPagination(__data:ArrayCollection = null) {
      super(__data);
    }

    override public function set data(value:ArrayCollection):void {
      super.data = value;
      var dataFilters:Array /* of IDataFilter */ = getDataFilters();
      for each (var dataFilter:IDataFilter in dataFilters) {
        // injection du dataProvider
        if (dataFilter is DataFilterIntervalPagination) {
          DataFilterIntervalPagination(dataFilter).dataProvider = value;
        }
      }
    }
  }
}

Voilà, notre code est maintenant propre.

Application en ligne

Voici l'application exemple que j'ai utilisé avec les sources:

Flex Source Code Download: Télécharger le code source complet de l'application

This movie requires Flash Player 11

Articles similaires

Commentaires (14) Trackbacks (0)
  1. Merci beaucoup pour ton tuto ! Cependant, j'ai un petit soucis. Lorsque je veux changer le programme flex, je me retrouve avec une erreur dans le DataFilterSetPagination. Il me dit qu'il ne trouve pas la définition de la classe DetaFilterSet.

    Est-ce que tu aurais une idée d'ou ca pourrait venir ?

    Merci beaucoup et encore merci de ton aide pour le tree et le datagrid :)

  2. Salut,
    cela veut dire que tu n'a pas importé la classe (instruction "import" au début de ton fichier) ou qu'il n'a pas trouvé la référence. Dans le deuxième cas, cela signifie que tu n'as pas ajouté la DataFilterLib en référence de ton projet

    Fabien

  3. Ah non, je n'ai pas rajouté de .swc. Ca doit être ça alors. Je te tiens au courant. Merci beaucoup de ton aide.

  4. Alors, j'ai une erreur au lancement. Mais au moins, ce problème est résolu. ^^

    Merci encore de ton aide Fabien et bon courage pour ton site qui est vraiment excellent ! Merci encore.

  5. Quelle erreur?

  6. Ca me met VerifyError : Errir #1053: Remplacement Illégal de FocusManager dans mx.managers.FocusManager.

    Merci beaucoup du temps que tu prends. Tu voudrais pas enseigner le flex ? :)

  7. Hum, ça sent le conflit entre les versions de SDK. J'aurais du compiler ma librairie avec le SDK en External (j'utilisais le SDK Flex 3.2 à l'époque). Ce que tu peux faire, est de prendre le projet directement (les sources) avec SVN comme ça, tu auras tout sous la main, dans la bonne version.
    Si tu as la flemme, je te compile une version de la lib avec le SDK en external (en espérant que ton problème vienne de là)

    Fabien

  8. Oui, je suis sur un Ubuntu avec un flex 3.5.
    Ok, pas de soucis, j'essaie ça dès demain.

    Merci beaucoup de ton aide !

  9. Hello, thank you for the code but I get 2 errors when compiling it:

    DataFilterSetPagination.as
    - The definition of base class DataFilterSet was not found;
    - Method marked override must override another method.

    If you could help it would be great, thanks!

  10. Hi,
    It's pretty strange, it seems like there is something wrong in that release. Try using a previous release or checkout the code and locate the override instruction that doesnt override anything
    Fabien

  11. Hello Fabien,
    Is it possible to use the datafilterlib and pager with data filled from external server? I'm using zend and amf to remote the source from the mysql database.
    Thanks anyway.

  12. Hey,
    it is possible to use the datafilterlib with any ArrayCollection object, it doesn't really matter that it comes from a remote source or a local one. In the end, you still use an ArrayCollection as your data.
    To use both pagination and filtering, you have to have the right logic. I wrote an article about it with example here:
    http://www.flex-tutorial.fr/2010/07/24/datafilterlib-pagination-de-donnees-filtrees-arraycollection-avec-filterfunction/
    If you don't speak french you can use BabelFish to translate the page (be careful that it doesn't try to translate the code as well.

    Fabien

  13. Salut Fabien,

    j'ai un problème avec la combinaison de la dataFilterLib et du paginator et dans ta démo y a le même : c'est le tri par colonne qui fait vraiment n'importe quoi. As tu remarqué et si oui as tu une solution ?

  14. Salut,
    Effectivement, c'est assez étrange, je n'avais pas vu ce bug. Il faudrait passer en debug dans la méthode filterFunction / sort / refresh pour voir ce qu'il se passe vraiment

    Fabien


Leave a comment

(required)

Aucun trackbacks pour l'instant