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

12juin/110

AIR Mobile – Application Pokémon (6) – Filtrer la liste des pokémons avec un champ de recherche

Dans le tutorial précédent, on a vu comment récupérer la donnée (liste des pokémons) depuis une base SQLite et comment l'afficher dans un composant List:

AIR Mobile – Application Pokémon (5) – Liste des Pokémon depuis la BDD SQLite

Maintenant que l'on a les bases de notre application, on va ajouter une fonction de recherche. Vous l'avez remarqué, la liste des pokémons est longue, plus de 500 éléments. On a fait un "order" sur le nom dans notre requête pour qu'ils ne soient pas mélangés mais si vous cherchez "Salameche", vous allez scroller un moment.

Si vous vous souvenez de la maquette que l'on avait fait en début du projet, vous remarquerez que l'on a un champ texte en haut de la vue permettant de rechercher un Pokémon.

Ajout du composant TextInput

Un champ de saisie est un composant nommé "TextInput" en Flex. Celui-ci devra se trouver dans la partie du haut qui est nommée ActionBar. Une petite piqûre de rappel si vous n'avez pas suivi les tutoriaux précédents:

AIR Mobile – Comprendre les composants ViewNavigator et View

Regardez surtout à la fin, le schéma expliquant le découpage naturel de l'ActionBar en 3 parties:

ActionBarDiagram

Dans notre cas, on a pas affaire à des boutons de navigation ou d'action mais à un composant que l'on veut centrer. On va donc l'afficher dans la partie "titleContent". Pour cela, ouvrez une balise XML ("<") dans votre vue PokemonInfoHomeView.mxml et tapez "titlec" puis Entrée et fermez le tag XML pour que Flash Builder vous complète la balise fermante:

...
  </fx:Declarations>
  <s:titleContent>

  </s:titleContent>
  <s:List width="100%" height="100%" dataProvider="{_dp}" labelField="nom">
...

Insérez un tag TextInput dans cette balise. Au passage, donnez lui un id égal à "input" et une width de valeur "100%". Vous avez une nouvelle propriété sur les composants de saisie de texte en Flex 4.5 qui est la propriété "prompt". Celle-ci sert à afficher une valeur lorsque le champ texte n'est pas sélectionné pour inviter l'utilisateur à taper. Donnez lui la valeur "Rechercher un Pokémon":

  <s:titleContent>
    <s:TextInput id="input" width="100%" prompt="Rechercher un Pokémon" />
  </s:titleContent>

Lancez le programme et voilà le résultat:

tin

Ajout d'une méthode de filtrage "filterFunction"

Pour effectuer notre filtrage, nous avons plusieurs solutions. La plus évidente est de faire à chaque frappe, une nouvelle requête SQL avec une clause LIKE qui nous renvoie tous les pokémons dont le nom correspond. On ne va pas adopter cette solution car dans notre cas, on a déjà requête tous les éléments de la base, qui sont conservés dans une variable nommée "_dp" de type ArrayCollection. Notre filtrage étant plutôt simple, on va utiliser un mécanisme de filtrage présent sur la classe ArrayCollection.

ArrayCollection offre un mécanisme de filtrage paramétrable grâce à sa propriété "filterFunction". Cette propriété va référencer comme son nom l'indique une "function" de votre code. Cette fonction a une signature bien particulière qui est la suivante (cf. docs):

function filterByName(item:Object):Boolean {

Elle prend un objet en entrée et renvoie un Boolean. En d'autre terme, on lui passe un élément de donnée et elle décide si cet élément doit être filtré ou non. Si la méthode renvoie "true" pour un élément, celui-ci fera partie de la liste, sinon celui-ci sera filtré.

On va tout d'abord récupérer la saisie de l'utilisateur. Celle-ci se trouve dans la propriété "text" de notre composant repéré par la propriété id="input". On va tout de suite faire un test d'échappement dans le cas où l'utilisateur n'a rien tapé:

  private function filterByName(item:Object):Boolean {
	var inputValue:String = input.text;
	if (!inputValue || inputValue == "") {
	  return true;
	}

  }

Si aucune saisie, on renvoie "true" pour laisser passer tous les éléments.

26mar/110

DataFilterLib – Le multi-filtrage par l'exemple sur droomhuiscostablanca.nl

Si vous suivez flex-tutorial depuis un moment, vous connaissez peut-être le projet DataFilterLib, une librairie que j'ai créé pour vous aider à faire du multi-filtrage sur une source de donnée comme un ArrayCollection. Un peu comme si vous pouviez avoir plusieurs filterFunction cumulatives.

Sinon, session de rattrapage:

DataFilterLib – Filtrez votre donnée de manière simple et dynamique

Le but est bien ici de vous simplifier la vie en écrivant vos filtres de manière déclarative (ou en AS si vous avez envie) avec possibilité de Binding et opérations de filtrage pré-configurées.

Il y a quelques jours, un certain Wout den Teuling est venu me demander un coup de main pour utiliser la DataFilterLib dans son application. Celle-ci reçoit des données par un script PHP, en l’occurrence une liste de résidence ayant des propriétés (région, nom, type, prix, …).
Le but était de faire un multi-filtrage sur ces propriétés et sur le résultat, de faire une pagination automatique.

Et voilà le résultat! :

http://www.droomhuiscostablanca.nl/

A gauche, vous avez le filtrage par propriété (LIKE sur le nom, emplacement, type de location, prix) et au dessus des résultats, la pagination:

datafi

Voilà, le site n'est certes pas "magnifique" (le header et le footer ne devraient pas faire partie de l'application selon moi) mais c'est un bon exemple de ce qu'il est possible de faire avec cette librairie. Elle est gratuite et open source (sur Google Code), n'hésitez pas à vous en servir !

24juil/1012

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.

29mai/100

Flex 4 – (2) Création de composants Flex ré-utilisables, les meilleures techniques

La création de composants est un must quand on travaille avec Flex. Mais autant faut-il savoir les créer de la meilleures manière afin qu'ils puissent être ré-utilisés et faciliter la maintenance de votre code.

A ce propos, Tom Sugden a écrit un très bon article qui explique bien les meilleures manières pour faire un bon composant Flex:

Writing Genuinely Reusable Flex Components

Pour faire simple, je vais traduire cet article, et rajouter quelques commentaires et suggestions…

Pour de gros projets, en entreprise, on se retrouve souvent à extraire un ensemble de composants réutilisables dans un projet Flex Library. En théorie, les mêmes composants peuvent être utilisés dans plusieurs modules et/ou dans des applications Flex / Air. Cela permet d'avoir une base de code solide et facile à maintenir pour un développement plus rapide.

Cependant, dans la pratique, certaines erreurs de conception sont faites, qui limitent la ré-utilisabilité des composants. Cet article explique comment créer un composant totalement ré-utilisable et met en avant certaines techniques du SDK Flex qui peuvent être appliquées à vos propres composants.

Comment définir un composant "vraiment" réutilisable?

Il y a différents niveaux de ré-utilisabilité, mais un composant ré-utilisable devrait toujours pouvoir faire le rendu de n'importe quel type de donnée. Il devrait pouvoir fonctionner avec un tableau d'objets basiques (type Object) ou une collection d'objet de type Kangaroo par exemple.

Le composant Flex DataGrid permet de faire cela:

<mx:DataGrid dataProvider="{ kangaroos }">
 <mx:columns>
 <mx:DataGridColumn headerText="Name" dataField="name"/>
 <mx:DataGridColumn headerText="Weight" labelFunction="calculateWeight"/>
 </mx:columns>
</mx:DataGrid>

Notez que les propriétés "dataField" et "labelFunction" indiquent au composant comment récupérer sa donnée depuis les objets de type Kangaroo, sans imposer de dépendance. On a ici deux mécanismes possibles pour rendre un composant vraiment ré-utilisable. Même si le développeur n'as pas le contrôle sur la classe Kangaroo (si elle fait partie d'une librairie externe par exemple), il peuvent tout de même être rendu dans une DataGrid.

L'Anti-Pattern Data Interface

Une des erreurs courantes est d'obliger la donnée qui va être rendue par un composant à implémenter une interface spécifique. Par exemple, on va prendre le composant DistributionBar qui fait le rendu d'un graphique comme celui-ci:

DistributionBar

On affiche un certain nombre de régions de tailles différentes, chacune contenant un label. Il est tentant de configurer ce composant pour qu'il utilise un tableau d'objet IRegion:

public interface IRegion
{
 function get label() : String;
 function get size() : int;
}

Le composant pourra ensuite extraire les informations de taille et de label pour chaque région par cette interface. L'argument en faveur de cette démarche est que l'interface découple le composant de l'objet concret qui est rendu. N'importe quel objet peut être rendu tant qu'il implémente IRegion, mais c'est dans le "tant que" que réside la faille de conception.

En imposant l'utilisation de l'interface IRegion, la ré-utilisabilité du composant est limitée. L'interface doit être implémentée par les classes du Model avant de pouvoir être rendue par le composant. Encore pire, si vos models sont produits par une autre librairie (dont vous pouvez ne pas avoir les sources) ou par une autre équipe, il se peut que vous ne puissiez pas changer les classes, elles devront donc être "wrappées" (encapsulées dans d'autres objets qui implémentent IRegion).

Pour ces raisons, le composant n'est pas vraiment ré-utilisable.

Notez que cette considération est importante pour les composants qui doivent être complètement ré-utilisables comme les DataGrid ou les List Flex. Pour des composants plus "métiers" ou le fait de pouvoir instancier son composant sans configurer trop de propriétés, on pourra utiliser le "pattern Data Interface". Parfois, il vaut mieux passer du temps à créer un composant qui soit ré-utilisable mais parfois il est plus aisé/rapide de créer plusieurs composants (par héritage) pour des besoins métiers spécifiques.

Les composants ré-utilisables du SDK Flex

Le SDK Flex contient de nombreux composants ré-utilisables, utilisant plusieurs approches:

  1. Data Fields
  2. Data Functions
  3. Data Descriptor
  4. Factory Objects

Ces approches et ces techniques peuvent être appliquées à vos propres composants pour les rendre ré-utilisables.

4nov/0915

Composant Flex – Interface de Pagination générique (exemple)

Il y a quelques semaines, je présentais sur Adobe Flex Tutorial un composant Flex nommé Paginator qui était une interface de pagination générique pour vos applications. Pour rappel, la pagination sert à présenter la donnée par bouts (pages) afin de ne pas noyer l'utilisateur. Dans un commentaire, CapoeiraDance demandait à voir un exemple d'application de ce composant. Voici donc un exemple que j'ai concocté moi-même.

Dans cet exemple, on a une donnée sur les états américains avec quelques valeurs. J'ai choisi de n'afficher que 8 éléments pas page pour montrer le fonctionnement de l'interface de pagination. La vraie pagination (au niveau de la data) se fait par une "filterFunction" sur le dataProvider (ArrayCollection). Ainsi, on va regarder l'index de l'item dans la donnée pour savoir s'il est dans la page courante. Le code n'est sûrement pas optimal mais cela donne une idée.

Attention, pour indiquer le nombre total d'éléments au paginator, on utilise bien la propriété "length" de la "source" de l'ArrayCollection et pas la "length" de l'ArrayCollection. En effet, si on utilise directement statesData.length, les résultats seront erronés car la fonction de filtre va modifier cette longueur.

J'aurai pu être chauvin et utiliser la DataFilterLib pour créer cette pagination mais gardons les choses simples pour l'instant :) .

Un peu de code pour commencer

 [Bindable]
  private var statesData:ArrayCollection = new ArrayCollection([...]);

  ...

  // variables utilisées par la fonction de filtre
  private var _startIdx:int = 0;
  private var _endIdx:int = 1000;

  private function init():void{
	// on donne une fonction de filtre à la donnée pour n'afficher que les résultats courants
	statesData.filterFunction = paginateFilterFunction;
	statesData.refresh();
  }

  private function pageChangeHandler(e:PaginateEvent):void{
	// affichage du nombre de résultats courant
	var page:int = e.index;

	var startIndex:int = (page * e.itemsPerPage) + 1;
	var endIndex:int = Math.min((startIndex + e.itemsPerPage -1), e.itemsTotal);
	_startIdx = startIndex - 1;
	_endIdx = endIndex;

	txt.text = "Results: " + String(startIndex) + " - " + String(endIndex) + " of " + e.itemsTotal;
	// filtrage de la donnée par page
	statesData.refresh();
  }

  private function paginateFilterFunction(item:Object):Boolean{
	var itemIdx:int = statesData.getItemIndex(item);
	return (itemIdx >= _startIdx && itemIdx < _endIdx) || itemIdx == statesData.source.length;
  }

  ...

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

L'application en ligne

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

This movie requires Flash Player 11