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.

Comments

How are you getting Seam to return JSON?

I stumbled across your little library here the other day and found it to be exactly what I was looking for. Everything makes sense except that I can't seem to figure out how you are getting Seam to return JSON. Are you using a 3rd party JAR like JSONTools or did you write your own servlet or something? I'm still pretty new to Seam so anything you could share would be fantastic.

Thanks

- Justin

Json handling

Hey Justin,

I just logged into this site for the first time in a few weeks. I typically approach Json encoding on the server pragmatically. I have a couple of Utilities. One for dealing with basic Json stuff that supports grids and another that supports Json-Form binding. I like using BeanUtils to merge the form state into detached domain objects in the web tier. So the Json-Form binding flattens out an object graph of named value pairs. I have a custom SeamForm that extends the FormPanel and amongst other overrides, has an override for the load method that kicks off the loading of the form json and later consumes it and populates the form.

If you need more help just let me know. Oh and my json utilities ride on top of json-lib and beanutils. So I also use converterutils to convert things like dates to a format for display.

Sean

Json handling

Would it be possible to share a small full example? I am in the same boat as Justin, I don't fully understand how you are getting json from the Seam server side.

Matt