How To Create Multi Row Edit Forms in a JSP Page with Oracle JDeveloper 10g

Written By Duncan Mills, Oracle Corporation
May 2004

Introduction

Oracle JDeveloper 10g, using the Oracle Application Development Framework (ADF) provides the JSP page developer with out of the box functionality for binding input forms on JSP pages to business service data. The development environment and framework allow the developer to create HTML rendered forms capable of full Create, Update and Delete functionality with no coding using a combination of the ADF model layer metadata and specialized Struts Actions which automatically handle the data transfer, record navigation and transactional state of the screen for the developer.

However, this code free solution is based around single row input for a record set. This paper covers the situation where the developer is tasked with creating a more sophisticated multi-row input form often emulating a grid or spread sheet type of interface on a JSP based web page. Although this is a less traditional interface for web based applications it is becoming more popular as desktop style systems are migrated or emulated on HTML client deployment.

Here is an example of the style of screen that this article is discussing.


Figure 1 - JSP screen with multi-row update capability

Screens like this may consist of one or more updateable columns mixed in with read only data, the technique readily handles any such mix.

Outline of the Technique

The Apache Struts controller, which is responsible for the page flow in most ADF applications, already has the functionality to handle multi-row input and make that data available to the developer in a simple to consume fashion, although it must be stressed that the responsibilities of Struts stop with making the information available and that framework provides no model layer integration to further help the developer reconcile that data with their data store.

Once Struts has marshaled the data off of the request into an array suitable for processing we can override the relevant part of the ADF Data Action lifecycle to handle the processing of all of the updated rows at once rather than the single update row that it normally dealt with. This customized method can feed the updated data to the ADF model layer in a controlled way and all of the other default functionality of the ADF Model / Struts integration such as record scrolling and transactional state are maintained.

The implementation as it stands requires a certain amount of coding for each multi-row update form that has to be created, it is not at this stage a generic framework component that can be driven by metadata.

The implementation in this example, is based around the use of ADF Business Components as the business service provider, although the principles are extendable to any updateable collection exposed through the ADF model.

The Sample

This document will take you through the process of building a multi-row update screen for the Employees table from the standard Oracle HR demo schema. The screen will allow the user to update the salary and commission columns of the Employees table only. You can download the completed workspace from here.

Implementation

Create a Row Transfer JavaBean

The first step of the process is to define, in your ViewController project, a simple JavaBean to represent those attributes that will be updateable on the multi-row form. This bean does not have to extend any of the Struts Bean classes such as ActionForm or DynaActionForm, it can simply extend Object. The bean that is defined needs to have one String member variable for each of the updateable attributes in the View Object. This member variable needs to have both a getter and setter and have the same name as the View Object attribute. For example, for the View Object attribute "Salary", this JavaBean should have a member variable called "salary" and a corresponding getSalary() and setSalary() method. The conversion from String to the correct data type for the attribute will be handled by the framework.

As well as the attributes that you wish to update, you will need to add an extra attribute to take the row identifier that will match the update to the correct row in the collection. This should implemented in your JavaBean as a member variable called rowKeyStr, of type String and with the corresponding getRowKeyStr() and setRowKeyStr() methods.

Here is the code for the JavaBean which will be used to support the multi row update of the Salary and CommissionPct columns in the employees table.

package multiRowUpdate.view;


public class UpdateRowBean
                                    
{
private String salary;
private String commissionPct;
private String rowKeyStr;

public UpdateRowBean()
{
}
public String getSalary()
{
return salary;
}
public void setSalary(String salary)
{
this.salary = salary;
}
public String getCommissionPct()
{
return commissionPct;
}
public void setCommissionPct(String commissionPct)
{
this.commissionPct = commissionPct;
}
public String getRowKeyStr()
{
return rowKeyStr;
}
public void setRowKeyStr(String rowKeyStr)
{
this.rowKeyStr = rowKeyStr;
}
}
Figure 2 - UpdateRowBean code

Create a BeanInfo Class for the Row Transfer Bean

In addition to the basic row-transfer bean you will, in most cases, have to create a corresponding BeanInfo class to help Struts correctly map to the updateable attributes on the screen. You will not have to carry out this step if all of the multi-row updateable attributes in the View Object are named with an lower case first letter. For instance if the View Object attribute for commission is called "commissionPct" as opposed to "CommissionPct". However, in most View Objects, the attributes will be init-capped so you will have to create the BeanInfo class.

The purpose of the BeanInfo class is to map the attribute names as exposed though the View Object to be mapped to the getters and setters defined in your row transfer bean. In the default JavaBean implementation, a setter called setSalary() in the transfer bean would map to a request attribute of "salary" posted from the page. In this case if our attribute names are initcapped e.g."Salary" this would not match, and the setter would never be called. The BeanInfo allows us to tell the introspection mechanism that it's valid to call "setSalary" for a parameter called "Salary" with an uppercase initial letter.

To create the BeanInfo Class, you need to create a new Java class with the name <transferbeanclass>BeanInfo e.g. UpdateRowBeanBeanInfo, which extends java.beans.SimpleBeanInfo. This class should be in the same package as the row transfer bean. The class needs a single public method called getPropertyDescriptors() which returns an array of PropertyDescriptor objects, one for each attribute (plus one for the rowKeyStr). Note that JDeveloper has a BeanInfo entry in the General > JavaBeans branch of the New Gallery to help you with the creation of the BeanInfo class.

Inside the getPropertyDescriptors() method you need to create a PropertyDescriptor for each of the attributes, passing the name of the attribute in the same case as defined in the View Object, a reference to the row transfer bean class and the named getter and setter to handle that attribute in the transfer bean class.

Here is the BeanInfo class for the above row transfer bean:

package multiRowUpdate.view;
                                    
import java.beans.SimpleBeanInfo;
import java.beans.BeanDescriptor;
import java.beans.PropertyDescriptor; public class UpdateRowBeanBeanInfo extends SimpleBeanInfo
{ public PropertyDescriptor[] getPropertyDescriptors()
{
try
{
PropertyDescriptor rowKeyStr = new PropertyDescriptor("rowKeyStr", UpdateRowBean.class, "getRowKeyStr", "setRowKeyStr");
PropertyDescriptor salary = new PropertyDescriptor("Salary", UpdateRowBean.class, "getSalary", "setSalary");
PropertyDescriptor commPct = new PropertyDescriptor("CommissionPct", UpdateRowBean.class, "getCommissionPct", "setCommissionPct");
PropertyDescriptor[] pds = new PropertyDescriptor[] {rowKeyStr, salary, commPct};
return pds;
}
catch(Exception e)
{
e.printStackTrace();
return null;
} }
}
Figure 3 - BeanInfo class for the UpdateRowBean

 

Create a Struts Form Bean

The row transfer bean that you have created, will be populated with the values entered for a particular row in the screen. You now need to create a Struts Form-Bean definition which will tell the Struts controller that it needs to handle the page update using an array of your custom row transfer beans. No additional code is required for this step just a form-bean definition in the Struts configuration file. You can create this either using the structure pane and property inspector, or you can add the definition directly to the XML:

<form-bean name="MultiRowUpdateForm" type="org.apache.struts.action.DynaActionForm">
  <form-property type="multiRowUpdate.view.UpdateRowBean[]"
                                    
name="Row"
size="10"/> </form-bean>
Figure 4 - Struts <form-bean> definition

The important element here is the <form-property> which defines that the Form-Bean should be made of an array of UpdateRowBean objects (the transfer bean created above), in this case with 10 elements ( as defined by the size attribute). The size of the array should match the size of your table which is controlled by the Range Size property of the data collection iterator which has a default value of 10 rows. It is also important that the name attribute of the <form-property> is set to Row. This has to match the value of the var attribute of the <c:forEach> loop in the update page, which will be created as the value Row when you create an ADF data bound table. .

Create the Basic UI

Creating the screen involves the initial following tasks

  1. Create a new DataPage on the Struts Page flow
  2. Drill into the DataPage, setting the page name as required and the type of page to JSP
  3. Open the component palette, and select the Struts Html palette set.
  4. Drag and drop the form element from the palette onto the page. A dialog will pop up with attributes for the <html:form> tag. You must supply a value for the action attribute. From the drop down list in the value column, select the name of the DataPage that you just created. For example multiRowUpdateScreen.do.
  5. Switch to the Data Controls palette and locate the View Object that the screen needs to be based on.
  6. In the drop down list at the base of the palette, set the Drag and Drop As value to Read-Only Table, and drag the View Object from the palette into the JSP page. Be sure to drop the table inside of the Form control that you have just created.
  7. Expand the Operations node for the View Object in the Data Control palette and drag and drop the Next Set and Previous set operations into the Form as well. This will allow you to scroll through the table.
  8. [Optionally] Expand the top level Operations node and drag the commit button in the form as well so you can commit your changes.
  9. In the table that you dragged in from the View Object collection, remove all the columns you don't want to see in this case. In our sample we'll just need to view the Employee Id, First Name, Last Name and of course the Salary and Commission Pct columns.
  10. Now we have the correct tabular view of the data (you may want to run the DataPage at this stage to make sure you are seeing the correct things). The next step is to replace the display versions of the updateable fields with input fields. Switch to the component palette and drag a text component from the Struts Html palette into your page. When you drop the tag, a dialog will pop up. Set the property attribute to the name of the View Object attribute that you want to update, e.g. Salary. The indexed property should be set to true, and the name property set to Row.

    Figure 5 - Setting the <html:text> properties
  11. Repeat this exercise for each updateable field
  12. Finally add a hidden field (an <html:hidden> tag from the Struts Html components). Place this within the <c:forEach> loop so that one instance is created for each row. Set the property attribute of this hidden field to rowKeyStr, the name attribute to Row and the indexed attribute to true. This hidden field is important because it will help us to uniquely identify each row to apply the updates correctly.

Assign the Custom Form Bean to the DataPage

Next we have to link the form-bean that Struts will use to process user input from the page, with the DataPage action that will need access to that data. In the page flow diagram, or the structure pane, select the DataPage that supports the multi-row update form. Set it's name property to the name of the form-bean that you created earlier (e.g. MultiRowUpdateForm) and set the scope property to request.


Figure 6 - Assigning the custom form-bean to the DataPage

Extend the DataPage Action

As mentioned in the introduction, the DataPages (and DataActions) are set up to process single row updates, so we have to add a little extra code to handle a multi-row array of updates. To do this you need to subclass the DataPage. The simplest way of doing this is to select the action on the page flow and choose Go To Code from the context menu. Once you have a DataPage subclass you need to override the processUpdateModel() method. Struts will already have done most of the work for you and marshaled the data from the Form fields into an array of UpdateRowBeans. You will now have to loop through these rows, check to see which contain real updates and then apply those updates.

Here are the various steps that have to be implemented:

  1. Obtain the array of UpdateRowBean objects from Struts. These beans will contain all the values posted from the screen, some or all of which may have been updated by the user.
  2. Obtain the original set of rows so you can do a compare and only make updates where required
  3. Loop through each submitted row bean in turn and compare the values for each of the input fields with the stored values in the record collection from the View Object.
  4. For each row and attribute, if there has been a change in an attribute, then update the model with that change.
  5. Force validation on the DataControl if any row changes were made.

The code for all of this is not complex, the major things that you have to watch for are data type conversion errors due to invalid user input, and deciding how you should handle Empty input. Let's look at each step in turn for the Salary / Commission update screen.

1. Obtain the UpdateRowBean from Struts

Native Struts functionality has done most of the hard work for us here, and has marshaled the data from the indexed input fields (Salary, CommissionPct and rowKeyStr) into an array of our data transfer beans, in this example called multiRowUpdate.view.UpdateRowBean.

Here's the code fragment:

//Get the array of beans created by Struts from the Form
                                    
DynaActionForm updateForm = (DynaActionForm)ctx.getActionForm();
UpdateRowBean[] updatedRows = (UpdateRowBean[])updateForm.get("Row");
Figure 7 - Populating the array of updated rows

The ctx variable is the DataActionContext that is passed by the framework into lifecycle methods such as processUpdateModel. This context variable gives you access to the request and other useful Struts artifacts such as the ActionMapping and the form-bean. We use ctx.getActionForm() to get hold of the form-bean that Struts has populated from the page. The array of UpdateRowBean objects is then extracted from that form bean. So we now have an array of "row" objects that can be processed.

2. Get the original set of records for comparison

The following code will return us a list object containing the actual model rows.

//Get the original rowset  
                                    
java.util.List origRows = (java.util.List)DCUtil.findSpelObject(ctx.getBindingContainer(), "EmployeesVO.rangeSet");
Figure 8 - Getting the existing rowset

The DCFindSpelObject programmatically references the same data as the <c:forEach> loop which drives the table on the JSP page and uses the same expression (minus the bindings keyword) as that tag

3. Loop through the submitted updates

The Struts form-bean will have supplied you with an array of your row transfer beans with whatever size was defined in the <form-bean> definition. You will always receive this number of rows, no matter how many rows are actually displayed on the screen or how many rows the user has changed. So the code at this stage that loops through the array needs to handle that by checking the submitted values against the original rowset, matching on the rowKeyStr.

// Now loop through the bean array
                                    
for (int i = 0; i < origRows.size(); i++)
{
//Get the rowKey of the submitted updated Row
String theRowKey = updatedRows[i].getRowKeyStr();
//Get the corresponding row from the origional set
JUCtrlValueBindingRef oldRow = (JUCtrlValueBindingRef)origRows.get(i);

//Check the update row needs processing and matches the source row
if (oldRow.get(JUCtrlValueBindingRef.RANGESET_ROW_KEY_STR).equals(theRowKey))
{ ... Handle the attributes ...
}
}
Figure 9 - Looping through the user updates

We extract the original row that should match the updated row and compare it's rowKey with the key that came with the submitted row. If they match we can then start to examine individual attributes to see which need to be updated.

4. Compare the transfer bean value with the Model value.

Within the loop you now have both the updated values from the UI and the current values stored in the model. All that you have to do now is to compare the two and decide, for each attribute, if the model needs to be updated. This is the part of the process where you have to code defensively. The user may have entered invalid values in the UI, for instance, a string for a number field, so the code should anticipate the possible inputs and either ignore bad values, or raise an appropriate error.

You may find it simplest to create a convenience method that handles the job of comparing the new and old values, updating the model if required. Here is a simple example which you can adapt. Note that for brevity this version does not do any defensive error handling - you should do so in your version.

private void updateIfReqd(JUCtrlValueBindingRef baseRow, String attr, String newVal)
                                    
{
Object baseAttrValue = baseRow.get(attr);
if ((baseAttrValue == null && (newVal == null || newVal.length() == 0)) ||((baseAttrValue != null && baseAttrValue.equals(newVal))))
{
return;
} baseRow.put(attr, newVal); this.setDirtyState(true); }
                                     
Figure 10 - A basic function for conditionally updating the model

Using this convenience method, all the attributes can be tested and pushed to the model if changed.

 //Process each attribute
 updateIfRequired(oldRow,"Salary",updatedRows[i].getSalary());
                                    
updateIfRequired(oldRow,"CommissionPct",updatedRows[i].getCommissionPct());
Figure 11 - Processing the attributes from the form submission (Error handling omitted)

5. Validate the model changes

If changes were applied to the model based on the input data, then you need to validate those changes so as to catch any validation checks applied by the business service. This validation will include (in the ADF Business Components case) things like precision errors and declarative validation rules created against the underlying entity object. This code overrides another lifecycle method and just has to defer to the data control to handle the validation if it is required.

protected void validateModelUpdates(DataActionContext ctx)
                                    
{
if (isDirty())
{
ctx.getBindingContainer().getDataControl().validate();
setDirtyState(false);
}
}
Figure 12 - Validating model changes

Note the use of isDirty() and setDirtyState() which are private convenience methods used to maintain a flag indicating that validation is required.

Complete Data Action code

For reference here is the complete code for the employees example, including the error handling routines. You should adapt and improve this code according to the needs of the input screen in question. For instance, you may want to add more upfront validation to the attributes before they are submitted to the model, so that you have more control over the display and content of error messages.

package multiRowUpdate.view;
import org.apache.struts.action.DynaActionForm;
import oracle.adf.controller.struts.actions.DataActionContext;
import oracle.adf.controller.struts.actions.DataForwardAction;
import oracle.adf.model.binding.DCUtil;
import oracle.jbo.uicli.binding.JUCtrlValueBindingRef;
                                    
public class EmployeesAction extends DataForwardAction { private boolean mHasChanges = false; /** * Overridden lifcycle method that applies changes to a whole * row array at once, rather than a single row * @param ctx */ protected void processUpdateModel(DataActionContext ctx) { //Get the array of beans created by Struts from the Form DynaActionForm updateForm = (DynaActionForm)ctx.getActionForm(); UpdateRowBean[] updatedRows = (UpdateRowBean[])updateForm.get("Row"); //Get the original rowset java.util.List origRows = (java.util.List)DCUtil.findSpelObject(ctx.getBindingContainer(), "EmployeesVO.rangeSet"); // Now loop through the bean array for (int i = 0; i < origRows.size(); i++) { //Get the rowKey of the submitted updated Row String theRowKey = updatedRows[i].getRowKeyStr(); //Get the corresponding row from the origional set JUCtrlValueBindingRef oldRow = (JUCtrlValueBindingRef)origRows.get(i); //Check the update row needs processing and matches the source row if (oldRow.get(JUCtrlValueBindingRef.RANGESET_ROW_KEY_STR).equals(theRowKey)) { //For each attribute update it if required updateIfReqd(oldRow, "Salary", updatedRows[i].getSalary()); updateIfReqd(oldRow, "CommissionPct", updatedRows[i].getCommissionPct()); } } } /** * Overridden lifecycle method used to validate any model updates if * the validation state is marked as dirty * @param ctx */ protected void validateModelUpdates(DataActionContext ctx) { if (isDirty()) { ctx.getBindingContainer().getDataControl().validate(); setDirtyState(false); } } /** * A convenience method for checking to see if a real change has been made to * a particular attribute in a row and applying it if needs be. * @param baseRow a reference to the source row that the update will be applied to * @param attributeName the name of the attribute to update * @param newValue the new value to apply to the attribute in this row */ private void updateIfReqd(JUCtrlValueBindingRef baseRow, String attr, String newVal) { Object baseAttrValue = baseRow.get(attr); if ((baseAttrValue == null && (newVal == null || newVal.length() == 0)) || ((baseAttrValue != null && baseAttrValue.equals(newVal)))) { return; } //Push the change to the base row baseRow.put(attr, newVal); setDirtyState(true); } /** * Convenience method for setting the validation flag * @param dirty */ private void setDirtyState(boolean dirty) { this.mHasChanges = dirty; } /** * Convenience method for getting the validation state * @return */ private boolean isDirty() { return mHasChanges; } }
Figure 13 -Completed DataPage subclass code

Restrictions

  1. This technique currently has one flaw which relates to the handling of model validation. If a value is posted which beaks a validation rule such as a number precision, the error will be correctly reported to the user in the standard Struts <html:error> tag. However, the values shown in the updateable fields will revert to the original pre-update values, so it may be difficult for the user to locate which field was in error. This is an unfortunate side effect of the mechanics of this technique and cannot be simply resolved. We would therefore recommend that you aggressively validate input from the screen before updating the model row. This will allow you more control over the errors raised and their context.
  2. This technique cannot be applied to ADF UIX based pages as they do not use the Struts form-bean mechanism.

Troubleshooting

Problem: Running the page results in a Server 500 error and an message like: javax.servlet.jsp.JspException: Cannot find bean <name> in any scope.
Solution: Check all of your updateable attributes and the hidden rowKeyStr item and make sure that the name property matches the Name property of the <form-property> element under the Struts form-bean that you created. This value should normally be Row, a value of "row" in the JSP page would cause this error..

Problem:The page runs but the enterable fields are empty even though there is data in the table.
Solution: The value of the property attribute of the <html:text> tag needs to match the View Object Attribute exactly. Check that the value in the JSP is in the same case as the View Object attribute.

Problem:When the page is submitted the columns come back with the old values and no error is shown.
Solution: You are probably suffering from a naming mismatch between the attributes as used in the property= attribute of the <html:text> tag and the row transfer bean. You may need a BeanInfo class to handle the naming mismatch.

Problem: When the page is submitted the following error is displayed: JBO-29000 - Unexpected Exception - java.lang.UnsupportedOperationException
Solution: Check the names of the attributes in your processUpdateModel() method of the DataPage. If you use an incorrectly named attribute in the put() call on the model row this error can happen.

Problem: When the page is submitted a validation error is shown, but the grid now shows the original values not the value that caused the error.
Solution: See the restrictions above. You will have to add some manual validation checking if you want to capture this.

Summary

Hopefully this article has shown how ADF based JSP applications can be extended to provide sophisticated multi-row user input with little custom coding. More formal support for multi-row input will appear in future versions of the framework, but in the meantime this solution can be used to satisfy your multi-row input needs.

drmills v1.2 07/July/2004

false ,,,,,,,,,,,,,,,