Skip navigation.
Home

Dynamic list binding in Spring MVC... what? why?

Spring IconPreviously, I wrote an article on how to achieve dynamic list binding in Spring MVC. Since I wrote that article, I have received emails and comments that ask (in a roundabout way) Why would I need to do this? When I wrote the other article I didn't explain the why; I just wrote the how. Here's why you need dynamic binding and the problems it is trying to solve.

Example

Let's say that you have an application that allows you to modify a Hand. As in the thing at the end of your arm. You have a two class object model: Hand and Finger. A Hand has Fingers. More specifically, a Hand has a List of Fingers. For the impatient (who don't want to read the Why section), I created an online application to demonstrate some of the issues.

Why

Here are some reasons why you need dynamic binding in general:

  1. Creating the object graph takes a really long time
  2. The object graph could have changed between subsequent page calls
  3. The page allows for list changes via javascript

Creating the object graph takes a really long time
Let's say that creating a Hand from its persistent storage takes a really long time. You will want to minimize the number of times you go to that persistent storage. Spring invokes the formBackingObject() method in controllers to create object graphs that will be used with web forms.

In our example, when the Edit Hand page is being loaded, the formBackingObject() method is invoked and the page (with the form) is displayed. When that page is submitted, Spring will invoke the formBackingObject() method and then overlay the results of the form onto the object graph returned from that method invocation. So what just happened? You just invoked an expensive method twice.. once for display, once for submission. The second invocation was completely unnecessary. In order to stop the second invocation, you will have something like this in your formBackingObject() implementation:


if (!isFormSubmission(request)) {
  //create expensive object graph
} else {
  //create non-populated object graph shell
}

So what's the problem? Nothing with regard to the initial page load (provided it isn't from a form submit), but the submission will fail with an Exception. In our example, the List of Fingers on the Hand will be empty when the submission takes place. Spring will then try to get the first Finger out of the List (in order to overlay the values) but will fail because the List is empty.

The object graph could have changed between subsequent page calls
Now let's say that it isn't all that expensive to create a Hand from persistent storage. The double call to formBackingObject() isn't really that big of a deal from a cost (cpu, elapsed time, etc) perspective. Let's say you are on the Edit Hand page typing in your edits. Another user goes to the Edit Hand page, removes a Finger and submits the changes. Now you finish your edits and hit submit. That submit will fail. The reason is the List of Fingers. In the first call, you get a Hand with a List containing five Fingers and those five Fingers are rendered on the page. In the second call, you get a Hand with four Fingers that Spring attempts to overlay the four finger graph with the elements on the form (which has five fingers). The fifth finger will error out.

The page allows for list changes via javascript
On the first call, a Hand with five Fingers is returned and displayed. However, this is one of those fancy web 2.0 kind of pages... one where you can click an Add New Finger button and magically the new Finger appears without submitting the form. Once you finish adding Fingers you eventually click the submit button.. only to fail miserably. Much like the previous two reasons, the mismatch between the number of Fingers on the form and the number of Fingers in persistent storage is the problem. When Spring attempts to overlay the new set of Fingers from the form onto the graph created by formBackingObject() there is a mismatch.. the new form has let's say 12 Fingers.. while the graph from storage still only has five Fingers. When the overlaying of the sixth finger is attempted an Exception is thrown.

Solution

In addition to an explanation of the how, I created an online web application to illustrate the problem and the solution. The source war is also attached to this article below.


Requirement for Dynamic Binding to work

Hi, I have this problem with Weblogic 9.2: I was able to use ${} inside spring tag's path property with Weblogic 8.1, but with 9.2 it doesn't work. Any idea? Can it be because of a missing jar? I tried everything but it doesn't work for me. I get this exception:

javax.servlet.jsp.el.ELParseException: Error occured while trying to parse 'object1.objList[${loopStatus.index}].myOtherObj.id'
	at javelin.jsp.el.ExpressionEvaluatorImpl.parseEL(ExpressionEvaluatorImpl.java:169)
	at javelin.jsp.el.ExpressionEvaluatorImpl.parseExpression(ExpressionEvaluatorImpl.java:132)
	at javelin.jsp.el.ExpressionEvaluatorImpl.evaluate(ExpressionEvaluatorImpl.java:123)
	at org.springframework.web.util.ExpressionEvaluationUtils$Jsp20ExpressionEvaluationHelper.evaluate(ExpressionEvaluationUtils.java:188)

What about deleting a row from the fingers list?

Hi,
What would spring do if we removed a row from fingers. So instead of the index being 0,1,2,3 and so on, if there is a missing index then would spring be able to remove that object from the list?

Matt Fleming's picture

Re: Deletes

I think it depends on your persistence method (and should be handled there). If you are doing a save/update then the fingers wouldn't get deleted. If you are doing a merge/overwrite kind of persistence then the hand would only have the fingers passed to the save method.

-Matt

How to get it work with AbstractWizardForm?

Hi Matt,

Thanks for the sample implementation. I have tried to apply your implementation into my project using AbstractWizardForm. The first page allows user to add new patient to the existing list, the list is then carried to next page. I got exception before it gets into formBackingObject(). I keep getting index out of bound exception. Can you advise me how to approach this problem?

Thanks a lot.

Sum

How to populate a list containing POJOs

Hi Matt,

I read your post and impressed. I am very new to Spring and confused a bit in proper usage of the framework.

I have a situation where my command object holds a list containing POJOs.

Now I am using this list to populate on the JSP and it was successful. But when I tried to submit it is throwing exception. In my case I am not using LazyList, as I thought I donthave a need to add any new objects into the command object. Here is how the classes and JSP looks like in my scenario.

// POJO which needs to be populated

public class UserDefault extends BaseDO 
{
	private Long pk;
	private Long usrId;
	private Resource resource;
	private String isAccessAllowed;
	private Long duration;
	private Integer count;
	private Long countTimeUnit;
	private String accessType;
	private String message;
	private Date expiryDate;
	private Boolean selected;
          .
          .
          .
       // All setters and getters go here
 
}

// Command object holding the list of POJOs

public class UserAccessLimitCO 
{
	private Collection<UserDefault> userDefaultSet;
 
	public Collection<UserDefault> getUserDefaultSet() {
		return userDefaultSet;
	}
 
	public void setUserDefaultSet(Collection<UserDefault> userDefaultSet) {
		this.userDefaultSet = userDefaultSet;
	}
}

// Here is my controller

public class UserAccessLimitController extends SimpleFormController 
{
 
	private UserAccessLimitDAO userAccessLimitDAO = null;
	private UserDAO userDAO = null;
 
	private User loggedUser = null;
 
	private static final Logger LOGGER = Logger.getLogger(UserAccessLimitController.class.getName());
 
	/* Fetch all UserAccessLimit.
	 * Copy them to UserDefault.
	 * Set this to the command object.
	 * (non-Javadoc)
	 * @see org.springframework.web.servlet.mvc.SimpleFormController#referenceData(javax.servlet.http.HttpServletRequest, java.lang.Object, org.springframework.validation.Errors)
	 */
	@Override
	protected Map referenceData(HttpServletRequest request, Object command,
			Errors errors) throws Exception 
	{
		// TODO Auto-generated method stub
 
		loggedUser = (User)request.getSession().getAttribute("authorizedUser");
 
		Collection<UserDefault> userDefaultSet = new LinkedHashSet<UserDefault>(); 
 
//		Collection<UserAccessLimit> ualList = userAccessLimitDAO.findByUserId(loggedUser.getUsrId());
		Collection<UserAccessLimit> ualList = loggedUser.getUserAccessLimits();
		Iterator<UserAccessLimit> iter = ualList.iterator();
 
		while(iter.hasNext())
		{
			UserDefault ud = new UserDefault(iter.next());
			userDefaultSet.add(ud);
		}
 
		LOGGER.info("userDefaultSet = " + userDefaultSet);
 
		UserAccessLimitCO ualCO = (UserAccessLimitCO)command;
 
		ualCO.setUserDefaultSet(userDefaultSet);
 
		LOGGER.info("ualCO.userDefaultSet = " + ualCO.getUserDefaultSet());
 
		return super.referenceData(request, ualCO, errors);
	}
 
 
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception 
	{
		// TODO Auto-generated method stub
		LOGGER.info("-> onSubmit()");
 
		UserAccessLimitCO ualCO = (UserAccessLimitCO)command;
 
		Collection<UserDefault> userDefaultSet = ualCO.getUserDefaultSet();
 
		Set<UserAccessLimit> ualSet = copyFromUserDefaultToUserAccessLimit(userDefaultSet, loggedUser.getUserAccessLimits());
 
		LOGGER.info("ualSet = " + ualSet);
 
		loggedUser.setUserAccessLimits(ualSet);
 
		userDAO.updateUser(loggedUser);
 
		request.getSession().setAttribute("authorizedUser", loggedUser);
 
		LOGGER.info("loggedUser = " + loggedUser);
 
		LOGGER.info("<- onSubmit()");
		return super.onSubmit(request, response, command, errors);
	}
 
 
	// Copies data from user default objects to corresponding fields of user access limit objects.
	private Set<UserAccessLimit> copyFromUserDefaultToUserAccessLimit(Collection<UserDefault> udSet, Collection<UserAccessLimit> ualSet)
	{
		LOGGER.info("-> copyFromUserDefaultToUserAccessLimit()");
//		Collection<UserAccessLimit> ualSet = loggedUser.getUserAccessLimits();
 
		Set<UserAccessLimit> ualToReturn = new HashSet<UserAccessLimit>();
 
		Iterator<UserDefault> udSetIter = udSet.iterator();
		while(udSetIter.hasNext())
		{
			LOGGER.info("1st LEVEL");
			UserDefault tempUd = udSetIter.next();
			LOGGER.info("tempUd = " + tempUd);
			Iterator<UserAccessLimit> ualSetIter = ualSet.iterator();
			while(ualSetIter.hasNext())
			{
				LOGGER.info("\t 2nd LEVEL");
				UserAccessLimit tempUal =  ualSetIter.next();
				if(tempUd.getPk().longValue() == tempUal.getUalId().longValue())
				{
//					tempUal.setUalAccessType(tempUd.getAccessType());
					if(tempUd.getSelected().toString().trim().equalsIgnoreCase("true"))
					{
						tempUal.setUalIsAccessAllowed("Y");
					}
					else
					{
						tempUal.setUalIsAccessAllowed("N");
					} // End of if
					if(tempUd.getCount() == null || tempUd.getCount().toString().trim().equalsIgnoreCase(""))
					{
						tempUal.setUalCount(null);
					}
					else
					{
						tempUal.setUalCount(tempUd.getCount());
					}// End of if
 
					if(tempUd.getDuration() == null && tempUd.getDuration().toString().trim().equalsIgnoreCase(""))
					{
						tempUal.setUalDuration(null);
						tempUal.setUalCountTimeUnit(null);
					}
					else
					{
						tempUal.setUalDuration(tempUd.getDuration());
						tempUal.setUalCountTimeUnit(tempUd.getCountTimeUnit());
					}// End of if
 
					tempUal.setUalAccessType(tempUd.getAccessType());
 
					ualToReturn.add(tempUal);
					break;
 
				}// End of if
			}// End of while
		}// End of while
 
		LOGGER.info("<- copyFromUserDefaultToUserAccessLimit()");
		return ualToReturn;
	}
 
 
	public UserAccessLimitDAO getUserAccessLimitDAO() {
		return userAccessLimitDAO;
	}
 
 
	public void setUserAccessLimitDAO(UserAccessLimitDAO userAccessLimitDAO) {
		this.userAccessLimitDAO = userAccessLimitDAO;
	}
 
 
	public UserDAO getUserDAO() {
		return userDAO;
	}
 
 
	public void setUserDAO(UserDAO userDAO) {
		this.userDAO = userDAO;
	}
 
}

// Here goes my JSP

<?xml version="1.0" encoding="ISO-8859-1" ?>
 
<%@ page contentType="text/html" isELIgnored="false" %>
 
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt_rt" %>
 
<%@page import="com.nsn.rest.domain.User" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
<html>
 
	<head>
		<link href="styles/RESTstyles.css" rel="stylesheet" type="text/css" />
 
		<script type="text/javascript">
 
			function setCount(pk,id)
			{
				var id_length = id.length;
				var indexOf_0 = id.lastIndexOf("0");
				var textCount_id = document.getElementById("textCount_"+pk);
				if(id_length - indexOf_0 == 1)
				{
					textCount_id.disabled = true;
					document.getElementById("count_"+pk).value = "";
				}
				else
				{
					textCount_id.disabled = false;
					document.getElementById("count_"+pk).value = textCount_id.value;
				}
			}
 
			function setDuration(pk,id)
			{
				var id_length = id.length;
				var indexOf_0 = id.lastIndexOf("0");
				var textDuration_id = document.getElementById("textDuration_" + pk);
				if(id_length - indexOf_0 == 1)
				{
					textDuration_id.disabled = true;
					document.getElementById("duration_"+pk).value = "";
				}
				else
				{
					textDuration_id.disabled = false;
					document.getElementById("duration_"+pk).value = textDuration_id.value;
				}
			}
 
		</script>
	</head>
 
	<body>
 
		<div class="myBox">
 
		<!-- This is the Header Section. Oncludes the top logo and top links -->
 
			<div id="Header">
				<div id="h_sec1">
					<div class="titlelogo1">Teleweb</div>
					<div class="titlelogo2">RESTop</div>
				</div>
				<div id="h_menu"> 
				  <div align="right" class="topLinks">Home&nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp;Manage Content&nbsp;&nbsp;&nbsp; : &nbsp;&nbsp;&nbsp;Help | &nbsp;&nbsp;&nbsp;Logout </div>
				</div>
			</div>
 
			<div id="ContentArea">
			  <div id="SideLeft"> 
			    <div class="sideContent">
			    </div>
			   </div>
 
				<table width="100%" border="0" cellpadding="6" cellspacing="6" class="tableBorder1">
 
					<thead>
						<tr>
							<th>Access limits</th>
						</tr>
					</thead>
 
					<tbody>
 
						<tr>
							<td>
								<form action="" method="post" id="ual">
									<table>
 
										<spring:nestedPath path="ualCO">
											<c:if test="${empty ualCO.userDefaultSet}">
												<tr>
													<td>No default limits found/defined</td>
												</tr>
											</c:if>
 
											<c:if test="${not empty ualCO.userDefaultSet}">
												<tr>
													<td>Allow access?</td>
												</tr>
												<c:forEach items="${ualCO.userDefaultSet}" var="ud" varStatus="rowStatus">
													<tr>
														<spring:bind path="userDefaultSet[${rowStatus.index}].selected">
															<td>
																<input type="hidden" name="_<c:out value="${status.expression}"/>"/>
															    <input type="checkbox" name="<c:out value="${status.expression}"/>" value="true" 
															    	<c:if test="${status.value}">checked</c:if>/>
															</td>
														</spring:bind>
 
														<td>
															<table>
																<tr>
																	<td><input type="radio" name="radioCount_<c:out value="${ud.pk}"/>" value="<c:out value="${ud.pk}"/>" onclick='setCount(<c:out value="${ud.pk}"/>,this.id)' id='radioCount_<c:out value="${ud.pk}"/>_0' >unlimited</input> </td>
																</tr>
																<tr>
																	<td><input type="radio" name="radioCount_<c:out value="${ud.pk}"/>" value="<c:out value="${ud.pk}"/>" onclick='setCount(<c:out value="${ud.pk}"/>,this.id)' id='radioCount_<c:out value="${ud.pk}"/>_1' >allow</input></td>
																	<td><input type="text" name="textCount_<c:out value="${ud.pk}"/>" value="<c:out value="${ud.count}"/>" id='textCount_<c:out value="${ud.pk}"/>' size="5"/></td>
																</tr>
																<tr>
																	<spring:bind path="userDefaultSet[${rowStatus.index}].count">
																		<td><input type="hidden" name="count_<c:out value="${ud.pk}"/>" value="" id='count_<c:out value="${ud.pk}"/>' /></td>
																	</spring:bind>
																</tr>
															</table>
														</td>
														<td>&nbsp;<c:out value="${ud.resource.resName}"/>&nbsp; requests &nbsp;</td>
														<td>
															<table>
																<tr>
																	<td><input type="radio" name="radioDuration_<c:out value="${ud.pk}"/>" value="<c:out value="${ud.pk}"/>" onclick='setDuration(<c:out value="${ud.pk}"/>,this.id)' id='radioDuration_<c:out value="${ud.pk}"/>_0' >forever</input> </td>
																</tr>
																<tr>
																	<td><input type="radio" name="radioDuration_<c:out value="${ud.pk}"/>" value="<c:out value="${ud.pk}"/>" onclick='setDuration(<c:out value="${ud.pk}"/>,this.id)' id='radioDuration_<c:out value="${ud.pk}"/>_1' >for</input></td>
																	<td><input type="text" name="textDuration_<c:out value="${ud.pk}"/>"  id='textDuration_<c:out value="${ud.pk}"/>' size="5" value='<fmt:formatNumber value="${ud.duration/86400}" pattern="#####"/>'/></td>
																	<td>
																		<spring:bind path="userDefaultSet[${rowStatus.index}].countTimeUnit">
																			<select name="selectDuration_<c:out value="${ud.pk}"/>">
																				<option value="86400" <c:if test="${status.value == option}">selected</c:if> >Days</option>
																				<option value="604800" <c:if test="${status.value == option}">selected</c:if> >Weeks</option>
																			</select>
																		</spring:bind>
																	</td>
																</tr>
																<tr>
																	<spring:bind path="userDefaultSet[${rowStatus.index}].duration">
																		<td><input type="hidden" name="duration_<c:out value="${ud.pk}"/>" value="" id='duration_<c:out value="${ud.pk}"/>' /></td>
																	</spring:bind>
																</tr>
															</table>
														</td>
														<td>
															<table>
																<tr>
																	<td>
																		&nbsp; to &nbsp; 
																	</td>
																	<td>
																		<spring:bind path="userDefaultSet[${rowStatus.index}].accessType">
																			<select name="selectAccessType_<c:out value="${ud.pk}"/>">
																				<c:choose>
																			        <c:when test='${ud.resource.resName == "SMS" or ud.resource.resName == "sms"}'>
																			            <option value="W" <c:if test="${status.value == option}">selected</c:if> >Write</option>
																			        </c:when>
																			        <c:otherwise>
																			        	<option value="R" <c:if test="${status.value == option}">selected</c:if> >Read</option>
																						<option value="W" <c:if test="${status.value == option}">selected</c:if> >Write</option>
																			            <option value="RW" <c:if test="${status.value == option}">selected</c:if> >Read-Write</option>
																			        </c:otherwise>
																			    </c:choose>
																			</select>
																		</spring:bind>
																	</td>
																</tr>
															</table>
														</td>
													</tr>
												</c:forEach>
											</c:if>
										</spring:nestedPath>
 
										<tr>
									<td>
										<input type="submit" value="Update" />
									</td>
								</tr>
									</table>
								</form>
							</td>
 
						</tr>
 
						<tr>
							<td colspan="2">
								<spring:hasBindErrors name="ualCO">
									<div class="errorform"> 
										<c:out value="${errors.globalError.defaultMessage}" escapeXml="false"/><br/>
									</div>	
								</spring:hasBindErrors>
							</td>
						</tr>
 
					</tbody>
 
				</table>
 
			</div>
		</div>		
	</body>
 
</html>

Hmmm.. thats the code which handles all this.

Sorry to post all the code in here.

When I submit, I have the following exception and here is the trace

org.springframework.beans.NullValueInNestedPathException: Invalid property 'userDefaultSet[0]' of bean class [com.nsn.rest.domain.commandobject.UserAccessLimitCO]: Cannot access indexed value of property referenced in indexed property path 'userDefaultSet[0]': returned null
	at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:547)
	at org.springframework.beans.BeanWrapperImpl.getNestedBeanWrapper(BeanWrapperImpl.java:441)
	at org.springframework.beans.BeanWrapperImpl.getBeanWrapperForPropertyPath(BeanWrapperImpl.java:418)
	at org.springframework.beans.BeanWrapperImpl.setPropertyValue(BeanWrapperImpl.java:635)
	at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:78)
	at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:532)
	at org.springframework.validation.DataBinder.doBind(DataBinder.java:434)
	at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:147)
	at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:108)
	at org.springframework.web.servlet.mvc.BaseCommandController.bindAndValidate(BaseCommandController.java:369)
	at org.springframework.web.servlet.mvc.AbstractFormController.handleRequestInternal(AbstractFormController.java:263)
	at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:153)
	at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:48)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:858)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:792)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:476)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:441)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:709)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:213)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:178)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:126)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:107)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:148)
	at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:869)
	at org.apache.coyote.http11.Http11BaseProtocol$Http11ConnectionHandler.processConnection(Http11BaseProtocol.java:664)
	at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(PoolTcpEndpoint.java:527)
	at org.apache.tomcat.util.net.LeaderFollowerWorkerThread.runIt(LeaderFollowerWorkerThread.java:80)
	at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:684)
	at java.lang.Thread.run(Thread.java:595)

I understood that it is unable to access the data in indexed values, but it displayed properly and while submission its throwing the exception.

Can you tell me whats wrong I am doing and a correct way to deal this.

Thanks in advance.

Ravi Bhargava

Have you tried the delete

The Figners adding is cool.
How then would you go about doing a delete exactly?

Matt Fleming's picture

Re: Have you tried the delete

I'm actually about to write an article about this because the seemingly benign question involves a bigger scope than expected. Here's how I usually manage this condition..

  1. Use Spring's OpenSessionInView filter (or something analogous)
  2. Use the detached object pattern with Hibernate
  3. Have the relationship set to "all-delete-orphan" (e.g. the relationship between Hand and Finger)
  4. Call Hibernate's merge() method on the parent object (e.g. Hand).

-Matt

Ultimately what would be

Ultimately what would be nice to do is have a javascript button that would delete the input field. I did this removing it from the node.
This works great if you do not have a validation error.
If you have a validation error it will bring you back to the form and the fingers are still there (in my case phone numbers).

The issue comes in when a user would fix the issue like fill in the empty field and then if decides to remove a phone number that phone number is not removed from the binded collection list.

I was thinking of writing a method that would pull deleted indexes from a hidden input field on the page and remove them from the list in overridden initBinder?

With your solution above for the deleting how are you showing this on the UI side?

Thanks.

Matt Fleming's picture

I'm a little confused about

I'm a little confused about the nature of the problem. Let me get the order of operations straight here:

  1. Hand page shows with five fingers
  2. User deletes a finger and modifies some other stuff on the form (which will be invalid)
  3. Validation happens, and doesn't succeed
  4. Hand page shows with five fingers and some error message on the wrong field.
  5. User fixes error, deletes finger again and submits form
  6. Backend still has five fingers

Is that the flow?

-Matt

confusion

Yes that is the flow of the problem.

With the step of the user deleting a finger. Are you using javascript and just removing it from the page or are you saying delete in that you are sending it to a controller to delete the finger and then refreshing the entire page?

When resubmitting after a validation error the list in the command object is bound by what was in there before the error in our case 5 fingers. If you remove the fingers and say only leave four when you resubmit after you have a validation error and check your list in the command object it did not rebind what was visible but uses what was bound to the list before your validation error. Therefore instead of seeing only 4 I have what was there originially in the 5.

The interesting thing is if I change say a value of one of input to be Sample to say SampleTest on the error page that has the validation when you resubmit it picks up the change you made so I am assuming it is rebinding those values.

I can post what I have in my jsp and what my command object has in it if that will help

Thanks.
Dan

Matt Fleming's picture

Re: confusion

Quote:
Are you using javascript and just removing it from the page
Yes. Just removing it from the page.

Quote:
If you remove the fingers and say only leave four when you resubmit after you have a validation error and check your list in the command object it did not rebind what was visible but uses what was bound to the list before your validation error. Therefore instead of seeing only 4 I have what was there originially in the 5.
I think this is a Spring MVC bug (see next response). If you wanted to make only four appear, you would have to (I think you suggested this earlier) set an deletedFinger.id hidden field and then trigger the same remove() javascript if this id (or ids) were present.

Quote:
The interesting thing is if I change say a value of one of input to be Sample to say SampleTest on the error page that has the validation when you resubmit it picks up the change you made so I am assuming it is rebinding those values.
It seems like Spring should be able to handle this properly, but I'm not surprised. In the validation logic, Spring isn't considering our case where the model originally had five elements and four get posted. This probably should be an issue logged to the Spring MVC bug tracker. The error flow and normal flow should be consistent.

-Matt

confusion

For my backing list in the command object I am using an AutoPopulatingList
private AutoPopulatingList phones = new AutoPopulatingList(Phone.class);

I wonder if I should try using what you suggested with the LazyList.

Have you been successful in doing the delete and coming back from an error before like I described?

Thanks.

Matt Fleming's picture

Haven't tried it

I don't know what the AutoPopulatingList class does (so I don't know if it would work) but it is a quick change/test to do. Unfortunately, I haven't had to write an application that exposes this issue. Maybe I'll update the Hand app to test this case. I'll probably be updating it for the new articles (for the how to bind a multi-select form element article).

-Matt

Got It!

I think I have a good working example. I still can not get Spring to rebind after coming back from an error from the Validator to rebind if I remove the inputs dynamcally. Here are the code samples.
I got this to work for the edit controller as well any further ideas or tweaks to this please let me know.

Thanks.
Dan

Contact Class
public class Contact {
private int id;
private String firstname;
private String lastname;

private Set phones = new LinkedHashSet();

public Set getPhones() {
return phones;
}
public void setPhones(Set phones) {
this.phones = phones;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
}

Phone Class
public class Phone {

private int id;
private String phonenumber;
private Contact contact;

public Contact getContact() {
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getPhonenumber() {
return phonenumber;
}
public void setPhonenumber(String phonenumber) {
this.phonenumber = phonenumber;
}

}

Contact Backing Bean
public class ContactBackingBean {
private int id;
private String firstname;
private String lastname;

private String phcounter;

private AutoPopulatingList phones = new AutoPopulatingList(Phone.class);

public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public AutoPopulatingList getPhones() {
return phones;
}
public void setPhones(AutoPopulatingList phones) {
this.phones = phones;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getPhcounter() {
return phcounter;
}
public void setPhcounter(String phcounter) {
this.phcounter = phcounter;
}
}

This has the autopopulating list in it.

EnterContactController

public class EnterContactController extends CancellableFormController {

protected Object formBackingObject(HttpServletRequest request)
throws Exception {
ContactBackingBean cbb = new ContactBackingBean();
cbb.setPhcounter("0");
return cbb;
}

protected void onBindAndValidate(HttpServletRequest request, Object object, BindException errors) throws Exception {
ContactBackingBean cbb= (ContactBackingBean)object;
String removeids = ServletRequestUtils.getStringParameter(request, "removeids", "");
if(removeids != null && removeids.length() > 0) {
removeids.trim();
String [] array = removeids.split(":");
String listid = "";
ArrayList newlist = new ArrayList();
if(array != null && array.length > 0) {
for(int i = 0; i < array.length; i++) {
listid = (String) array[i];
if(listid.length() > 0) {
Phone p = (Phone)cbb.getPhones().get(i);
newlist.add(p);
}
}
}
cbb.getPhones().removeAll(newlist);

}

}

protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object backingbean, BindException errors)
throws Exception {

ContactBackingBean cbb = (ContactBackingBean)backingbean;
Contact c = new Contact();

c.setFirstname(cbb.getFirstname());
c.setLastname(cbb.getLastname());

//save contact
sbmanager.createContact(c);

//deal with set
if(cbb.getPhones() != null && !cbb.getPhones().isEmpty()) {
c.getPhones().clear();
Iterator it = cbb.getPhones().iterator();
while(it.hasNext()) {
Phone p = (Phone)it.next();
p.setContact(c);
c.getPhones().add(p);
}

sbmanager.updateContact(c);

}

String destination = (WebUtils.hasSubmitParameter(request, "buttonSaveAndContinue") ? "addcontact.htm" : getSuccessView());

return new ModelAndView(new RedirectView(destination));
}

protected ModelAndView onCancel(HttpServletRequest request,
HttpServletResponse response, Object backingbean) throws Exception {
return new ModelAndView(new RedirectView(getCancelView()));
}

private SBManager sbmanager;

public SBManager getSbmanager() {
return sbmanager;
}

public void setSbmanager(SBManager sbmanager) {
this.sbmanager = sbmanager;
}
}

I set what I am calling the phcounter in the backingbean. This is the counter of how many the index of the item I am adding to the list. This will be a hidden field on the jsp.

To handle the issue with the mvc not removing the input fields if coming back from the validator in handling an error I override the onBindAndValidate method.

Delete in Spring Dynamic list Binding

Hi Matt,

Your online application demonstrating list binding is very helpful. We are encountering issue in deletes. In such a case the request parameter for the list is not same as the binded list on submit of form. We are using Spring 3.0 with annotation based controllers . We are not using hibernate. Please suggest. Also if your demo application would have delete functionality , it would be of great help. Thank you.

-Aarti

Matt Fleming's picture

Deletes

It sounds like you have one collection that is the list of elements as they existed in the db at the time of the first request. The users then "remove" some elements from that collection and that in turn creates a different collection of the "deleted" elements. This is fine of course but it means that you'll have to do all of the collection management yourself.

In the spring controller layer, you'll need to make sure that you pass the deleted list to the controller via an @RequestParam, then simply delete the elements that come back in the list from the db. The thing to think about with all modifications is concurrency. You need to make sure that the elements in the db haven't changed from the first request (where you show the list) to the second request (where you're doing the deleting). Hibernate does this via versioning but it is something to consider.

-Matt

re:How to populate a list containing POJOs

Even i m facing the same pbm.
while binding it is unable to access the data in indexed values throwing the exception:
org.springframework.beans.NullValueInNestedPathException: Invalid property 'AnsVOList[0]' of bean class [com.common.vo.ApplicationVO]: Cannot access indexed value of property referenced in indexed property path 'AnsVOList[0]': returned null
can anyone fix it y it is happening.

Matt Fleming's picture

The exception is pretty clear no?

It says that the object that you are trying to bind to is null. So you have a list but the list is empty. Have you read Dynamic list binding in Spring MVC? That article explains how to have lazy constructed beans. -Matt

re:How to populate a list containing POJOs

No the list has values its not empty, but while binding i get the exception.
Even i tried with lazylist.
I didnt get get the exception rather,
its creating new instance of the pojo class
(LazyList.decorate(new ArrayList(),FactoryUtils.instantiateFactory(Answer.class));
) so the values are getting overridden.