ASP.NET MVC: How to implement an edit form for an entity with a sortable child collection

March 30, 2015, (updated on June 10, 2015), 26 comments, Software Development

This article shows how to implement the edit form for an entity with a collection property. As a show case, we will implement the edit form for a person entity with multiple addresses. The following diagram shows the UML diagram of the two entity classes:

The final edit form for a person entity should look like this:

The goal is to implement this form so that the address items are sortable and validated directly on the page. The article describes how to implement the described edit form in a simple way with as little as possible lines of code.

The code in this article uses some custom HTML helper methods which can be found in the MyToolkit library and installed using a NuGet package. The source code can also be directly copied into your project so that no dependency to the library is required.

A complete sample application can be downloaded from this GitHub repository.

Install JQuery UI to support collection sorting

The client side sorting of collection items will be handled by the sortable extension of JQuery UI. To install the JavaScript library, follow the following steps:

  1. Install JQuery.UI.Combined using the NuGet package manager or download it manually from the JQuery UI site.
  2. In the meta tag of the _Layout.cshtml file, add the following script tag with the correct file path:

    <script src="@Url.Content("~/Scripts/jquery-ui-1.11.4.min.js")" type="text/javascript"></script>        
    

Create a view model for a single collection item

Each address item editor in the collection will be rendered using a partial view. For this partial view you need to implement a view model class:

public class AddressEditorViewModel
{
    public int Id { get; set; }

    [Required]
    public string Street { get; set; }

    [Required]
    public string City { get; set; }
}

The view model class uses data annotations to specify the validation rules. If JQuery unobtrusive validation is enabled, the view model will be validated on the client.

Create an editor for a collection item as partial view

As mentioned before, the editor for a single address is rendered using a reusable partial view. Partial views must be located in the Shared directory. I recommend to put this partial view into a new directory with the name Editors in the Shared directory. In our case, the partial view with the name _AddressEditor has the following content:

@using MyToolkit.Html
@model OrangeSteel.Models.Editors.AddressEditorViewModel

<li style="cursor: move">
    <div class="panel panel-default panel-body">
        @using (Html.BeginCollectionItem("Addresses"))
        {
            @Html.HiddenFor(m => m.Id)
            <p>
                @Html.LabelFor(m => m.Street)
                @Html.TextBoxFor(m => m.Street, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.Street, null, new { @class = "text-danger" })
            </p>
            <p>
                @Html.LabelFor(m => m.City)
                @Html.TextBoxFor(m => m.City, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.City, null, new { @class = "text-danger" })
            </p>
            <input type="button" value="Delete" class="btn btn-default" 
                   onclick="$(this).parent().remove();" />
        }
    </div>
</li>

The BeginCollectionItem method is a custom HTML helper method which sets a renders a hidden input with the row’s index so that the ASP.NET MVC model binder correctly generates the collection after submitting the form. The only parameter of the helper method is the collection property name from the master form’s view model which we will implemented next.

Add the collection editor to the master form

In your view model of the person’s edit form, add a new property with a collection of AddressEditorViewModel objects. This collection provides the current addresses to the view and contains the edited items after the form has been submitted:

public class PersonEditViewModel
{
    ...

    public IEnumerable<AddressEditorViewModel> Addresses { get; set; }
}

In the Edit controller action for the GET method, implement the logic to convert the currently persisted addresses to AddressEditorViewModel objects:

public async Task<ActionResult> Edit(int id)
{
    ...
    IEnumerable<Address> addressEntities = await GetAddressesAsync(id); 
    var viewModel = new PersonEditViewModel
    {
        ...

        Addresses = addressEntities.Select(a => new AddressEditorViewModel
        {
            Id = a.Id, 
            Street = a.Street, 
            City = a.City
        }), 
    };

    return View(viewModel);
}

In the master form view (in our case the Edit.cshtml view), add the following code which renders the editor for the collection property Addresses:

<div class="form-group">
    @Html.LabelFor(m => m.Addresses, htmlAttributes: new {@class = "control-label col-md-2"})
    <div class="col-md-10">
        @Html.CollectionEditorFor(m => m.Addresses, "Editors/_AddressEditor", "/Person/GetAddressEditor", 
            "Add address", new { @class = "btn btn-default" })
    </div>
</div>

The CollectionEditorFor method is also a custom HTML helper method which does the following things:

  1. Render the row editors for all current address rows on the server
  2. Render the add button to dynamically add an additional address row editor
  3. Render the JavaScript with the add button handler and the code to update the form validation for additionally added rows. When the add button is clicked, the handler dynamically requests the HTML of a new editor from the server using an HTTP GET request. The received HTML is then appended to the editor row list.

To make the retrieving of new editors work (i.e. the HTTP GET request to the controller action /Pattern/GetAddressEditor), you need to implement a new controller action which just renders the previously created partial view:

public ActionResult GetAddressEditor()
{
    return PartialView("Editors/_AddressEditor", new AddressEditorViewModel());
}

Process submitted form

The final step is to process the edited addresses from the PersonEditViewModel when the form is submitted:

[HttpPost]
public async Task<ActionResult> Edit(int id, PersonEditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        // TODO: Save changes of viewModel.Addresses to database

        return RedirectToAction("Details", new { id = id });
    }
    else
        return View(viewModel);
}

In the code above, you have to persist the AddressEditorViewModel objects from the Addresses into your database when the form has been successfully validated. If the Id is 0 the address is not yet in the database and has to be inserted.

Tweet about this on TwitterShare on FacebookEmail this to someoneShare on TumblrShare on LinkedIn

Tags: , , , ,

26 responses to “ASP.NET MVC: How to implement an edit form for an entity with a sortable child collection”

  1. Tobias says:

    Dear Rico,
    Thank you very much for the great work and making all of this available to the public. I was able to implement such parent-child form like above. However, since the child has quite a few attributes and there can be many children, I’d like to use a table rather than subforms.
    Do you think this is possible? Could you give me a hint how to do it?
    Best regards, Tobias

    • Rico Suter says:

      I think this should be possible, it’s just another form of html rendering…

      • Tobias says:

        Not being a pro, I’m wondering where to start. I tried to use a table already and failed greatly. Do I have to amend my code only? It’s confusing how to show the header only once and still apply same width to all columns. I’m very grateful for a few hints to get started – thank you in advance!

  2. dmester says:

    Hi there, where does GetAddressesAsync come from? Showing up as an error for me. Sorry, I’m still new at development!

    • Rico Suter says:

      This is just a sample method which retrieves addresses from the database. You have to implement this yourself.

  3. Rico Marcelo says:

    Hi Rico,

    Thanks for sharing your solution. I have been using BeginCollectionItem helper for my projects which I think is similar to yours. The only problem with my current approach is I am having problem implementing Remote Validation in the child fields. Does your solution allow remote validation in the child fields?

    Rico M

    • Rico Suter says:

      I don’t know if remote validation is supported. You have to try it… I didn’t use the classes in such a scenario.

  4. Paul says:

    It would be great if you were able to do an updated version of this post but for asp.net core! Thank you much for all your work.

    • Rico Suter says:

      I currently do not have the time to implement this for .NET Core. Because my solution needs access to some “undocumented” internals, the implementation for .NET Core may be not trivial.

  5. Ed says:

    Hi, does this work with the most recent (not core) version of MVC. I have it working other than that the button to add a new record does nothing.

    Thanks,
    Ed

  6. Toni says:

    Hi any help please…I am trying to implementing this in a project but it doesnt seems to work for me,I am implementing only the multiple form part. Nothing happens when i click the button. No error is displayed.

    Thanks in advance

  7. Hi Rico, you might be interested in my library http://formfactory.apphb.com/ which has a pretty neat generic solution for nested entity collections.

  8. idrys says:

    Error: Object doesn’t support property or method „sortable”
    then in file _Layout.cshtml in line:
    @Scripts.Render(“~/bundles/jquery”)
    change on
    @Scripts.Render(“~/bundles/jqueryui”)

  9. Jian says:

    Hi Rico,

    I’m trying to use your way, I can show the collection data on the form, but after ajax submitted, the sub-collection field is always null. looks like model binding is not working when postback in my case.

    Can you give some advice?

    Thanks

    • Jian says:

      I got the reason – I’m using a model which contains 2 level object, and collection in the 2nd level – like pagemodel.personalObj.Addresses, it can not be recognized.

      I have a workaround, just add “personalObj.” in html element id, follow the rule, then everything is working

  10. Steve says:

    Hey Rico,

    I am trying to use your approach with a collection nested within a collection(in the terms of this tutorial, having a collection of forwarding addresses that belong to each address that belongs to a person). In using this extra level of nesting the model binder seems to get confused and dosnt recognize the innermost collection. Is there anything I need to do differently in order for the model binder to interperet the innermost collection correctly?

    Thanks for your time,
    Steve

    • Rico Suter says:

      I haven’t tested the approach with double nested collections… Do you have a sample project to test it?

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax