Thursday, July 5, 2012

Learning MVC: Editing the Variable Length List

Continuing the recipe database example, the next step is to not just keep a list of ingredients that are required for a recipe, but also to allow specifying the quantity of each ingredient. Up to this point, I did not need any "mapping table" due to the MVC magic - the Recipe object kept a list of ingredients, and the Ingredient object kept a list of recipes. That was all that MVC needed to resolve the many-to-many relationship between recipes and ingredients and I could get away with just two models.

Now that I want to know how much of the ingredient is required, I don't have a place to save this information. I don't think I can get away without the RecipeIngredient model any longer. I'm adding that and also giving the user the ability to add and remove ingredients. This requires a technique to edit and save a list of variable length, which is referenced at the end of this post and which I applied, with small modifications.

To display the list, the following technique is used: a div element which contains one entry from the list is placed in a partial view. Each time the user adds an entry to the list, another such div is appended to the list using an Ajax call to a jQuery append() function. Each time the user removes an entry, a div is removed, which is even easier. To begin with, I added the following to the recipe's Edit view

<fieldset>
 <div id="editorRows">
 <ol>
  @foreach (var item in Model.RecipeIngredients)
  {
   @Html.Partial("_RIEditor", item)
  }
 </ol>
 </div>
 @Html.ActionLink("Add another ...", "Add", null, new {id = "addItem"})
</fieldset>

The same partial view _RIEditor is repeated once per each ingredient. The "Add another ..." link will later add an ingredient when the user clicks it. And here's a sample _RIEditor partial view ( the Layout=null part was added because otherwise my partial view was rendered with the header and the footer).

@using Recipes.HtmlHelpers
@model Recipes.Models.RecipeIngredient
           
@{ Layout = null; }

<div class="editorRow">
    @using (Html.BeginCollectionItem("RecipeIngredients"))
    {
        <li class="styled">
        <div class="display-label">Ingredient:</div>@Html.TextBoxFor(model => model.Ingredient.IngredientName)
        <div class="display-label-nofloat">Amount:</div>@Html.TextBoxFor(model => model.Quantity, new { size = 4 })
        <a href="#" class="deleteRow">delete</a>
        </li>
    }
</div>

The key part here is the Html.BeginCollectionItem, which renders a sequence of items that will later be bound to a single collection. In short, it keeps track of the items as they are added or deleted, and when the form is finally submitted, the neat collection of items is returned, ready to be saved to database.

Now to allow for adding or deleting elements, I need to add two functions. Here's the example:

<script type="text/javascript">
<!--
    $("#addItem").click(function () {
        $.ajax({
            url: this.href,
            cache: false,
            success: function (html) { $("#editorRows").append(html); }
        });
        return false;
    });

    $("a.deleteRow").live("click", function () {
        $(this).parents("div.editorRow:first").remove();
        return false;
    });
-->
</script>

I also need to reference a couple of JavaScript files to make it work

<script src="@Url.Content("~/Scripts/MicrosoftAjax.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/MicrosoftMvcValidation.debug.js")" type="text/javascript"></script>

Almost done, now I only need to take care of that "Add another ..." link that I added to the Edit view. To make it work, I only need a simple action added to the controller. which will return the partial view.

public ViewResult Add()
{
 return View("_RIEditor", new RecipeIngredient());
}

So, what have I achieved? Here's the first approximation of how my ingredient list may look like when I load the recipe from the database

Original List of Ingredients

And how it looks when I click "Add another ...": a line for a new ingredient is added, looking the same as other lines and I can enter some data

Modified List of Ingredients

And then I can verify that some data is returned back on Submit, so my changes are not being lost

Data Posted by the View

The concept is working at this point - I get back my three "RecipeIngredients" and the data I entered. It's only the proof of concept at this point, I need to make a number of modifications to make it functional.

References:

BeginCollectionItem source code
Editing a variable length list, ASP.NET MVC 2-styleby . Also posted on my website

No comments: