Discussion:
[Springframework-user] Re: errors on form submission does not redirect
Christof Laenzlinger
2006-03-25 10:36:58 UTC
Permalink
It seems that the default behavior when a bind/validation error
occurs in a Spring MVC controller's is to *forward* to the
view rather than redirect. This winds up giving the user the form page
but with the input url (the value of the form's "action" attribute) in
the browser's address bar. This is extremely poor usability. Am I
missing something here? Is there a simple and straightforward way of
ALWAYS redirecting after post?
I have had similar thought when I first looked at Spring MVC. I am
also convinced that a very good strategy to avoid all sorts of
back-button and page-reload problems is to ALWAYS redirect after POST
(On success and also in case of bind/validation errors).

The SimpleFormController does a forward to the form view, so I was
thinking of RedirectFormController that behaves similar to the
SimpleFormController in terms the workflow. But instead of forwarding
to the successView in case of validation errors it does a redirect
back to the view defined in the formRedirect property. Before it does
the redirect, the errors are stored in the session. When the form is
requested again, the errors are removed from the session and stored
again in the request. I have copied the source code of the
RedirectFormController below.

This is just a proposal and I am not sure if this is the best solution
for solving this problem, so please let me know what you think about it.


Christof

------------------------------------------------------------------------------

package example;

import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractFormController;

public class RedirectingFormController extends AbstractFormController {

protected static Log log =
LogFactory.getLog(RedirectingFormController.class);

private String formRedirect;
private String formView;
private String successView;

protected ModelAndView showForm(HttpServletRequest request,
HttpServletResponse response, BindException errors)
throws Exception {

return showForm(request, response, errors, null);
}

protected ModelAndView showForm(
HttpServletRequest request, HttpServletResponse response,
BindException errors, Map controlModel)
throws Exception {

log.debug("showForm");
String errorsAttrName = getErrorsSessionAttributeName(request);
BindException sessionErrors =
(BindException) request.getSession().getAttribute(errorsAttrName);
if (sessionErrors != null) {
if (log.isDebugEnabled()) {
log.debug("redirected error request");
}
errors.addAllErrors(sessionErrors);
request.getSession().removeAttribute(errorsAttrName);
}

return showForm(request, errors, getFormView(), controlModel);
}


protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Object sessionForm = request.getSession().getAttribute(
getFormSessionAttributeName(request));
if (sessionForm != null) {
return sessionForm;
}
return super.formBackingObject(request);
}

protected ModelAndView processFormSubmission(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
if (errors.hasErrors()) {
if (log.isDebugEnabled()) {
log.debug("Data binding errors: " + errors.getErrorCount());
}
return redirectForm(request, response, command, errors);
} else {
log.debug("No errors -> processing submit");
return onSubmit(request, response, command, errors);
}
}

// redirect view --------------------------------------------------------
protected ModelAndView redirectForm(
HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors)
throws Exception {

ModelAndView mv = new ModelAndView(getFormRedirect());

if (isSessionForm()) {
String formAttrName = getFormSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting form session attribute ["
+ formAttrName + "] to: " + errors.getTarget());
}
request.getSession().setAttribute(formAttrName,
errors.getTarget());
}

String errorsAttrName = getErrorsSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting errors session attribute [" +
errorsAttrName + "] to: " + errors);
}
request.getSession().setAttribute(errorsAttrName, errors);

if (log.isDebugEnabled()) {
log.debug("send redirect to [" + getFormRedirect() + "]");
}

return mv;
}

protected String getErrorsSessionAttributeName(
HttpServletRequest request) {
return getErrorsSessionAttributeName();
}

protected String getErrorsSessionAttributeName() {
return getClass().getName() + ".ERRORS." + getCommandName();
}

// on submit chain ------------------------------------------------------
protected ModelAndView onSubmit(
HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors)
throws Exception {

return onSubmit(command, errors);
}

protected ModelAndView onSubmit(Object command, BindException errors)
throws Exception {
ModelAndView mv = onSubmit(command);
if (mv != null) {
// simplest onSubmit version implemented in custom subclass
return mv;
}
else {
// default behavior: render success view
if (getSuccessView() == null) {
throw new ServletException("successView isn't set");
}
return new ModelAndView(getSuccessView(), errors.getModel());
}
}

protected ModelAndView onSubmit(Object command) throws Exception {
doSubmitAction(command);
return null;
}

protected void doSubmitAction(Object command) throws Exception {
}

/**
* @return Returns the formView.
*/
public String getFormView() {
return formView;
}

/**
* @param formView The formView to set.
*/
public void setFormView(String formView) {
this.formView = formView;
}

/**
* @return Returns the successView.
*/
public String getSuccessView() {
return successView;
}

/**
* @param successView The successView to set.
*/
public void setSuccessView(String successView) {
this.successView = successView;
}

/**
* @return Returns the formRedirect.
*/
public String getFormRedirect() {
return formRedirect;
}

/**
* @param formRedirect The formRedirect to set.
*/
public void setFormRedirect(String formRedirect) {
this.formRedirect = formRedirect;
}


}
Ben Munat
2006-03-25 18:35:28 UTC
Permalink
Post by Christof Laenzlinger
It seems that the default behavior when a bind/validation error
occurs in a Spring MVC controller's is to *forward* to the
view rather than redirect. This winds up giving the user the form page
but with the input url (the value of the form's "action" attribute) in
the browser's address bar. This is extremely poor usability. Am I
missing something here? Is there a simple and straightforward way of
ALWAYS redirecting after post?
I have had similar thought when I first looked at Spring MVC. I am
also convinced that a very good strategy to avoid all sorts of
back-button and page-reload problems is to ALWAYS redirect after POST
(On success and also in case of bind/validation errors).
The SimpleFormController does a forward to the form view, so I was
thinking of RedirectFormController that behaves similar to the
SimpleFormController in terms the workflow. But instead of forwarding
to the successView in case of validation errors it does a redirect
back to the view defined in the formRedirect property. Before it does
the redirect, the errors are stored in the session. When the form is
requested again, the errors are removed from the session and stored
again in the request. I have copied the source code of the
RedirectFormController below.
This is just a proposal and I am not sure if this is the best solution
for solving this problem, so please let me know what you think about it.
Chistof, you're gonna laugh when you hear my resolution... I'd meant to post it to the
list but forgot (it had nothing to do with my profound embarassment, really....)

When I posted that dilemma to the list I also complained to my co-worker who has done more
SpringMVC than me. He said he didn't have any problem with how displaying errors were
handled. I insisted that he must and I'd show him how (we even bet a coffee on it). He ran
his app, and I caused an error on a form, and... it worked flawlessly!?!

Well, it turns out that he was simply NOT including an action attribute value on his form
tag. This means that the form is submitted back to the URL the produced the form to begin
with... the URL that displays the form is also the URL that handles the form post.

So, when there are validation errors, the controller can go straight to the view and all
is well because the URL in the address bar is the still the view URL instead of an input
URL. In fact, there are no separate input URLs.

I was so focused on my previous practice -- delineation of get/view vs post/input URLs --
that this solution didn't even occur to me.

So basically, in SpringMVC a form page is backed by a single controller that handles all
aspects of dealing with that form... you always go back to that controller until you
successfully completed its requirements. *Then* you are redirected to a success view.

I'm happy with the way it works now... just needed a little world-view adjustment. ;-)

b
Post by Christof Laenzlinger
package example;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractFormController;
public class RedirectingFormController extends AbstractFormController {
protected static Log log =
LogFactory.getLog(RedirectingFormController.class);
private String formRedirect;
private String formView;
private String successView;
protected ModelAndView showForm(HttpServletRequest request,
HttpServletResponse response, BindException errors)
throws Exception {
return showForm(request, response, errors, null);
}
protected ModelAndView showForm(
HttpServletRequest request, HttpServletResponse response,
BindException errors, Map controlModel)
throws Exception {
log.debug("showForm");
String errorsAttrName = getErrorsSessionAttributeName(request);
BindException sessionErrors =
(BindException)
request.getSession().getAttribute(errorsAttrName);
if (sessionErrors != null) {
if (log.isDebugEnabled()) {
log.debug("redirected error request");
}
errors.addAllErrors(sessionErrors);
request.getSession().removeAttribute(errorsAttrName);
}
return showForm(request, errors, getFormView(), controlModel);
}
protected Object formBackingObject(HttpServletRequest request)
throws Exception {
Object sessionForm = request.getSession().getAttribute(
getFormSessionAttributeName(request));
if (sessionForm != null) {
return sessionForm;
}
return super.formBackingObject(request);
}
protected ModelAndView processFormSubmission(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
if (errors.hasErrors()) {
if (log.isDebugEnabled()) {
log.debug("Data binding errors: " +
errors.getErrorCount());
}
return redirectForm(request, response, command, errors);
} else {
log.debug("No errors -> processing submit");
return onSubmit(request, response, command, errors);
}
}
// redirect view
--------------------------------------------------------
protected ModelAndView redirectForm(
HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors)
throws Exception {
ModelAndView mv = new ModelAndView(getFormRedirect());
if (isSessionForm()) {
String formAttrName = getFormSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting form session attribute ["
+ formAttrName + "] to: " + errors.getTarget());
}
request.getSession().setAttribute(formAttrName,
errors.getTarget());
}
String errorsAttrName = getErrorsSessionAttributeName(request);
if (log.isDebugEnabled()) {
log.debug("Setting errors session attribute [" +
errorsAttrName + "] to: " + errors);
}
request.getSession().setAttribute(errorsAttrName, errors);
if (log.isDebugEnabled()) {
log.debug("send redirect to [" + getFormRedirect() + "]");
}
return mv;
}
protected String getErrorsSessionAttributeName(
HttpServletRequest request) {
return getErrorsSessionAttributeName();
}
protected String getErrorsSessionAttributeName() {
return getClass().getName() + ".ERRORS." + getCommandName();
}
// on submit chain
------------------------------------------------------
protected ModelAndView onSubmit(
HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors)
throws Exception {
return onSubmit(command, errors);
}
protected ModelAndView onSubmit(Object command, BindException errors)
throws Exception {
ModelAndView mv = onSubmit(command);
if (mv != null) {
// simplest onSubmit version implemented in custom subclass
return mv;
}
else {
// default behavior: render success view
if (getSuccessView() == null) {
throw new ServletException("successView isn't set");
}
return new ModelAndView(getSuccessView(), errors.getModel());
}
}
protected ModelAndView onSubmit(Object command) throws Exception {
doSubmitAction(command);
return null;
}
protected void doSubmitAction(Object command) throws Exception {
}
/**
*/
public String getFormView() {
return formView;
}
/**
*/
public void setFormView(String formView) {
this.formView = formView;
}
/**
*/
public String getSuccessView() {
return successView;
}
/**
*/
public void setSuccessView(String successView) {
this.successView = successView;
}
/**
*/
public String getFormRedirect() {
return formRedirect;
}
/**
*/
public void setFormRedirect(String formRedirect) {
this.formRedirect = formRedirect;
}
}
-------------------------------------------------------
This SF.Net email is sponsored by xPML, a groundbreaking scripting language
that extends applications into web and mobile media. Attend the live
webcast
and join the prime developer group breaking into this new coding territory!
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=110944&bid=241720&dat=121642
_______________________________________________
Springframework-user mailing list
https://lists.sourceforge.net/lists/listinfo/springframework-user
Christof Laenzlinger
2006-03-26 17:44:09 UTC
Permalink
Hallo Ben,

Thank you for your reply.

I understand that always the same URL is used by the SimpleFormController.
(Both for GETting the form and also for POSTing the form data).

After successful submission of the form, I can redirect to a success form.
That solves most of the problems.

There is a (minor) problem in case there were validation- or bind-errors.
The SimpleFormController will (as a result of the validation errors) forward
to the form view.
When the User the reloads the page (using the reload function of the
browser) most modern browsers will show a pop-up window asking the User if
he wants to send the form data again.

This is (for some users) a strange, misleading behaviour. Sending back a
redirect response after the POST method (to the same form URL) avoids this
pop-up window.

Maybe I missed something (and I would more than happy to learn what I
missed).

Many Thanks

Christof
Post by Ben Munat
Post by Christof Laenzlinger
It seems that the default behavior when a bind/validation error
occurs in a Spring MVC controller's is to *forward* to the
view rather than redirect. This winds up giving the user the form
page
Post by Christof Laenzlinger
but with the input url (the value of the form's "action" attribute)
in
Post by Christof Laenzlinger
the browser's address bar. This is extremely poor usability. Am I
missing something here? Is there a simple and straightforward way
of
Post by Christof Laenzlinger
ALWAYS redirecting after post?
I have had similar thought when I first looked at Spring MVC. I am
also convinced that a very good strategy to avoid all sorts of
back-button and page-reload problems is to ALWAYS redirect after POST
(On success and also in case of bind/validation errors).
The SimpleFormController does a forward to the form view, so I was
thinking of RedirectFormController that behaves similar to the
SimpleFormController in terms the workflow. But instead of forwarding
to the successView in case of validation errors it does a redirect
back to the view defined in the formRedirect property. Before it does
the redirect, the errors are stored in the session. When the form is
requested again, the errors are removed from the session and stored
again in the request. I have copied the source code of the
RedirectFormController below.
This is just a proposal and I am not sure if this is the best solution
for solving this problem, so please let me know what you think about
it.
Chistof, you're gonna laugh when you hear my resolution... I'd meant to
post
it to the
list but forgot (it had nothing to do with my profound embarassment,
really....)
When I posted that dilemma to the list I also complained to my co-worker
who
has done more
SpringMVC than me. He said he didn't have any problem with how
displaying
errors were
handled. I insisted that he must and I'd show him how (we even bet a
coffee on
it). He ran
his app, and I caused an error on a form, and... it worked flawlessly!?!
Well, it turns out that he was simply NOT including an action attribute
value
on his form
tag. This means that the form is submitted back to the URL the produced
the
form to begin
with... the URL that displays the form is also the URL that handles the
form post.
So, when there are validation errors, the controller can go straight to
the
view and all
is well because the URL in the address bar is the still the view URL
instead
of an input
URL. In fact, there are no separate input URLs.
I was so focused on my previous practice -- delineation of get/view vs
post/input URLs --
that this solution didn't even occur to me.
So basically, in SpringMVC a form page is backed by a single controller
that
handles all
aspects of dealing with that form... you always go back to that
controller
until you
successfully completed its requirements. *Then* you are redirected to a
success view.
I'm happy with the way it works now... just needed a little world-view
adjustment. ;-)
b
Continue reading on narkive:
Loading...