Welcome!

AJAX & REA Authors: John Drachman, John Funnell, Bob Little, Kevin Hoffman, Maureen O'Gara

Related Topics: AJAX & REA

AJAX & REA: Article

Real-World AJAX Book Preview: Row Selection in the SimpleTable

Real-World AJAX Book Preview: Row Selection in the SimpleTable

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.

Row Selection in the SimpleTable
Note in the "setData" method of the SimpleTable, the following code.

  • The code to add "cellClicked" as an "onclick" listener to each cell.
  • The ID attribute of each attribute is set so that we can parse it to retrieve the row and column of the cell.
When the cell is clicked, the control is transferred to the "cellClicked" method. We extract the ID of the cell that is the source of the event. From this ID, we retrieve the "row" and "col" of the cell. Using the "row" we invoke the "selectRow" method. The "selectRow" method changes the background color of the cells of the given row to a color that indicates that the row is selected. The code and embedded comments in Listing 5.14 provide the details of these steps.

Listing 5.14 - SimpleTable - Row Selection

// cell clicked event handler
SimpleTable.prototype.cellClicked = function(event) {
   var ev = event ? event : window.event;
   var e = ev.srcElement ? ev.srcElement : ev.target;
   // retrieve cell id
   var cellId = e.id;
   // retrieve row and column
   var idx = cellId.indexOf("_");
   var idx2 = cellId.lastIndexOf("_");
   var row = parseInt(cellId.substring(idx+1, idx2));
   var col = parseInt(cellId.substring(idx2+1));
   // select the clicked row
   this.selectRow(row);
   // send message to its cell selected listener
   for (var i=0; i<this.cellSelectedListeners.length; i++) {
     this.cellSelectedListeners[i].cellSelected(row, col);
   }
}

// highlights the given row
SimpleTable.prototype.selectRow = function(row) {
   var tr;
   // reset the background of currently selected row
   if (this.currentRow != -1) {
     tr = this.tbody.rows[this.currentRow];
     for (var i=0; i<tr.cells.length; i++) {
     tr.cells[i].style.backgroundColor = this.savedColor[i];
     }
   }
   this.currentRow = row;
   this.savedColor.length = 0;
   // highlight the newly selected row
   // save the current background color before highlighting
   if (this.currentRow != -1) {
     tr = this.tbody.rows[this.currentRow];
     for (var i=0; i<tr.cells.length; i++) {
     this.savedColor.push(tr.cells[i].style.backgroundColor);
     tr.cells[i].style.backgroundColor = __selectionColor;
     }
   }
}

Row Selection Listener
It's a very commonly used function that when a cell in a table is clicked, a listener is notified of the cell's row and column number. We also use this function to tie a table to a form. The "addCellSelectedListener" method of the SimpleTable is used to add the listener. The code assumes that the given listener implements a "cellSelected" method.

Listing 5.15 - SimpleTable - Adding Selection Listener

// adds a cell selected listener
// assumes that the passed listener defines cell selected method
SimpleTable.prototype.addCellSelectedListener = function(l) {
   if (l.cellSelected == null) {
     alert("The given listener should implement cellSelected(r, c) method");
     return;
   }
   this.cellSelectedListeners.push(l);
}

When the cell is selected, the "cellClicked" method of the SimpleTable invokes the "cellSelected" method of its listeners.

An example cell selected listener is shown below.

var someTable;
... ... ...
someTable.addCellSelectedListener(new CellSelectedListener());
... ... ...
// The cell selected method of the following listener will be called when
// user clicks on the cell of ‘someTable'.
function CellSelectedListerner() {
   this.cellSelected = function(r, c) {
     // do something
   }
}

Example Components
In the sections above we discussed an implementation of the table. A form is another commonly used UI component. It consists of labels and fields. Before we get into the details of a form, let's take a look at commonly used fields and how they're modeled using the component class. We'll discuss these fields through the following classes.

  • Component: The base class of all components
  • TextComp: For a text field
  • SelectComp: For the selection of a value using a dropdown list
  • DateComp: For the date field
  • TextArea: For the text area field
These components illustrate the concepts and are also used in the form discussed in the following sections. Since our goal is to illustrate concepts, we've simplified the implementation.

Component
Component is the base class of all other components. In the following section, we'll see how other classes inherit its behavior. A component fulfills the following specifications:

  • A component has a name.
  • It's associated with an HTML element used to display and edit its value.
  • A component is bound to an object.
  • It displays the value of the attribute of the object that has the same name as the name of the component.
  • When the user changes the value, the component updates the value of the object's attribute.
  • Before updating the value, the component might validate the value. If the validation fails, then the value of attribute isn't updated.
  • A value-changed listener can be added to the component. After the value is successfully updated, the value-changed listener is triggered by passing the reference to its object, attribute name, and the new value of the attribute.
The code and embedded comments in Listing 5.16 provide details of the component class.

Listing 5.16 - Component Class

// base class of all other component types
   function Component(name, displayName) {
     // the associated element
     this.element = null;
     // name of the component
     this.name = name;
     // label of the component
     this.displayName = displayName;
     // the data, this compoennt is bound to this.data = null;
     // list of value changed listener
     // ‘valueChanged' method of each listener will
     // be called after successful update.
     this.valueChangedListeners = new Array();
   }

   // This method is used to associate an HTML element with
   // the component
   Component.prototype.setHtmlElement = function(e) {
     this.element = e;
     var self = this;
     // add onchange listener to the HTML element
     e.onchange = function() {self.changed();};
   }

   // This method is used to bound an object with
   // the component.
   Component.prototype.setData = function(data) {
     // set the given data to its attribute
     this.data = data;
     if (this.element == null) {
     return;
     }
     // retrieve attribute value
     // assumes that the component name is
     // same as the attribute name
     var v = data[this.name];
     if (v == null) {
     v = ‘';
     } else {
     v = v.toString();
   }
   // set the value of element
   this.element.value = v;
}

   // This method is triggered by onchange of the HTML element.
   Component.prototype.changed = function() {
     var v = this.element.value;
     // validate new value
     if (!this.validate(v)) {
       return;
     }
     // set the value of object's attribute
     if (this.data != null) {
       this.data[name] = v;
     }
     // inform value changed listeners
     for (var i=0; i<this.valueChangedListeners.length; i++) {
       this.valueChangedListeners[i].valueChanged(this.data, this.name, v);
     }
   }

   // validates the value
   // might be overridden by its subclass.
   Component.prototype.validate = function(v) {
     return true;
   }

   // adds a value changed listener
   // assumes that the listener implements ‘valueChanged' listener
   Component.prototype.addValueChangedListener = function(l) {
     if (l.valueChanged == null) {
       alert("The given listener should implement valueChanged(data, name, newValue)
   method");
       return;
     }
     this.valueChangedListeners.push(l);
   }

   Component.prototype.render = function() {
     var elem;
     elem = document.createElement(‘input');
     elem.type = ‘text';
     this.element = elem;
     return elem;
   }

Inheritance in JavaScript
Since attributes and methods are treated alike in JavaScript, assigning the properties of one object to another is the equivalent of the latter inheriting from the former. In the following code the "destination" object inherits from the "source" object:

Object.extend = function(destination, source) {
   for (property in source) {
     destination[property] = source[property];
   }
   return destination;
}

We make use of the inheritance in the following component implementation.

TextComp
TextComp inherits from component. It's used for a text-type value. Our implementation of TextComp doesn't have any specific behavior.

function TextComp(name, displayName) {
   this.type = "TextComp";
   Object.extend(this, new Component(name, displayName));
}

SelectComp
SelectComp inherits from the component. It's used for displaying and editing the pairs of code names, where "code" is the internal value and "name" is the display value. It's associated with a SELECT-type HTML element. The list of SELECT-element options can either be specified in the HTML template or populated programmatically.

function SelectComp(name, displayName) {
     Object.extend(this, new Component(name, displayName));
   this.type = "SelectComp";
   // clears all option elements of its element
     this.clearOptions = function() {
     ... ... ...
     }
}
// programmatically adds an option
SelectComp.prototype.addOption = function(name, value) {
     ... ... ...
}

DateComp
Our example DateComp is really a rather simplified implementation. It's used to enter date in a mm/dd/yy format. It extends the component class and overrides the "validate" method to verify if the user-entered value is in the desired format.

function DateComp(name, displayName) {
   this.type = "DateComp";
   Object.extend(this, new Component(name, displayName));
}

// validates if the value is in mm/dd/yy format
DateComp.prototype.validate = function(v) {
   ... ... ...
   return true;
}

TextAreaComp
TextAreaComp inherits from the component. It's associated with the TextArea HTML element and is used for a text-type value when the length of the string is long and shown in multiple lines. Our implementation of TextAreaComp doesn't have any specific behavior.

function TextAreaComp(name) {
   Object.extend(this, new Component(name));
}

Project Detail Form
We've discussed a set of useful components. Here we'll see how to use them in a form. Say we have a requirement to display the selected project's summary as a form below the project list. The user would use this to change the properties of a project.

Refer to the MainStep-6.html for a discussion in this section.

We follow the same steps as in the case of a table. We ask our UI designer to design a project summary form. She gives us the following template as ProjectSummary.html.

Like the table example, we write ProjectSummary.js to load this template and display it below the project list. Listing 5.17 shows how this is done; the code is similar to the code for ProjectList.js.

Listing 5.17 - Displaying ProjectSummary.html Template

// Reference to Form component
var projectSummary = null;
// Reference to the block element containing Form
// This is attached in the main area
var projectSummaryContainer = null;
// Actual form element
var projectSummaryElement = null;

// attaches project summary form to MainArea2
// loads the template from the server
function displayProjectSummary() {
   if (projectSummaryContainer == null) {
     // project summary template is not yet loaded
     // load the template
     loadProjectSummaryElement();
   } else {
     // project summary template has been loaded
     // simply attach it
     // See Util.js for the details of attachElement function.
     attachElement(‘MainArea2', projectSummaryContainer);
   }
}

// loads HTML template for the project summary form
function loadProjectSummaryElement() {
   var req = new DataRequest(basePageURL + ‘ProjectSummary.html',
     requestCompletedProjectSummaryElement,
     requestFailedProjectSummaryElement,
     requestTimedoutProjectSummaryElement);
   ajaxEngine.processRequest(req);
}

// call back after the project summary is loaded
// response.responseText gives the HTML retrieved
function requestCompletedProjectSummaryElement(response) {
   projectSummaryContainer = setElementFromHtml(response.responseText,
‘MainArea2',
‘projectSummaryContainer');
   // our projectSummaryContainer element contains the projectSummaryElement
   projectSummaryElement = document.getElementById("projectSummaryElement");
   // create a Form component
   projectSummary = new Form();
   // associate projectSummaryElement with it
   projectSummary.setHtmlElement(projectSummaryElement);

   // associates field components with the corresponding elements
   projectSummary.setFieldComp(new TextComp("name"));
   projectSummary.setFieldComp(new TextComp("resource"));
   projectSummary.setFieldComp(new TextAreaComp("description"));
   projectSummary.setFieldComp(new DateComp("startDate"));
   projectSummary.setFieldComp(new DateComp("endDate"));
   projectSummary.setFieldComp(new SelectComp("status"));
   projectSummaryTemplateLoaded();

}

When we invoke "displayProjectSummary" from the "initialize" method of the MainPage-6.html, the resulting MainPage-6.html is shown below.

function initialize() {
     loadTopNaviagation();
   displayProjectList();
   displayProjectSummary();
}

Form
We've seen how to load the ProjectSummaryTemplate.html template and display it in MainArea2 of the main page and how the element corresponding to the form is associated with the Form component. This section discusses the design of the Form class.

Listing 5.18 shows the implementation of the Form. The comments in the code explain its methods. The "setHtmlElement" method is used to associate the Form with the element. The "setFieldComp" method is used to associate a field element contained inside it with a given component. The "setData" method assumes that it's passed a JavaScript object where attributes of the object correspond to the fields of the component. The "setData" method in turns sets the value of the field components. A Form listens for the value-change event from all its fields. The Form, in turn, triggers its value-change listeners, when the value of a field changes.

Listing 5.18 - Form Class

function Form(name) {
   // name of the component
   this.name = name;
   // the associated form element
   this.element = null;
   // collection of sub-elements contained inside
   // the form element
   this.collection = null;
   // list of components (fields)
   this.components = new Array();
   // list of value change listeners
   this.valueChangedListeners = new Array();
   // current data set displayed by this form
   this.data = null;

   // original objects table
   this.originalObjs = new Object();
   // recorded changes by object
   this.changesByObj = new Object();
}

// sets the form element
// builds a collection of sub-elements
Form.prototype.setHtmlElement = function(e) {
   this.element = e;
   this.collection = new ElementCollection(e);
}

// sets a field component
// retreieves the field element using the name of the
// component (component name is same as the field id)
Form.prototype.setFieldComp = function(comp) {
   if (this.element != null) {
     var e = this.collection.getElementById(comp.name);
     if (e != null) {
     comp.setHtmlElement(e);
     }
   }
   // form listens for any change to its field
   comp.addValueChangedListener(this);
   // add the component to the components' list
   this.components.push(comp);
}

// sets the dataset of the form
// iterates over the component list and
// sets data for each field
Form.prototype.setData = function(data) {
   this.data = data;
   this.copyOriginalData(data);
   for (var i=0; i<this.components.length; i++) {
     this.components[i].setData(data);
   }
   }

   // invoked when the value of one of the field of the
   // form changes
   // when any field of the form changes, the form
   // invokes valueChanged method of its listener
   Form.prototype.valueChanged = function(data, name, value) {
     this.recordChanges(data, name, value);
     for (var i=0; i<this.valueChangedListeners.length; i++) {
       this.valueChangedListeners[i].valueChanged(data, name, value);
     }
   }

// add a value change listener
// when any field of the form changes, the form
// invokes valueChanged method of its listener
Form.prototype.addValueChangedListener = function(l) {
   if (l.valueChanged == null) {
     alert("The given listener should implement" +
       " valueChanged(data, name, newValue) method");
     return;
   }
   this.valueChangedListeners.push(l);
}

Displaying Selected Rows in the Form
In the form above, we want to display the selected row of the table. Note that the names of the components are the same as the column IDs and the attributes of the object. This name/ID matching is key to data binding.

To display the selected row, we add the cell selection listener to the table after it's loaded. Refer to the MainPage-7.html for a discussion of this section.

function projectListTemplateLoaded() {
   // load the project list data
   loadProjectList();
   // add a cell selection listener to the table
   projectList.addCellSelectedListener(new CellSelectedListerner());
}

Inside the listener, we retrieve the object corresponding to the selected row and set it as the data of the form component. As seen above, the "setData" method of the form in turn sets the value of its components.

function CellSelectedListerner() {
   // called when the user clicks on the cell of the table
   // ‘r' is the row and ‘c' is the column of the cell
   this.cellSelected = function(r, c) {
     // retrieve object corresponding to the row
     var rowData = projectList.getRowData(r);
     if (rowData == null) {
       rowData = new Object();
     }
     // set the object as the value of the form
     projectSummary.setData(rowData);
   }
}

Propagating Changes
In the above example a selected row is displayed inside the form. But in the reverse direction, when a field is edited, the changes aren't reflected in the table. In any UI, it's important that the updates are reflected consistently. This is handled through properly crafted event listeners.

Refer to the MainPage-8.html for a discussion in this section. We add a value-change listener to the form after it's loaded. The listener informs the table that the value has changed.

// add value change listener after the from's template is loaded.
function projectSummaryTemplateLoaded() {
   projectSummary.addValueChangedListener(new ValueChangedListener());
}
// when the value of an attribute changes, inform the table
function ValueChangedListener() {
   // this method is called when the user edits a field of the form
   this.valueChanged = function(data, name, value) {
     // inform the project list that the ‘value' of ‘name' attribute of
     // the ‘data' object has changed
     projectList.processValueChange(data, name, value);
   }
}

The "processValueChange" method of the table is shown below. It updates the value of the cell of the row that corresponds to the updated object.

// this method is called when the value of an object's
// attribute changes.
// If the given, object is found in this table's dataset
// then the value of the cell corresponding to the
// changed attribute is updated.
this.processValueChange = function(data, name, value) {
   if (this.dlist == null) {
     return;
   }

   for (var i=0; i<this.dlist.length; i++) {
     // check if the given object is present in its dataset
     if (this.dlist[i] == data) {
       // given object is present in my dataset
       for (j=0; j<this.columns.length; j++) {
         var col = this.columns[j];
         // check if the col.id matches with the given name
         if (col.id == name) {
           // column matches the given attribute name
           var tr = this.tbody.rows[i];
           var td = tr.cells[j];
           removeAll(td);
           // update value of the cell.
           td.appendChild(document.createTextNode(value));
           break;
         }
       }
       break;
     }
   }
}

Retrieving Change List When the requirement is to send updates to the server immediately, the value-changed handler can be used to send the changes to the server through an AJAX call. Another scenario is to record changes in the form until the UI retrieves them at the user's request and sends them to the server. When the user clicks on the "Save" button, the changes are sent to the server.

Since a form can be used to edit many objects before they're saved at the server, we record changes by object ID (we're assuming that the original data sent by the server has an "id" attribute for each object). The form makes a copy of the original object when it's set to the form the first time. Subsequently, the value-change listener compares the changed value with the original value and records the change. The code also takes care of the situation when the value is reset back to the original value. The change recorded for the reset field is removed. The following changes are made to the Form class to record the changes made by the user.

  • A new "copyOriginalData" method is added to make a copy of the original object so that the new value can be compared against the original value.
  • A new "recordChanges" method is added to record the changes. The changes are recorded in the table indexed by the "id" of the object (this is a per-object changes table). A form records the changes for multiple objects until they're cleared.
  • A new "clearChanges" method is added to clear all the changes after they've been saved at the server.
  • A new "getChangeList" method is added to get the list of changes as an XML string.
  • The "setData" method is modified to call "copyOriginalData" to make a copy of the data to be edited by the form. The copy is made only if this is the first time the object is set in the form.
  • The "valueChanged" method is modified to call "recordChanges" when the value of one of its field changes.
The new methods and modified code of the existing methods is shown in Listing 5.19.

Listing 5.19 - Form's Methods for Recording Changes

// sets the dataset of the form
// makes a copy of the data
// iterates over the component list and
// sets data for each field
Form.prototype.setData = function(data) {
   this.data = data;
   this.copyOriginalData(data);
   ... ... ...
}

// invoked when the value of one of the field of the
// form changes
// it records the changes
// when any field of the form changes, the form
// invokes valueChanged method of its listener
Form.prototype.valueChanged = function(data, name, value) {
   this.recordChanges(data, name, value);
   ... ... ...
}

// makes a copy of the given data
// works only if the given data has __id attribute
Form.prototype.copyOriginalData = function(data) {
// data has id and not added to the originalObjs yet
  if (data.__id && this.originalObjs[data.__id] == null) {
     var newData = new Object();
     // copy attributes
     for (property in data) {
       newData[property] = data[property];
     }
     // insert into the orginal objects table
     this.originalObjs[data.__id] = newData;
   }
}

// records changes in the given object
Form.prototype.recordChanges = function(data, name, value) {
   // retrieve original object
   var origObj = this.originalObjs[data.__id];
   if (origObj != null) {
     // is value different
     if (value != origObj[name]) {
       // look for the object that records the change
       var obj = this.changesByObj[data.__id];
       if (obj == null){
         // the first time the object is changed
         // create an object for recording changes
         obj = new Object();
         // set its id and class to the object for which it is
         // recording the changes
         obj.__id = data.__id;
         obj.__className = data.__className;
         // insert into changedByObj table
         this.changesByObj[obj.__id] = obj;
       }
       obj[name] = value;
     } else {
       // cleanup any change recorded for this attribute
       var obj = this.changesByObj[data.__id];
       // if changes were recorded for this attribute, remove them
       if (obj != null && obj[name]){
         // delete the changes
         delete obj[name];
       }
     }
   }
}

// clears the changes recorded
Form.prototype.clearChanges = function() {
   // recreate the tables
   this.originalObjs = new Object();
   this.changesByObj = new Object();
   if (this.data != null) {
     // copy the current object
     this.copyOriginalData(this.data);
   }
}

// returns the changes list as XML
Form.prototype.getChangeList = function() {
   var changes = "";
   // for each changed object
   for (id in this.changesByObj) {
       var obj = this.changesByObj[id];
       var s = "";
         // for each changed attribute (except __id and _className)
       for (property in obj) {
         if (property != "__id" && property != "_className") {
           s = s + "<" + property + ">" + obj[property] +
             "</" + property + ">";
         }
       }
       // if there were changes
       if (s != "") {
         var t = obj.__className;
         if (t == null) {
           t = "Change";
         }
       changes = "<" + t + " id=\"" + obj.__id + "\">" + s + "</" + t + ">";
   }
   // if changes are present, wrap them in changelist element
   if (changes != "") {
     return "<changelist>" + changes + "</changelist>";
   }
   return changes;
}

Controller Servlet in Detail
In the section "Message Format for the Server Invocation", we saw the format of the messages to be sent to our example server. The service specification is in an XML format and is sent as a parameter of the HTTP request. The rest of the message, for example, a change list, is passed as the body of the POST request. The controller servlet converts both documents into an XML document. The controller servlet uses the service specification to identify the service. It then simply delegates the message to the service for further processing.

For simplicity's sake, we assume that our services implement the following interface:

Listing 5.20 - Service Interface

public interface Service {
   // processes the get request
   // sdoc is the service-specification document
   public String serviceGet(Document sdoc)
     throws RemoteException, ServiceException;
   // processes the post request
   // sdoc is the service-specification document
   // doc is the document sent in the body of the post request
   public String servicePost(Document sdoc, Document doc)
     throws RemoteException, ServiceException;
}

The controller servlet code, assuming the above service specification, is shown below.

Listing 5.21 - Controller Servlet

public class ControllerServlet extends HttpServlet {

   public void init(ServletConfig config) throws ServletException {

     super.init(config);
   }

   // processes service-specification sent through GET method by the client
   public void doGet(HttpServletRequest req, HttpServletResponse res)
     throws ServletException, IOException {

   res.setHeader("Pragma", "no-cache");
   res.setHeader("Cache-Control", "no-cache");
   res.setHeader("Cache-Control", "no-store");
   res.setHeader("Expires", "0");
   res.setContentType("text/xml");

   // Service spec is passed as a request parameter called "service_spec"
   // Convert "service_spec" parameter to XML document
   Document sdoc = getServiceSpecDoc(req);
   // retrieve service name from the XML doc
   String serviceName = getServiceName(sdoc, req);
   // look up the service by its name
   Service svc = getService(serviceName);

     if (svc != null) {
       try {
         // delegate the request to the service
         String s = svc.serviceGet(sdoc);
         res.getWriter().write(s);
         } catch (ServiceException exc) {
         ... ... ...
       }
     } else {
       // error
       // throw exception
     }
   }

   // processes the service-specification sent in the POST request
   // and the document sent in the body of the post request
   public void doPost(HttpServletRequest req, HttpServletResponse res)
     throws ServletException, IOException {

   res.setHeader("Pragma", "no-cache");
   res.setHeader("Cache-Control", "no-cache");
   res.setHeader("Cache-Control", "no-store");
   res.setHeader("Expires", "0");
   res.setContentType("text/xml");

   // Read XML document from the request body
   Document doc = null;
   try {
     doc = getDocument(req);

   } catch (Exception exc) {
     // error, throw exception
   }

   // Service spec is passed as a request parameter called "service_spec"
   // Convert "service_spec" parameter to XML document
   Document sdoc = getServiceSpecDoc(req);
   // retrieve service name from the XML doc
   String serviceName = getServiceName(sdoc, req);
   // lookup the service by its name
   Service svc = getXmlService(serviceName);

   if (svc != null) {
     try {
       // delegate the processing to the service with
       // the service-specification and the XML document sent in the body
       String s = svc.servicePost(sdoc, doc);
       if (s != null) {
         res.getWriter().write(s);
       }
     } catch (ServiceException exc) {
       // error
       }
     } else {
       // error
     }
   }
}

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.

More Stories By Anil Sharma

Anil Sharma is a founder of Vertex Logic and architect of its AjaxFace product. He is working with Vertex Logic's customers and helping them deploy WEB 2.0 applications built using AjaxFace. These applications are in the areas of online printing, community WEB sites and social networking. He is driving future products of Vertex Logic. Prior to Vertex Logic, he was CTO and founder of Softrock Systems and Component Plus. There he built a model driven application platform using J2EE. His primary interests include user interface infrastructures and model driven applications. He is one of the contributors of the book "Real World AJAX - Secrets of the Master".

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.