Question

The context

I have a simple association between two entities - Category and Email (NtoM). I'm trying to create web interface for browsing and managing them. To browse the category and to add e-mails into that category I use controller wrapped with @RequestMapping with category ID (UUID), so all controller actions are always taking place in context of category specified with path.

I use @ModelAttribute to pre-load context category for entire controller scope.

The problem

This approach worked well for listing and for displaying the forms. However it fails on form submission - after debugging a little, I found out that form data overrides my category @ModelAttribute parameter.

In my code, in method save() the category is not really the model attribute loaded with addCategory() method, but is populated with form data (email model is also populated, and that is correct).

I'm looking for the solution that will allow me to bind form data only to specific @ModelAttribute.

I've read in Spring MVC documentation that order of arguments matters, but I ordered them accordingly to examples and still it doesn't work like expected.

The code

Here is my controller:

@Controller
@RequestMapping("/emails/{categoryId}")
public class EmailsController
{
    @ModelAttribute("category")
    public Category addCategory(@PathVariable UUID categoryId)
    {
        return this.categoryService.getCategory(categoryId);
    }

    @InitBinder
    public void initBinder(WebDataBinder binder)
    {
        binder.registerCustomEditor(Set.class, "categories", new CategoriesSetEditor(this.categoryService));
    }

    @RequestMapping(value = "/create", method = RequestMethod.GET)
    public String createForm(@ModelAttribute Category category, Model model)
    {
        // here everything works, as there is just a single @ModelAttribute

        return "emails/form";
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public String save(
        @ModelAttribute @Valid Email email,
        BindingResult result,
        Model model,
        @ModelAttribute("category") Category category
    ) {
        // saving entity, etc

        // HERE! problem is, that response is bound BOTH to `email' and `category' model attributes
        // and overrides category loaded in `addCategory()' method
        return String.format("redirect:/emails/%s/", category.getId().toString());
    }
}

Just in case here is also the form code:

<form:form action="${pageContext.request.contextPath}/emails/${category.id}/save" method="post" modelAttribute="email">
    <form:hidden path="id"/>
    <fieldset>
        <label for="emailName"><spring:message code="email.form.label.Name" text="E-mail address"/>:</label>
        <form:input path="name" id="emailName" required="required"/>
        <form:errors path="name" cssClass="error"/>

        <label for="emailRealName"><spring:message code="email.form.label.RealName" text="Recipient display name"/>:</label>
        <form:input path="realName" id="emailRealName"/>
        <form:errors path="realName" cssClass="error"/>

        <label for="emailIsActive"><spring:message code="email.form.label.IsActive" text="Activation status"/>:</label>
        <form:checkbox path="active" id="emailIsActive"/>
        <form:errors path="active" cssClass="error"/>

        <form:checkboxes path="categories" element="div" items="${categories}" itemValue="id" itemLabel="name"/>
        <form:errors path="categories" cssClass="error"/>

        <button type="submit"><spring:message code="_common.form.Submit" text="Save"/></button>
    </fieldset>
</form:form>

Note: I don't want multiple @ModelAttributes to come from POST, just want to distinguish somehow form model from previously generated attribute(s).

Was it helpful?

Solution

I'm not sure I understand the problem entirely, but to me, it seems you want category object present in the model when you display the form, but don't want it to be changed with form post?

When you specify @ModelAttribute("categories") in argument list you basically tell spring MVC to bind form data to the annotated object using the parameter name "categories".

If you don't want the object to be bound just leave it out from the parameters list. If you need the original object in the handler method fetch it manually by calling addCategory and providing id mapped with @PathVariable:

@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(
    @ModelAttribute @Valid Email email,
    BindingResult result,
    Model model,
    @PathVaribale("categoryId") UUID categoryId
) {
    // saving entity, etc

    return String.format("redirect:/emails/%s/", categoryId.toString());
    //if category object is needed and not just id then fetch it with Category c = addCategory(categoryId).
}

(PS. If you register a converter that converts Long to Category using categoryService you can also put @PathVariable("categoryId") Category category to map Category object to path variable instead of UUID, if you'd like that take look at 7.5.5 Configuring a ConversionService)

(EDIT: removed suggestion to name model differently as that will not help as noted in comments, and added example)

Personally, if I needed this kind of behavior (an object that needs to be present in the form when displaying the form, but not bound to it when form is posted) I would not use ModelAttribute annotated method to populate the model. Instead, I'd populate the model manually when displaying the form. That is a bit more code (well, one line actually) but is less magical and easier to understand.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top