Spring MVC: Having multiple @ModelAttribute in form handling action

19,169

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.

Share:
19,169
Rafał Wrzeszcz
Author by

Rafał Wrzeszcz

I'm a computers enthusiast since when I got my first one in age of 3. From the very beginning, even though it was more about playing games then, it was fascinating adventure for me and very quickly I began to be curious how does it work "inside". I wrote my first program when I was 10, in Pascal. Not much later I started to participate in open source projects, which allowed me to gain a lot of knowledge and train my skills. Even before high school I was already sure what I want to do in my life and even then, in age of 13, I was already picking up some projects and earning real experience on a global scale. Soon later I started to work on professional and commercial projects. All of that gave me strong base for further experience. Even without graduating my studies (I left college after deciding to concentrate on my career -- I've found out that there is nothing more I could have learn there), I have a well-based knowledge, long-term experience, had opportunities to work in various environments, technologies and kinds of projects. I'm skilled developer, with strong analytics skills. Always happy to taste something new, always open for new adventures! Worked on desktop applications, server-side services, but mostly on web appplications. While of course I work for various customers, I'm still involved in a lot of open-source projects (even developing some of them on my own) and still have time to play with new technologies, which makes me always one step ahead of the others! But beyond all of that I'm a happy father, so if I'm not responding, it may be, that I dropped everything and went playng with my beloved sons - please be patient, give us some time ;).

Updated on June 26, 2022

Comments

  • Rafał Wrzeszcz
    Rafał Wrzeszcz almost 2 years

    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).

  • Rafał Wrzeszcz
    Rafał Wrzeszcz over 10 years
    But I'm not using categories as attribute name, I use category, which does't overlap with any form field. Just that form fields are all bound to category object (both objects have name property for example and Spring binds form field value to both category.name and email.name). And for the populating "manually when displaying the form" approach - I need category in submission handling action for redirecting (note the last line of save() action).
  • Krešimir Nesek
    Krešimir Nesek over 10 years
    You're right, renaming model won't work as Spring binds on properties. I removed that from answer and edited answer with example how to get original category in save() action.
  • Rafał Wrzeszcz
    Rafał Wrzeszcz over 10 years
    Was hope to solve it different way, but your solution is in fact even more compact - in fact I don't need entire object. Seems little strange that such use cases are not supported (or at least I can't find out how) by Spring, but thanks a lot for helping with that.
  • Krešimir Nesek
    Krešimir Nesek over 10 years
    If you only need the id for redirect purposes and not for some kind of processing in save action, you can just return "redirect:/emails/{categoryId}/" and Spring will replace {categoryId} with the actual id from the request. In that case you can remove category from method argument list altogether.