AJAX with UIX – PrimaryClientAction for instantaneous server-based client refresh

The whole hype around AJAX (Asynchronous JavaScript And XML) for background client-server communication from JavaScript in HTML pages is very interesting. It allows for much more responsive, interactive and dynamic HTML user interfaces. It may bring HTML web-applications much closer to the user experience of applets or client/server applications. AJAX can be used for simple things as performing server side validations for client side data entry without refreshing the entire page, refreshing the contents of a tree with new data from the server when the user expands a node – without refreshing the entire page, refreshing the contents of a table in response to the user pressing the “next set” button – without refreshing the entire page. The theme should be clear.

Oracle’s UIX has its own AJAX implementation. It uses an IFRAME that is submitted, refreshed and read from – instead of the XMLHttpRequest() object or the ActiveXObject(“Microsoft.XMLHTTP”). This implementation is easy to use, pretty robust – even on somewhat older browsers and quite effective. UIX has the notion of Partial Page Refresh – where only specific components in a page are refreshed through the IFRAME based on the server response to the IFRAME-submit. This submit is triggered by a so-called primaryClientAction. This is a property defined in the UIX page on interactive items such as button and text-inputs. You can link targets – refreshable UIX elements in the UIX page – to a primaryClientAction. Whenever the action occurs, the form with the updated data is sumitted and the target element gets refreshed.

Yesterday at a ADF/UIX/JHeasdstart customer, I had the opportunity to look into the primaryClientAction in UIX and I was impressed by how simple it is to set up and use – no programming required! The path was paved by Steven Davelaar and Sandra Muller in their respective articles (http://www.orablogs.com/jheadstart/archives/001041.html) on the JHeadstart Blog.

In this article, I will give the almost simples example of developing a BusinessService (bean-based), a set of ADF DataControls based on that BusinessService and a UIX page that will contain a primaryClientAction. More specifically, the HrmService will provide EmployeeBeans. The EmployeeBean contains attributes name, job, salary, commission and income. Income is derived by adding up Salary and Commission. We build a DataPage, an editEmployeeForm.uix page, using the HrmService Employees-collection. Whenever we edit either the salary or the commission, the income is updated – through a server call: the income field is refreshed from the Model, after the Model has been updated from the primaryClientAction’s form-submit.

Steps to implement the Business Service

* create application workspace AjaxUix (web application)

in the Model Project:
* create bean Employee.java

package nl.amis.hrm.model;
public class Employee  {
  public Employee() {
  }
  public Employee( String name, double salary, double commission, String job) {
    this.setName(name);
    this.setSalary(salary);
    this.setCommission(commission);
    this.setJob(job);
  }
  private String name;
  private double salary;
  private double commission;
  private String job;
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
  public void setSalary(double salary) {
    this.salary = salary;
  }
  public double getSalary() {
    return salary;
  }
  public void setCommission(double commission) {
    this.commission = commission;
  }
  public double getCommission() {
    return commission;
  }
  public double getIncome() {
    return salary + commission;
  }
  public void setJob(String job) {
    this.job = job;
  }
  public String getJob() {
    return job;
  }
}

* create class HrmService.java

package nl.amis.hrm.model;
import java.io.*;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
public class HrmService  {
  private Collection employees;
  public HrmService() {
    employees = new Vector();
    employees.add(new Employee("John Doe", 5000,1000,"Account Manager"));
    employees.add(new Employee("Jane Doe", 7000,1500,"Account Manager"));
    employees.add(new Employee("Jim Jummy", 9000,0,"Sales Director"));
  }
  public Collection getEmployees() {
    return employees;
  }
}

* create DataControl from class HrmService from the RMB menu on HrmService (or drag and drop of the class to the DataControl Palette)
* select HrmService.xml, select employees in the Structure Window and open the Property Inspector; set the BeanClass property on Employees to the Employee Bean

Steps to implement the WebApplication

in the View Project:
* open the struts-config.xml PageFlowDiagram; add a DataPage
* edit the DataPage as editEmployeeForm.uix
* drag and drop the HrmService.employees data control as Input Form (with navigation) to the page
* select the messageTextInput ${bindings.income}; in the Property Inspector: edit the property Id – set the property to income (any value will do, as long as it is unique within the page)
* select the messageTextInput ${bindings.salary}; in the Property Inspector: edit the property primaryClientAction: check all three checkboxes, select the income-messageTextInput in the list for FirePartialTargets
* select the messageTextInput ${bindings.commission}; in the Property Inspector: edit the property primaryClientAction: check all three checkboxes, select the income-messageTextInput in the list for FirePartialTargets

Run the Action DataPage1 from the Struts-Config.xml file: you will see the Edit Employee Form with navigation buttons and data for three employees. When you edit the salary or commission values and leave the respective fields, after a very short delay you will see the income being refreshed!

You can download the JDeveloper 10.1.2 Workspace with all code here: AjaxUix.zip

Next Steps – Server-Side validations and Message Generation

Any item with a Partial Client Action defined against will cause the server to be called (virtually invisibly) to update the model and refresh the target item(s). We can make use of this mechanism for other objectives than just refreshing derived field values. Another popular option would be to implement instantaneous field-level validations through server side implementation. In the simple example of our HrmApplication we could implement a warning message to our end user whenever someone is awarded a salary over ten thousand. We make the following steps:

1. Extend DataForwardAction for our Data Page – in the Page Flow Diagrammer: select the dataPage1 Data Page icon (for editEmployeeForm) and select Go to Code from the RMB menu. This will open a dialog where the name of the DataForwardAction extension class can be entered. Accept the default: DataPage1Action.

2. Implement the onUpdateSalary() method to intercept the event update of salary – whenever the salary is updated in client, the primary client action on the salary item causes the server to be invoked; the model is updated with the new salary value. Next, if the event triggered in the Client has a Server-side counterpart – an onEvent method in the DataAction class – that method will be invoked. In our case:

package nl.amis.timeregistration.view;
import oracle.adf.controller.struts.actions.DataForwardAction;
import oracle.adf.model.binding.DCBindingContainer;
import oracle.adf.model.binding.DCControlBinding;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
public class DataPage1Action extends DataForwardAction  {
  public void onUpdateSalary(oracle.adf.controller.struts.actions.DataActionContext actionContext)	{
    DCBindingContainer bindings = actionContext.getBindingContainer();
    DCControlBinding binding = bindings.findCtrlBinding("salary");
    float salary = (binding != null) ? Float.parseFloat(binding.toString()) : 0;
    if (salary > 10000) {
      binding = bindings.findCtrlBinding("name");
      String name = (binding != null) ? binding.toString() : "";
      ActionMessages messages = (ActionMessages)actionContext.getHttpServletRequest().getAttribute(Globals.MESSAGE_KEY);
      if (messages==null) {
        messages = new ActionMessages();
      }
      messages.add(ActionMessages.GLOBAL_MESSAGE,new ActionMessage("message.salary.stiff", Float.toString(salary)), name);
      saveMessages(actionContext.getHttpServletRequest(), messages );
      // see http://forums.oracle.com/forums/thread.jsp?forum=83&thread=235723&message=672912
    }
  }
}

In this example we locate the ControlBinding called salary and retrieve its current value. If that value is over 10000, we will send our end user a warning. First we get hold of the name of the employee. Than we get hold of the ActionMessages object in the current request – if there is one. If there is none, we create one. Then we add our warning message to the ActionMessages object. The message refers to a message key corresponding to the key message.salary.stiff in the application.properties resource bundle.

3. Add the message to the resource bundle – add the message we want to show to our end user to the resource bundle ApplicationResource.properties:

message.salary.stiff=Is {0} not a bit nuch for {1}?

4. Prepare the UIX page for Struts ActionMessages – as per the instructions by Duncan Mills in the OTN Forum post http://forums.oracle.com/forums/thread.jsp?forum=83&thread=235723&message=672912, I make some changes to the editEmployeeForm.uix page – to make it aware of the Struts ActionMessages and display those in a messageBox.

a. Add namespace to the page-element:

      xmlns:struts="http://xmlns.oracle.com/uix/struts"

b. Add a messageBox in the Struts DataScope:

                    </struts:dataScope>
                     <struts:dataScope xmlns="http://xmlns.oracle.com/uix/ui"
                                       xmlns:data="http://xmlns.oracle.com/uix/ui">
                      <contents>
                        <messageBox id="messageBox" messageType="error" />
                      </contents>
                    </struts:dataScope>

c. Voeg de messageBox toe aan de target voor de primaryClientAction van het salary item:

                              <primaryClientAction>
                                <irePartialAction formSubmitted="true" unvalidated="true" event="updateSalary" targets="income messageBox"/>
                              </primaryClientAction>

Generic ActionMessages to UIX Message conversion

A different aproach is provided in Chapter 5 of the excellent JHeadstart Developer’s Guide:

With default ADF UIX, only model exceptions of type JboException are shown to the end user. Struts action errors are ignored. A possible workaround could be to write code to throw a new JboException for each Struts ActionError object, but then still the informational Struts action messages (ActionMessage objects) would not be shown. JHeadstart solves this by converting Struts ActionError / ActionMessage and ADF JboException objects to a UIX MessageData object, so the standard ADF UIX message box will display them to the user: <messageBox model="${data}"/>

This is implemented in the JhsUixStrutsLifecycle class (for more information about this class see section ‘JHeadstart PageLifecycleFactory’), by overriding the ADF lifecycle phase reportErrors(). This method has three stacks of messages to convert:
1. Model exceptions that are stored on the binding container (= the runtime representation of the page UI model). These are converted to Struts Action errors using the ReportingUtils described in the previous section, so that they can be picked up in the third step.
2. Standard Struts action messages, stored on the request with key Globals.MESSAGE_KEY, are converted to UIX MessageData.
3. Standard Struts action errors (including the result of step 1), stored on the request with key Globals.ERROR_KEY, are converted to UIX MessageData.
These messages are collected in a single UIX MessageData object, which is then stored on the request under the key ‘_uix.messages’.

To ensure that the UIX servlet does not clear this ‘_uix.messages’ entry, the method JhsInitModelListener.renderStarted() is different than its counterpart method in the default UIX InitModelListener class If the ‘_uix.messages’ entry already has a value, it is not cleared.

I have attempted to follow the instructions from the JHeadstart Developer’s Guide and be inspired by specific pieces of code from the JHeadstart source code:
1. Write my own class MyStrutsUixLifecycle extends StrutsUixLifecycle, that creates a UIX MessageData object with all Struts ActionMessages

  /**
   * Convert Struts ActionErrors and ActionMessages and Model
   * Exceptions to UIX MessageData, so they will be displayed by the
   * standard ADF UIX message box.
   * 
   *   <messageBox model="${data}"/>
   * 

*
* @param lcContext The lifecyclecontext holding the state of the lifecycle.
*/
public void reportErrors(LifecycleContext lcContext)
{
// super.reportErrors(lcContext);
MessageData md = new MessageData();
HttpServletRequest request = lcContext.getHttpServletRequest();
DataActionContext dac = (DataActionContext)lcContext;
BindingContext bc = dac.getBindingContext();
Locale locale = null;
if (bc==null)
{
// binding context can be null when there is an error creating
// the binding context, for example when a servlet filter executed
// before the adf binding filter has forwarded to some URI,
// bypassing the ADF binding filter that creates the context!
locale = Locale.getDefault();
}
else
{
locale = dac.getBindingContext().getLocaleContext().getLocale();
}
// Get resource bundle to translate the message
// Struts stores default resource bundle under Globals.MESSAGES_KEY
MessageResources resources = (MessageResources)request.getSession().getServletContext().getAttribute(Globals.MESSAGES_KEY);
// We have this stacks of messages to convert:
// - Standard Struts ActionMessages, stored on request with key Globals.MESSAGE_KEY
ActionMessages messages = (ActionMessages)request.getAttribute(Globals.MESSAGE_KEY);
if (messages!=null)
{
Iterator msgit = messages.get();
while (msgit.hasNext())
{
// translate message and add to UIX MessageData
ActionMessage message = (ActionMessage)msgit.next();
String text = resources.getMessage(locale,message.getKey(),message.getValues());
md.addInfo(null,text,null);
}
}
lcContext.getHttpServletRequest().setAttribute("_uix.messages", md);
} // end reportErrors

2. Write my own LifeCycleFactory: UIXStrutsPageLifeCycleFactory that returns MyStrutsUixLifeCyle

package nl.amis.ajaxuix.view;
import nl.amis.ajaxuix.view.MyStrutsUixLifecycle;
import oracle.adf.controller.struts.actions.StrutsPageLifecycle;
import oracle.adf.controller.struts.actions.StrutsPageLifecycleFactory;
public class UIXStrutsPageLifeCycleFactory   extends StrutsPageLifecycleFactory
{
    /**
   * Gets PageLifecycle for a page path. Currently not used, implementation
   * overridden in all subclasses.
   * @return The pagelifecycle.
   * @param path The path of the page.
   */
   public StrutsPageLifecycle getPageLifecycle(String path)
   {
       return getUIXLifeCycle();
   }
  /**
   * Gets the UIXLifecycle. Creates a new UIXLifecycle when there isn't one
   * already.
   * @return The UIXLifecycle.
   */
   protected static StrutsPageLifecycle getUIXLifeCycle()
   {
      if (UIX_LIFECYCLE == null)
      {
         UIX_LIFECYCLE = new MyStrutsUixLifecycle();
      }
      return UIX_LIFECYCLE;
   }
  /**
   * UIXLifecycle member.
   */
   public static StrutsPageLifecycle UIX_LIFECYCLE;
}

3. Configure a plugin for this factory in the struts-config.xml

  <plug-in className="oracle.adf.controller.struts.actions.PageLifecycleFactoryPlugin">
      <set-property property="lifecycleFactory" value="nl.amis.ajaxuix.view.UIXStrutsPageLifeCycleFactory"/>
   </plug-in>

4. Write my own UIXInitModelListener extends oracle.cabo.adf.rt.InitModelListener that will prevent the _uix.messages attribute on the Request from being overwritten in the renderStarted() method if it already exists

  /**
   * Don't clear _uix.messages if it already has a value. Or rather: save it before
   * calling the super.renderStarted and reinstate afterwards if it was not null
   * before calling super.
   */
    public void renderStarted(UIXRequestEvent uixrequestevent)
    {
        HttpServletRequest httpservletrequest = _getRequest(uixrequestevent);
        Object messageObject = httpservletrequest.getAttribute("_uix.messages");
        super.renderStarted(uixrequestevent);
        if (messageObject!=null)
        {
           httpservletrequest.setAttribute("_uix.messages", messageObject);
       }
    }

5. Configure the oracle.cabo.servlet.UIXRequestListeners parameter in the uix servlet mapping in the web.xml file to ensure my UIXInitModelListener class gets used

  <servlet>
    <servlet-name>uix</servlet-name>
    <servlet-class>oracle.cabo.servlet.UIXServlet</servlet-class>
    <init-param>
      <param-name>oracle.cabo.servlet.pageBroker</param-name>
      <param-value>oracle.cabo.servlet.xml.UIXPageBroker</param-value>
    </init-param>
    <init-param>
      <param-name>oracle.cabo.servlet.UIXRequestListeners</param-name>
      <param-value>nl.amis.ajaxuix.view.UIXInitModelListener</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

6. Voeg de messageBox toe aan de target voor de primaryClientAction van het salary item

                              <primaryClientAction>
                                <irePartialAction formSubmitted="true" unvalidated="true" event="updateSalary" targets="income messageBox"/>
                              </primaryClientAction>

This ensures that when the salary item is updated and the primaryClientAction is fired, the response from the partial server-side communication is also used to refreshed the messageBox. (I had forgotten this step and lost most of my weekend over it)

Now whenever the salary item is updated, the onUpdateSalary method is executed, potentially leading to the message that this salary is perhaps overdoing it.
AJAX with UIX - PrimaryClientAction for instantaneous server-based client refresh

The complete JDeveloper 10.1.2 Application Workspace with my attempts at server side validation and message creation can be downloaded here: AjaxUixMessages.zip. Any clues as to how to get the message displayed in the client are most welcome!

Resources

AJAX in Wikipedia
AJAX in Action – interesting example of using AJAX with JavaRSS
Ajax Matters – AJAX Matters AJAX Matters is an informational site about AJAX (short for “Advanced Javascripting and XML” or “Asynchronous JavaScripting and XML”) and how these technologies are applied to web development.
JHeadstart Weblog – my initial inspiration for this post.

2 Comments

  1. Jaiganesh July 28, 2006
  2. santosh July 30, 2005