Twitter About Home

Editing a variable-length list of items in ASP.NET MVC

Update: This post was originally written for ASP.NET MVC 1.0 Beta. I’ve written a newer version of this post that applies to ASP.NET MVC 2.0 (Release Candidate). Note that this technique doesn’t actually work quite so easily with the final version of ASP.NET MVC 1.0, because its model binding convention changed in a way that means you have to massage the data into a numerical sequence for it to work. See comment #25 for a hint about one way to do this, or upgrade to ASP.NET MVC 2.0.

Published Dec 22, 2008

Most web applications involve editing lists or collections of things at some point. For example,

  • Each blog post has a collection of tags
  • Each flight booking has a list of passenger names and corresponding food preferences
  • Each recipe has a collection of ingredients with their quantities and prices

It’s always awkward to create the right UI for variable-length lists, because you don’t know how many input controls to render. The user needs a way to add and remove items, and you have to retain all this state if they submit a form that causes validation errors.

How would you do it with ASP.NET MVC? There are loads of possible ways of doing it! In this post, I’ll show you one fairly elegant way I settled on in a recent project.

<a style="border-right: black 2px solid; padding-right: 0.5em; border-top: silver 2px solid; padding-left: 0.5em; padding-bottom: 0.5em; border-left: silver 2px solid; color: white; padding-top: 0.5em; border-bottom: black 2px solid; background-color: green" href="http://razor.codeville.net/listeditor/app.ashx/Home/EditList" target="_blank" onclick="javascript:_gaq.push(['_trackEvent','outbound-article','http://razor.codeville.net']);window.open("http://razor.codeville.net/listeditor/app.ashx/Home/EditList","mywindow","menubar=0,resizable=1,width=700,height=350"); return false;'>Try it yourself (launch live demo) </a> Download the source code

Getting started

I’ll assume you’re already building an application with ASP.NET MVC (Beta). Since it’s that time of year, let’s build a gift list editor. We can model a gift as follows:

public class Gift
{
    public int GiftID { get; set; }   // Unique key
    public string Name { get; set; }
    public double? Price { get; set; }
}

Rendering the initial UI

To render the initial UI, add an action method similar to the following:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult EditList()
{
    var initialData = new[] {
        new Gift { GiftID = 14, Name = "iPod Nano 16GB Red", Price = 184.95 },
        new Gift { GiftID = 221, Name = "Noddy: The Deleted Scenes", Price = 19.99 }
    };
 
    return View(initialData);
}

This sets up an initial gift list and renders a view based on it. In a real app, you might fetch this initial data from your model tier or database.

Next, you’ll need a view template for the EditList() action. Right-click inside the EditList() method and choose Add View. We’re going to render a series of Gift objects, so make the view Strongly Typed, using a model type of IEnumerable. Here’s our view template: </p>

<h1>Gift request form</h1>
 
<% using(Html.BeginForm()) { %>
 
    <div id="items">
        <% foreach (var gift in ViewData.Model) {
            Html.RenderPartial("GiftEditor", gift, new ViewDataDictionary(ViewData) {
                {"prefix", "gifts"}
            });
        } %>
    </div>
 
    <input type="submit" value="Save changes" />
<% } %>

As you can see, this iterates over the incoming collection of Gift objects, and for each one, it renders a partial view called GiftEditor. The GiftEditor partial will hold the UI needed to edit each entry in the collection.

To create the Gift Editor partial, right-click on the /Views/Shared folder, and choose Add –> New Item. Make an MVC View User Control called GiftEditor.ascx, then go to its code-behind class and set it to inherit from ViewUserControl, i.e., </p>

public partial class GiftEditor : ViewUserControl<gift>
{
}

Now you can go back to GiftEditor.ascx and add the markup representing the UI controls for a single Gift object:

<div>
    <input type="hidden" name="<%= ViewData["prefix"] + ".index" %>" value="<%= ViewData.Model.GiftID %>" />
    <% var fieldPrefix = string.Format("{0}[{1}].", ViewData["prefix"], ViewData.Model.GiftID); %>
 
    <%= Html.Hidden(fieldPrefix + "GiftID", ViewData.Model.GiftID) %>
    Gift name: <%= Html.TextBox(fieldPrefix + "Name", ViewData.Model.Name, new { size = "30"})%>
    Price ($): <%= Html.TextBox(fieldPrefix + "Price", ViewData.Model.Price, new { size = "5" })%>
</div>

If you don’t have a clue what’s going on here, don’t worry! This isn’t obvious. I’m following ASP.NET MVC’s control naming convention, putting clusters of input controls into groups using a kind of array notation. For more details about how this works, see Phil Haack’s post on the subject. The benefit of following this convention will become clear in a moment when we try to post the data back to the server.

With all this, the initial UI will now appear!

image

Receiving posted data

OK, so the user can edit the existing records and can click “Save Changes”. But when they do, they’ll get a 404 Not Found error because you don’t have any action method to receive the post. Add an action method as follows:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditList(IList<gift> gifts)
{
    return ModelState.IsValid ? View("Completed", gifts)
                              : View(gifts);
}

This is the magic of model binding: because we followed its naming convention earlier, we can now receive a fully-populated collection of .NET objects as an action method parameter. All the incoming data will be parsed into the collection automatically.

All the new action has to do is decide whether or not the incoming data is good.

  • If the data is good, the action will render the “Completed” view. That’s nothing special; it’s just any old view.
  • If the data is bad – for example because you typed some nondigit characters in a “Price” box – the action will re-render the same EditList view you created earlier.

Adding new items

So far, so good. But the user can only edit existing items… what about adding new ones? Well, that’s easy: we can use a bit of Ajax to inject extra edit rows on the fly.

Add the following code into EditList.aspx, just above the “Submit Changes” button:

<%= Ajax.ActionLink("Add another item", "BlankEditor", new AjaxOptions {
    UpdateTargetId = "items", InsertionMode = InsertionMode.InsertAfter
}) %>

Here, we’re using ASP.NET MVC’s built in Ajax helper to fetch the output of an action method and inject it directly into the existing DOM (note that your master page needs to reference ~/Scripts/MicrosoftAjax.js and ~/Scripts/MicrosoftMvcAjax.js for this to work). Specifically, we’re putting the output of an action called BlankEditor at the bottom of the existing DOM element called items.

Obviously, you also need to add an action method called BlankEditor. It simply returns a blank edit row:

public ActionResult BlankEditor()
{
    // Use convention that negative IDs represent unsaved records
    int tempUniqueID = -1 * new Random().Next();
 
    ViewData["prefix"] = "gifts";
    return View("GiftEditor", new Gift { GiftID = tempUniqueID });
}

That does it! Now there’s an Add another item link which causes a new edit row to appear. You can fill in some data, and model binding will automatically parse it as a Gift object. If there are validation failures, the number of input controls (and their contents) will all be preserved because of how we’re following the model binding conventions.

Deleting items

Deleting an item is simply a matter of removing the corresponding DOM elements. When the form is submitted, that data will no longer be sent, so there will be no corresponding item in the model-bound collection. Add the following link to the end of GiftEditor.ascx:

<a href="#" onclick="deleteContainer(event)">Delete</a>

(For example, put this just after the Price textbox.) To make it do something, add the following JavaScript function to some JavaScript file referenced by your page:

function deleteContainer(evt) {
    evt = evt || window.event;
    var target = evt.target || evt.srcElement;
    target.parentNode.parentNode.removeChild(target.parentNode);
}

(For example, put it inside a

READ NEXT

The ego post

Somebody recently pointed out to me that my “About me” page was ridiculously sparse. It just stated my name and city, and that was it. Was I trying to hide something? No, I just didn’t think it was important to most readers.

Published Dec 4, 2008