Site Meter
 
 

Monthly Archives: December 2008

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.

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.

Try it yourself (launch live demo) 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<Gift>. Here’s our view template:

<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<Gift>, i.e.,

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 <script> block somewhere in EditList.aspx) Of course, I could have done this using jQuery, and then I wouldn’t have had to deal with window.event, evt.srcElement and the other cross-browser nastiness. However, I’ve chosen not to use jQuery in this example just to avoid forcing readers to understand a further technology.

And that deals with deletion! You now have a fully-functional list editor that can add and remove items, and can preserve state if you need to deal with validation failures.

image

Remarks

In most applications, you’ll also need to do something with the incoming data, such as saving it to your database. I’m not going to cover that here because there are so many different data access technologies and you can use whatever you normally use.

For example, if you’re using LINQ to SQL, you’ll probably need to edit your POST handler (the EditList() action that responds to POST requests) so that it loads the existing collection from disk, deletes any records not present in the new data, updates any records still present in the new data (e.g., by using UpdateModel() to automatically transfer changes to the LINQ to SQL entity), and adds any new records (they’re the ones with the temporary negative IDs).

There’s also the matter of enforcing arbitrary validation rules. There’s nothing clever about this; you can do it however you normally do validation. In the downloadable demo project, you’ll see how I usually do it. Specifically, I keep the rules in my model tier, then let the model tier enforce compliance by throwing an exception if the rules are violated. I set this up so that the violation messages are automatically displayed at the right place in the UI. For example, in the live demo, try entering a negative price. Download the demo code to see how it works.

Is there a better way?

If you’ve implemented a variable-length list editor with ASP.NET MVC, how was the experience for you? If you think you’ve got a better way of doing it, I’d really like to hear about it – please post a brief comment explaining what you do! (But please don’t post massive code samples because I’d have to edit it down – try to explain your technique succinctly.)

In case anyone’s interested, it’s really easy to give the user the ability to drag-and-drop the items into a different order, and then to communicate that order to the server. I might well show that in a follow-up post.

kick it on DotNetKicks.com

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.

So anyway… If for any reason you’re interested in who I previously worked for, what I studied at university, who I’m married to, or what you’d have to do to hire me, you can now find out.