views:

242

answers:

1

Hi,

I'm working on an MVC web application and have a requirement to have a variable length list of items. User should be able to add / edit and delete rows with a save doing a batch update on all rows.

I started off working from two blog posts which have pretty much covered most of the work so all credit to Steve Sanderson.

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

I have my view as follows

<% using (Html.BeginForm()) { %>

<div id="placeholder">
    <% for (int i = 0; i < Model.Count(); i++)
       {
           ViewData["index"] = i;
           Html.RenderPartial("ProfileItemLine", Model.ElementAt(i), new ViewDataDictionary(ViewData) {
            {"prefix", "items"}
        });
       } 
    %>
</div>

<div id="addProfileItem">
    <a href="" id="addItem">Add another item</a>
</div>

<input type="submit" value="Save changes" />

<% } %>

And use jQuery to handle additions and deletes

    $(document).ready(function() {

    var nextIndex = <%=ViewData.Model.Count()%>;
    setRemoveLinks();

    $('#addItem').click(function() {
        var action = '/Profile/BlankItemLine?index=' + nextIndex;
        $.get(action, 'html', function(lineItemHtml) {
            $('#placeholder').append(lineItemHtml);
            setRemoveLinks();
        });
        nextIndex++;
        return false;
    });
});

function setRemoveLinks() {
    $('a.removeItem').click(function() {
        $(this).parent('div').remove();
        return false;
    });
}

The ProfileItemLine ViewUserControl is as follows minus formatting and plain text

<div>
<% var fieldPrefix = string.Format("{0}[{1}].", ViewData["prefix"], ViewData.Model.Sequence); %>
<%= Html.Hidden(fieldPrefix + "ProfileItemId", ViewData.Model.ProfileItemId)%>
<%= Html.TextBox(fieldPrefix + "Title", ViewData.Model.Title, new { size = "30"})%>
<%= Html.CheckBox(fieldPrefix + "IsHighlighted", ViewData.Model.IsHighlighted)%>   
<a href="" class="removeItem">Delete</a></div>

So far so good, the list binds correctly to the model and as long as I only add items it works too.

The problem is related to deletes. The delete works and removes the correct row but I lose all bindings on form submit. In Steve's post he mentions that the RC has changed the way the model binding works and it requires all indexes to be in an unbroken ascending sequence.

Steve listed some code to get working which involved reordering the list on form submit but it throws an exception at the "var controls =" line with method not supported.

$(function() {
$("form").submit(function() {
    // Normalize the sequence before submitting the form
    ensureControlGroupsInNumericalOrder("#placeholder div", "items");
});});

function ensureControlGroupsInNumericalOrder(groupSelector, controlNamePrefix) {
var groups = $(groupSelector);
var nameToEndOfIndexRegex = new RegExp(controlNamePrefix + "\\[\\d+");
for (var seq = 0; seq < groups.length; seq++) {
    var controls = $("*[name~=" + controlNamePrefix + "\[]", groups[seq]);
    for (var c = 0; c < controls.length; c++)
        controls[c].name = controls[c].name.replace(nameToEndOfIndexRegex, controlNamePrefix + "[" + seq);
}}

Example of html output after two deletes of indexes 1 and 2

    <div id="placeholder">
<div>
    <input id="items[0]_ProfileItemId" name="items[0].ProfileItemId" type="hidden" value="0fd3a3f5-907e-4eb1-8f69-26e3840e5e12" />
    <input id="items[0]_Title" name="items[0].Title" size="30" type="text" value="Number 1" />
    <input id="items[0]_IsHighlighted" name="items[0].IsHighlighted" type="checkbox" value="true" />
    <input name="items[0].IsHighlighted" type="hidden" value="false" />   
    <a href="" class="removeItem">Delete</a>  
</div>
<div>
    <input id="items[3]_ProfileItemId" name="items[3].ProfileItemId" type="hidden" value="0fa1a3f5-907e-4eb1-8f69-26e3840e5e12" />
    <input id="items[3]_Title" name="items[3].Title" size="30" type="text" value="Number 3" />
    <input id="items[3]_IsHighlighted" name="items[3].IsHighlighted" type="checkbox" value="true" />
    <input name="items[3].IsHighlighted" type="hidden" value="false" />   
    <a href="" class="removeItem">Delete</a>  
</div>
</div>

I model bind to an IEnumerable on form submit, in this example I would only get the first item back and not the second due to it being items[3] rather than items[1].

public ActionResult EditItems(Guid id, IEnumerable<ProfileItem> items)
{
     if (ModelState.IsValid)
     {
         // Save items
     }
     if (ModelState.IsValid)
         return RedirectToAction("Index");
     else
         return View(items);
 }

I'm still cutting my teeth on jQuery and regular expressions so I wonder if anybody can point me to a solution.

Thanks in advance

+1  A: 

Why would you need al the individual element info ?

Wouldn't it be wiser to use

<div id="div1">

For deleting ?

<a rel="1" id="item + item id (not per se 1 )" class="delete">delete</a>

So you can use

$('.delete').bind('click',function(){ 
  $('#div' + $(this).attr('rel') ).remove();
  // plus delete call to db ?
});

That's what I always do :)

You can have the numbers of the div and rel made up in your html creation loop, so the order is always correct.

FrankBr
Hi,Would this work in my scenario? I think the individual element info is related to model binding to a list so its required.See http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspxMy requirement is that its a batch operation so individual calls to a db cannot happen. So although I've yet to try your solution I'm not sure this is the answer. Have you a working example?
Mannfred