Integrating ExtJS with Seam: Loading stores with Json
As I mentioned in an earlier post the Seam framework has sparked my curiosity. To help me better understand some of its inner workings I wanted to figure out how to integrate Seam with the ExtJS framework for populating client side stores. Many of the Ext widgets are backed by stores so this was a logical place to start. What are stores? Quite simply stores, aka Ext.data.Stores, are the Ext construct for storing client side record data. Think of it as client-side MVC; where the record data is the Model, the widget is a View of the model, and event handlers are the Controllers. For this exercise I'm going to focus on the most rudimentary level of integration using a Json protocol on top of the Seam Remoting Framework. Simply put, I'm going to return records using Json upon which an Ext.data.Store will be populated.
Seam, at this time, only supports a view managed by a JSF framework(s). However it provides a direct RMI style interaction with its managed componets via a JS Remoting subsystem. From the server side the solution is very elegant, simply annotate a method @WebRemote. The client side is equally elegant. With the inclusion of a couple JS files you have a full JS based RMI solution. That's nice and all but I'm not really interested in a JS based RMI solution at this point. Let me qualifiy this, I'm not interested in JavaBean to JS obect serialization. Instead I want to use a simple Json based protocol that Ext natively understands. Meaning the server components should return Json formatted (String) results.
As it turns out this, the integration is very simple. Heck google even pointed to similar efforts by like-minded developers. The Ext.data.DataProxy class is the proper ExtJS extension point for fetching data outside of its connection framework. The solution is simply an extenstion of Ext.data.DataProxy that uses JS Seam component proxies to load Json data into stores. The solution consists of 2 classes the Ext.ux.data.SeamRemotingProxy:
/** * Copyright(c) 2008, http://www.mcdconsultingllc.com * * Licensed under the terms of the Open Source LGPL 3.0 * http://www.gnu.org/licenses/lgpl.html * @author Sean McDaniel */ Ext.namespace('Ext.ux.data'); /** * @class Ext.data.ux.SeamRemotingProxy * @extends Ext.data.DataProxy * Seam remoting proxy. An extension of the Ext.data.DataProxy class for * making requests to server side Seam components via Seam remoting. This * proxy can be bound to a single Seam component and futher to a single * method exposed by the component. In order to the remoting to work the * method must be annotated wtih @WebRemote. * * Constructor * @param {Object} config - Containing the following properties: * - seamComponent - A Seam component instance. * - remoteMethod - A reference to the remote method. * - requestComponent - The name of a Seam component */ Ext.ux.data.SeamRemotingProxy = function(config) { Ext.apply(this, config); Ext.ux.data.SeamRemotingProxy.superclass.constructor.call(this); }; Ext.extend(Ext.ux.data.SeamRemotingProxy, Ext.data.DataProxy, { /** * Calls the seam remote method. * * @param {Object} params An object containing properties which are to be used as parameters * to the remote method. If a requestComponent was specified the properties will * be copied to the requestComponent instance else they will be passed as individual args to the * remote method. * @param {Ext.data.DataReader} reader The Reader object which converts the data * object into a block of Ext.data.Records. * @param {Function} callback The function into which to pass the block of Ext.data.Records. * The function must be passed * - The Record block object * - The "arg" argument from the load function * - A boolean success indicator * @param {Object} scope The scope in which to call the callback * @param {Object} arg An optional argument which is passed to the callback as its second parameter. */ load: function(params, reader, callback, scope, arg) { this.fireEvent("beforeload", this, params); var args = []; params = params || {}; // pass the params as a component? if (this.requestComponent) { var request = Seam.Component.newInstance(this.requestComponent); for (var param in params) { request[param] = params[param]; } args.push(request); } else { // push the params onto the args[] for (var param in params) { args.push(params[param]); } } var proxy = this; args.push(function(response) { proxy.loadResponse(response, reader, callback, scope, arg); }); // invoke the method this.remoteMethod.apply(this.seamComponent, args); }, /** * Private! Processes the response. */ loadResponse: function(response, reader, callback, scope, arg) { var result; try { result = reader.read(response); } catch (e) { this.fireEvent("loadexception", this, response, e); callback.call(scope, response, arg, false); return; } this.fireEvent("load", this, response, arg); callback.call(scope, result, arg, true); } });
And the Ext.ux.data. SeamRemotingJsonReader:
/**
* @class Ext.data.ux.SeamRemotingJsonReader
* @extends Ext.data.JsonReader
* Need to extend Ext.data.JsonReader to account for the fact that the Ext.data.JsonReader
* expectes the actual reponse string to be assigned to the property 'responseText' on the
* response object it is passed. The override to the read method here simply calls the base
* class, packaging the reponse received from the callback in a responseText property of a
* new object.
*
* This class uses a simple protocol with the server. Since Seam Remoting abstracts away the
* Http layer we can't rely on an HTTP status to determine if the call resulted in a server
* side exception or not. The solution here use an interceptor on the server for the remote
* methods that returns Json. If an exception is raised the interceptor will return a Json
* string of '{exception:true}'. This reader will in turn throw an exception with
* the message which will ultimately result in a 'loadexception' to be fired from the proxy.
* Note: this interceptor infrastructure is required. It was determined that when error occured
* on the server our Seam registered callback was not invoked.
*/
Ext.ux.data.SeamRemotingJsonReader = function(meta, recordType) {
Ext.apply(meta, {successProperty: 'success'});
Ext.ux.data.SeamRemotingJsonReader.superclass.constructor.call(this, meta, recordType);
};
Ext.extend(Ext.ux.data.SeamRemotingJsonReader, Ext.data.JsonReader, {
/**
* This method is only used by a DataProxy which has retrieved data from a remote server.
* This override simply accounts for the need to create a response object with a responseText
* property.
*
* If the success indicator is false throw an exception.
*
* @param {Object} response The Seam Remoting object which contains the JSON data in its responseText.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
read: function(response) {
var json = Ext.decode(response);
if (json.exception) {
throw {
message: 'SeamRemotiingJsonReader.read: Exception raised on server.'
};
}
return Ext.ux.data.SeamRemotingJsonReader.superclass.read.call(this, {
responseText: response
});
}
});
And finally a sample usage to populate an Ext.data.Store:
var seamComponent = Seam.Component.getInstance('seamComponentName');
var proxy = new Ext.ux.data.SeamRemotingProxy({
remoteMethod: seamComponent.loadJson,
seamComponent: seamComponent
});
var reader = new Ext.ux.data.SeamRemotingJsonReader({id: 'id', root:'data'}, [
{name: 'company'},
{name: 'price', type: 'float'},
{name: 'change', type: 'float'},
{name: 'pctChange', type: 'float'},
{name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
]);
var ds = new Ext.data.Store({
proxy: proxy,
reader: reader
});
ds.load();
Click here to download the SeamRemoting.js file.