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
- Create a new DataPage on the Struts Page flow
- Drill into the DataPage, setting the page name as required and the type of page to JSP
- Open the component palette, and select the Struts Html palette set.
- 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.
- Switch to the Data Controls palette and locate the View Object that the screen needs to be based on.
- 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.
- 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.
- [Optionally] Expand the top level Operations node and drag the commit button in the form as well so you can commit your changes.
- 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.
- 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
- Repeat this exercise for each updateable field
- 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:
- 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.
- Obtain the original set of rows so you can do a compare and only make updates
where required
- 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.
- For each row and attribute, if there has been a change in an attribute,
then update the model with that change.
- 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
- 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.
- 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
|