Rico Suter's blog.
 


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.



Discussion