lundi 25 août 2008

ExtJS : no more OpenSource for commercial projects

Since ExtJS has changed it's licencing terms, we are not really keeping our efforts to bind ExtJS to Seam. We will turn our efforts to other platforms like GWT or Flex for our future web 2.0 developments...

Sorry for you folks who keep trying to use ExtJS. I can try to rebundle the work we did and put it to dowload again (link has been deleted).

lundi 11 février 2008

Seam : a way to handle server-side exceptions

In this article, we'll see how one can handle server-side exceptions using seam remoting.

Because Seam Remoting doesn't handle gracefully those exceptions (see http://jira.jboss.org/jira/browse/JBSEAM-633), we must manage those exceptions and be able to show the user a comprehensive error message.

For my current ExtJS + Seam project, I'm using the following pattern :

- a business SessionBean which do the actual business job (open a transaction, do the persistence work, etc)



package xxx.yyy.zzz;

import java.util.List;
import javax.ejb.Local;
import xxx.yyy.zzz.Chauffeur;

/**
* @author cvigouroux
*
*/

@Local
public interface HelloAction
{
/**
* @param nouveauPays
* @return Le pays mis à jour
*/

public Chauffeur modifierChauffeur(Chauffeur nouveauChauffeur);
}



package xxx.yyy.zzz;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.TransactionPropagationType;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.log.Log;

import xxx.yyy.zzz.Chauffeur;

/**
* @author cvigouroux
*
*/

@Stateless
@Name("faucon80_cartographie_helloAction")
@SuppressWarnings("unchecked")
public class HelloActionBean implements HelloAction
{
@Logger
private Log log;

@In
private EntityManager entityManager;

/*
* (non-Javadoc)
*
* @see com.elomobile.faucon80.cartographie.session.metier.HelloAction#modifierChauffeur(com.elomobile.faucon80.cartographie.entity.Chauffeur)
*/

@Transactional(TransactionPropagationType.REQUIRED)
public Chauffeur modifierChauffeur(Chauffeur nouveauChauffeur)
{
// mise à jour de la date
// nouveauChauffeur.setDtheumod(new Date());

// mise à jour de la base de données
entityManager.merge(nouveauChauffeur);
entityManager.flush();

return nouveauChauffeur;
}
}



- an adapter for that SessionBean (using adapter design pattern) in the form of another SessionBean whose job is to call the business and to handle the exceptions. The exception is then redirected to the client in the form of a message with a criticity (success, information, error). In order to ligthen the exception handling code within each service method, I use an interceptor for doing the job.


package xxx.yyy.zzz;

import java.io.InputStream;
import java.util.List;

import javax.ejb.Local;

import org.jboss.seam.annotations.remoting.WebRemote;

import xxx.yyy.zzz.Chauffeur;
import xxx.yyy.zzz.web.messages.BasicPopupMessage;
import xxx.yyy.zzz.web.paging.PagingQueryParam;
import xxx.yyy.zzz.web.paging.PagingResultList;
import xxx.yyy.zzz.web.paging.QueryParam;
import xxx.yyy.zzz.web.paging.ResultList;

/**
* @author cvigouroux
*
*/

@Local
public interface HelloWebAction
{
/**
* @param nouveauPays
* @return Le pays mis à jour
* @throws Exception
*/

@WebRemote
public BasicPopupMessage modifierChauffeur(Chauffeur nouveauChauffeur) throws Exception;

/**
* Méthode destroy...
*/

public void destroy();

}



package xxx.yyy.zzz.session.web;

import java.io.InputStream;
import java.rmi.RemoteException;
import java.util.List;
import java.util.Map;

import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.interceptor.Interceptors;
import javax.xml.rpc.ServiceException;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.*;
import org.jboss.seam.log.Log;

import xxx.yyy.zzz.Identity;
import xxx.yyy.zzz.Chauffeur;
import xxx.yyy.zzz.session.metier.HelloAction;
import xxx.yyy.zzz.web.messages.BasicPopupMessage;
import xxx.yyy.zzz.web.paging.PagingQueryParam;
import xxx.yyy.zzz.web.paging.PagingResultList;
import xxx.yyy.zzz.web.paging.QueryParam;
import xxx.yyy.zzz.web.paging.ResultList;

/**
* @author cvigouroux
*
*/

@Stateful
@Scope(ScopeType.SESSION)
@Name("faucon80_cartographie_helloWebAction")
public class HelloWebActionBean implements HelloWebAction
{
@Logger
private Log log;

@In(create = true, value = "helloAction")
private HelloAction helloAction;

@In
private Map<String, String> messages;

/*
* (non-Javadoc)
*
* @see com.alteca.seam.prototype.session.web.HelloWebAction#updatePays(com.alteca.seam.prototype.entity.Tpays)
*/

@Interceptors(InterceptorBasicPopupMessageMethod.class)
public BasicPopupMessage modifierChauffeur(Chauffeur nouveauChauffeur) throws Exception
{
Identity.instance().checkRestriction("#{org.jboss.seam.security.identity.hasPermission('helloAction')}");

Chauffeur chauffeurMaj = helloAction.modifierChauffeur(nouveauChauffeur);

return new BasicPopupMessage(BasicPopupMessage.Criticite.INFO, messages
.get("helloAction.modifierChauffeur.succes"), messages.get("bouton.ok"), chauffeurMaj);
}
}




package xxx.yyy.zzz.session.web;

import java.util.Map;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.AuthorizationException;

import xxx.yyy.zzz.web.messages.BasicPopupMessage;

@Name("interceptorBasicPopupMessageMethod")
public class InterceptorBasicPopupMessageMethod
{
@Logger
private static Log log;

@AroundInvoke
public Object interceptGenericExceptions(InvocationContext invocation) throws Exception
{
Object o = invocation.getParameters()[0];

Map<String, String> messages = org.jboss.seam.international.Messages.instance();

try
{
return invocation.proceed();
}
catch (AuthorizationException ae)
{
return new BasicPopupMessage(BasicPopupMessage.Criticite.ERROR, messages.get("erreur.nonAuthorise"), messages.get("bouton.ok"), o);
}
catch (Exception e)
{
log.error("", e);
return new BasicPopupMessage(BasicPopupMessage.Criticite.ERROR, messages.get("erreur.generique") + e.getMessage(), messages.get("bouton.ok"), o);
}
}
}



On the javascript side, I use a standard callback function which intercepts the returned message and shows a message box with the transmitted message string.


/**
*
* @param {Ext.data.Store} store The store to which belong the record.
* @param {Ext.data.Record} record The record to be updated server-side.
* @param {String} formName The name of the form to update with the server-side updated bean.
* @param {function} callbackFunction The function to call after handling.
*/

Ext.data.SeamRemotingRecordHandler = function(store, record, formName, callbackFunction)
{
this.callback = function(reponse)
{
if (reponse.criticite != 'ERROR')
{
Ext.getCmp(formName).getForm().updateRecord(record);

// get new data from server
record.data = reponse.data;
record.endEdit();

// tell the store to refresh
store.commitChanges();

// loads the new record in the form
Ext.getCmp(formName).getForm().loadRecord(record);
}

if (reponse.criticite != 'SUCCES')
{
// show an information or an error message
Ext.Msg.show(
{
minWidth: 200,
closable: false,
msg: reponse.message,
buttons:
{
ok: reponse.messageBouton
},
icon: Ext.MessageBox[reponse.criticite]
});
}

// call client's callback function
callbackFunction();
}
}


Example of use :


var handler = new Ext.data.SeamRemotingRecordHandler(store, recordAModifier, "tpays-form", function()
{
});

helloWebAction.modifierChauffeur(copieRecord.data, handler.callback);

Seam ExtJS Extension, updated code

Being used in developpement stage of our current projet, my extension keeps to be updated.

Changes since first version :
- optimized code : the field definition of the records are only parsed once. It was parsed for every returned row in the last version
- added static Ext.data.BeanRecord.createFromBean(bean) function that creates a record directly from a bean (usefull for example in order to populate a form via form.loadRecord(beanRecord)).


Future version will include a proxy Store using a modified "PagingMemoryProxy" implementation in order to be able to define a store able to do local paging from a defined SeamRemotingStore. The proxy is already prototyped but needs to be package in some smart form of a store encapsulation at once the paging store and the proxied SeamRemotingStore. This kind of store can be VERY usefull in case of a SeamRemotingStore having 1000+ rows. The grid cannot handle so much data (moving, sorting, rendering in general veryyyy slow), so we needed to add a pager to it (without compromising the needed functionnality to have all rows loaded on client-side !).

Download complete file here.

Seam + ExtJS, Java glue code

In order to use my ExtJS extensions, we need some Java glue code like the generic method parameters and return values.

So, let's begin with the generic method parameters :

- for a simple query method :

package xxx.yyy.zzz;
import org.jboss.seam.annotations.Name;

@Name("queryParam")
public class QueryParam
{
private String queryString;

public QueryParam()
{
super();
}

public String getQueryString()
{
return queryString;
}

public void setQueryString(String queryString)
{
this.queryString = queryString;
}

}

- for a simple paging query method :


package xxx.yyy.zzz;

import org.jboss.seam.annotations.Name;

@Name("pagingQueryParam")
public class PagingQueryParam extends QueryParam
{
private int start;
private int limit;


public int getStart()
{
return start;
}

public void setStart(int start)
{
this.start = start;
}

public int getLimit()
{
return limit;
}

public void setLimit(int limit)
{
this.limit = limit;
}
}




Now for the return values :
- for a simple query method :
package xxx.yyy.zzz;

import java.util.List;

import org.jboss.seam.annotations.Name;

/**
*
* @author cvigouroux
*
* @param <T>
*/

@Name("resultList")
public class ResultList<T>
{
private List<T> list;
private int totalRecords;

public ResultList(List<T> list)
{
this.list = list;
this.totalRecords = list.size();
}

public int getTotalRecords()
{
return totalRecords;
}

public void setTotalRecords(int totalRecords)
{
this.totalRecords = totalRecords;
}

public List<T> getList()
{
return list;
}

public void setList(List<T> list)
{
this.list = list;
}
}

- for a simple paging query method :

package xxx.yyy.zzz;

import java.util.List;

import org.jboss.seam.annotations.Name;

/**
* @author cvigouroux
*
* @param <T>
*/

@Name("pagingResultList")
public class PagingResultList<T> extends ResultList<T>
{
// private int totalRecords;
// private List<T> list;

public PagingResultList(int totalRecords, List<T> list)
{
super(list);
this.setTotalRecords(totalRecords);
}
}

vendredi 18 janvier 2008

Seam + ExtJS, i18n

How to internationalise a Seam + ExtJS application ?

I chose the following solution :
- for server-side generated messages and jsf labels, I need one or more resource bundles (properties file) for each langage

Example :

<messages_en.properties>

org.jboss.seam.loginFailed=Login failed
org.jboss.seam.loginSuccessful=Welcome, #0


<messages_fr.properties>

org.jboss.seam.loginFailed=Echec de la tentative de login
org.jboss.seam.loginSuccessful=Bienvenue, #0


In my Eclipse environment, I set content encoding of these files to ISO-8859-1

Now they can be used within the Seam framework scope (JSF, Pojos, EJBs, and so on). Nothing more to say that is explicitly documented in the Seam documentation.

- now for the client-side messages and labels, I create one javascript object type and an instance of it in a javascript file for each specific langage versions like this one :

<application-lang-en.js>

ApplicationLang = function()
{
// general dictionnary
this.codeIso = "ISO Code";

// helloAction labels and messages
this.helloAction_titreGrille = "Drivers list";
this.helloAction_titreFormulaire = "Driver details";
}

var lang = new ApplicationLang();


<application-lang-fr.js>

ApplicationLang = function()
{
// dictionnaire général
this.codeIso = "Code ISO";

// helloAction
this.helloAction_titreGrille = "Liste des chauffeurs";
this.helloAction_titreFormulaire = "Détail du chauffeur";
}

var lang = new ApplicationLang();


The lang variable can then be used in ExtJS object declarations :

var countryFieldSet = new Ext.form.FieldSet(
{
id: 'formulaireFields',
columnWidth: 0.3,
labelWidth: 50,
title: lang.helloAction_titreFormulaire,
...
}



To use the right javascript langage file, add these lines to the xhtml file :


<script type="text/javascript" src="ext/locale/ext-lang-#{localeSelector.localeString}.js"></script>
<script type="text/javascript" src="ext/locale/application-lang-#{localeSelector.localeString}.js"></script>


And to tell Seam which langages are supported, modify faces-config.xml :


<application>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>fr</supported-locale>
</locale-config>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>




Finally, I set the carset encoding for xhtml and javascript files to UTF-8. To be sure that the browser knows that my javascript files are UTF-8 I just had these lines to web.xml :




<mime-mapping>
<extension>js</extension>
<mime-type>text/javascript;charset=UTF-8</mime-type>
</mime-mapping>

Seam + ExtJS, Store, Reader, Proxy

Passons maintenant à la pratique. Je commencerai pas exposer les différentes classes côté ExtJS qui permettent de dialoguer avec Seam Remoting. Pour cela, je suis parti de l'exemple d'extension pour DWR proposé par Axel ici : http://extjs.com/forum/showthread.php?t=19529

Now it's time for some code examples. I'm begining to expose the different ExtJS extensions classes which permit to dialog with Seam Remoting. For this purpose, I started with the example extension for DWR proposed by Axel in the ExtJS User Extensions forum : http://extjs.com/forum/showthread.php?t=19529


/**
* Seam remoting proxy.
*
* Can be bound to any WebMethod which signature is like :
* - ResultList<T> webMethod(QueryParam param) for a non paging method.
* - PagingResultList<T> webMethod(PagingQueryParam param) for a paging method.
*
* @param {function} f Points to remote function.
* @param {Object} o Seam component.
* @param {boolean} paging Tells wether the remote method uses paging.
*/

Ext.data.SeamRemotingProxy = function(f, o, paging)
{
Ext.data.SeamRemotingProxy.superclass.constructor.call(this);
this.func = f;
this.instance = o;
this.paging = paging;
};

Ext.extend(Ext.data.SeamRemotingProxy, Ext.data.DataProxy,
{
/**
* Builds a query parameter object (queryParam or pagingQueryParam
* if paging is used by the remote method).
*
* Calls the seam remote method by passing to it the query parameter
* objet and the callback function.
*
* @param {Object} params Object with following fields defined : queryString,
* and if paging enabled, with following more fields : start, limit.
* @param {Object} reader
* @param {Object} loadCallback
* @param {Object} scope
* @param {Object} arg
* @param {Object} remoteArgs
*/

load: function(params, reader, loadCallback, scope, arg, remoteArgs)
{
var dataProxy = this;
dataProxy.fireEvent("beforeload", dataProxy, params);

var args = [];

// Wether the remote method method support paging or not,
// choose the right parameter class.
var queryParamComponentName;
if (this.paging)
{
queryParamComponentName = "pagingQueryParam";
}
else
{
queryParamComponentName = "queryParam";
}

var queryParam = Seam.Component.newInstance(queryParamComponentName);

//
for (var param in params)
{
queryParam[param] = params[param];
}

args[args.length] = queryParam;

args[args.length] = function(response)
{
dataProxy.fireEvent("load", dataProxy, response, loadCallback);
var records = reader.read(response);

if (records.records.length > 0)
{
scope.fields = records.records[0].fields;
}

loadCallback.call(scope, records, arg, true);
}

this.func.apply(this.instance, args);
}
});

/**
* Extension of Ext.data.Record to handle bean data.
* Extends record.get(name) in order to be able to access
* properties of underlying objects like bean.child.property.
*
* @param {Object} data
* @param {Object} id
*/

Ext.data.BeanRecord = function(data, id)
{
Ext.data.BeanRecord.superclass.constructor.call(this, data, id);
};

Ext.extend(Ext.data.BeanRecord, Ext.data.Record, {});

Ext.data.BeanRecord.create = function(o)
{
var f = Ext.data.Record.create.call(this, o);

/**
*
* @param {String} name
*/

f.prototype.get = function(name)
{
var names = name.split('.', 2);

if (names.length > 1)
{
var o = this.data[names[0]];

if (o)
{
var result = eval("o." + names[1]);
return result;
}
else
{
return "";
}
}
else
{
return this.data[name];
}
}

/**
* Copies a record and its underlying seam data bean.
* @param {String} componentName The seam component name of the underlying bean.
*/

f.prototype.copyRecord = function(componentName)
{
var copieRecord = this.copy();
var copieData = copyBean(this.data, componentName);
copieRecord.data = copieData;

return copieRecord;
}

return f;
};


/**
* Extends Ext.data.DataReader in order to read bean data.
*/

Ext.data.BeanReader = function()
{
Ext.data.BeanReader.superclass.constructor.call(this, null, []);
};

Ext.extend(Ext.data.BeanReader, Ext.data.DataReader,
{
read: function(response)
{

var records = [];
var bean;

for (var i = 0; i < response.list.length; i++)
{
bean = response.list[i];

// retreives the bean field names
var fields = [];
for (var prop in bean)
{
if ((new String(bean[prop])).substring(0, 8) != "function")
{
fields[fields.length] = prop;
}
}

// builds dynamically the record definition
var recordDefinition = "[";

var field;
for (var j = 0; j < fields.length; j++)
{
field = fields[j];
recordDefinition = recordDefinition + "{name: '" + field + "'}";
if (j < fields.length - 1)
{
recordDefinition = recordDefinition + ",";
}
}
recordDefinition = recordDefinition + "]";

var ObjectRecord = eval("Ext.data.BeanRecord.create(" + recordDefinition + ")");

// record instantiation
var myNewRecord = new ObjectRecord();

// record data is the bean
myNewRecord.data = bean;

records[records.length] = myNewRecord;
}
return {
records: records,
totalRecords: response.totalRecords
};
}
});

/**
* [en]
* Added configuration parameters :
* - remoteComponent (Object) : Seam component resulting from a call to
* Seam.Component.getInstance(name)
* - remoteComponentName (String) : Seam component name to remote-access.
* This parameter is non used if remoteComponent is defined.
* - remoteMethodName (String) : Web remote method name (annotated @WebRemote)
* - paging (bool) - optional (false by default) : tells wether the remote method uses paging.
*
* Use example :
*
* var store = new Ext.data.SeamRemotingStore(
* {
* remoteComponent: customerWebAction,
* remoteMethodName: "listAllCustomersWithPaging",
* paging: true,
* sortInfo:
* {
* field: 'nom',
* direction: "ASC"
* },
* groupField: 'customerType'
* });
*
* store.load(
* {
* params:
* {
* queryString: '',
* start: 0,
* limit: 15
* }
* });
*
* [fr]
* Paramètre de configurations supplémentaires :
* - remoteComponent (object) : Composant seam obtenu par un appel à Seam.Component.getInstance(name)
* - remoteComponentName (String) : nom du composant Seam à consulter (paramètre non pris en compte si le précédent est défini)
* - remoteMethodName (String) : nom de la méthode remote (marquée @WebRemote)
* - paging (bool) - optionnel (false par défaut) : la méthode remote supporte la pagination
*
* @param {Config} c
*/

Ext.data.SeamRemotingStore = function(c)
{
// configuration
var seamComponent = c.remoteComponent;

if (!c.remoteComponent)
{
var remoteComponentName = c.remoteComponentName;
seamComponent = Seam.Component.getInstance(remoteComponentName);
}

var remoteMethodName = c.remoteMethodName;

var paging;
if (c.paging)
{
paging = c.paging;
}
else
{
paging = false;
}

var remoteMethod = eval("seamComponent." + remoteMethodName);

// instantiale a proxy and a reader
this.proxy = new Ext.data.SeamRemotingProxy(remoteMethod, seamComponent, paging);
this.reader = new Ext.data.BeanReader();

// record currently selected
this.currentRecord = null;

this.getCurrentRecord = function()
{
return this.currentRecord;
}

this.setCurrentRecord = function(record)
{
this.currentRecord = record;
}

Ext.data.SeamRemotingStore.superclass.constructor.call(this, c);
};
Ext.extend(Ext.data.SeamRemotingStore, Ext.data.GroupingStore);

/**
* Utility method to create a copy of a bean knowing it's seam
* component name.
*
* @param {Object} bean The bean to be copied
* @param {String} beanName Seam component name
*/

function copyBean(bean, beanName)
{
var newBean = Seam.Component.newInstance(beanName);

for (var prop in bean)
{
if ((new String(bean[prop])).substring(0, 8) != "function")
{
newBean[prop] = bean[prop];
}
}

return newBean;
}


jeudi 3 janvier 2008

Seam + ExtJS, fondations

Après avoir pas mal recherché d'informations sur Internet à propos d'une intégration possible de Seam et ExtJS, il est apparu qu'aucun exemple de code ne circule ni même beaucoup de conseils en terme de conception.

Maintenant que j'ai un prototype qui fonctionne, je vais écrire quelques articles montrant pourquoi ce choix de technologie, comment le faire (avec de la théorie et du code source d'exemple), et aussi comment cela se passe sur un projet d'entreprise (montée en compétence sur cette technologie, limitations, bugs encore présents, etc), et enfin je pourrais donner des indications en terme de performances.

Pour commencer, ce premier article montre comment je compte lier ces deux frameworks prometteurs.

En premier lieu, mon souhait est de réhabiliter le poste client. Généralement on se contente de lui faire afficher les fichiers HTML, d'exécuter quelques javascripts simples pour valider les données saisies puis de soumettre le formulaire au serveur d'application en attendant d'afficher une nouvelle page complète.

Pour moi, le défaut de ce design est d'introduire une couche applicative relativement complexe, la couche de présentation, pour pouvoir manipuler ces soumissions de formulaires. Bien sûr de nombreux frameworks existent, comme Struts ou JSF, mais ils restent tout de même relativement lourds et représentent une part importante dans le temps de développement d'une application.

De plus ce mode de fonctionnement oblige à envoyer l'ensemble des données du formulaire au serveur d'application qui renvoie l'ensemble du code HTML à afficher côté client. On observe à cause de ce mécanisme :

  • une complexité accrue dans la gestion de l'état de la page : une donnée chargée pour être affichée dans la page dans un temps t0, pour ne pas avoir à être de nouveau chargée, doit être gérée dans un cache : par exemple dans un viewstate (applicable à jsf ou asp .net) ou alors un cache applicatif qu'il faut aller consulter lorsque l'on construit la page (Struts + jsp par exemple)
  • des pertes de performance et de ressources notables car d'une part on fait transiter sur le réseau des données inutiles (des données de formulaires inchangées, puis des blocs complets d'html non modifiés...), et d'autre part on s'oblige à valider côté serveur des données qui n'ont pas lieu d'être (encore une fois ces données de formulaire inchangées...) !

Mon souhait est donc d'évincer cette couche applicative et de rendre au client le rôle de contrôleur. Avec les technologies Ajax il est désormais possible d'effectuer un grand nombre d'actions sans pour autant avoir besoin de faire des soumissions de formulaires HTTP tout en limitant les données en transit au minimum nécessaire.

Dans ce cas de figure, le serveur d'application se contente d'être un serveur de services (on s'oriente donc vers une architecture de type SOA), en exposant une "façade web" à ses services métiers. Ces façades se contentent de centraliser la sécurité d'accès aux services, de gérer les exceptions métier et de remonter les objets et messages au client javascript. Ainsi pour un même service métier, on pourrait avoir une façade webservice et une autre façade Seam Remoting.

C'est là qu'entre en jeu Seam Remoting : plutôt que d'utiliser un webservice comme source de données aux composants clients ExtJS (relativement lourd à mettre en place côté serveur comme côté client), nous allons utiliser la possibilité d'interroger à distance un Session Bean (avec en plus la possibilité de s'inscrire dans un contexte de conversation).

Pour information et pour avoir une idée de ce que je tente de faire sur ce prototype voici une copie statique de l'interface :



La liste est chargée en consultant un SessionBean (EJB 3.0), la pagination est activée ainsi que la possibilité de trier les colonnes et de grouper les lignes selon un critère.

Quant on clique sur une ligne, son détail s'affiche dans le formulaire de droite. Save permet d'envoyer une requête à un SessionBean qui met à jour la donnée en base. Selon le message renvoyé par ce SessionBean (succès, erreur, ...), on met à jour côté client les données telles que sauvegardées.

L'avantage de cette solution : seules les données du formulaire de modification sont envoyées au serveur (par exemple pas besoin d'envoyer les informations d'état sur la page...), et surtout, aucun besoin de renvoyer tout le code html pour raffraîchir la liste !

Parce qu'un diagramme vaut mieux qu'un long discours voilà ce que cela donne de manière synthétique. Je ne suis pas rentré dans les détails de ce qui ce passe côté ExtJS :

La partie Faces/jsf est réduite à sa partie minimale, c'est-à-dire initialiser la page à son miminum (ce qui ne comprend même pas le chargement de la liste par exemple). Tous les événements se produidant sur la page seront traités par Seam Remoting.

Suite dans un prochain article où je montrerai le code source des classes Store, Reader, etc qui permettent de faire le lien entre ExtJS et Seam Remoting...

Vers l'article suivant : http://ntispace.blogspot.com/2008/01/seam-extjs-ext-store-reader-proxy.html