Dahamsta Dahamsta - 3 months ago 13
ASP.NET (C#) Question

How to delete rows dynamically with jquery from an asp.net view model without deleting the rest of the next objects in the collection

From what I understand, I have an indexation problem. Let's start with the actual code (this is a fragment from the whole page code, but other fragments, which are similar to this, also fail to work):

The cshtml code that creates Generic Object partial views.

<div id="genericObjects">
@if (Model.GenericObjects != null && Model.GenericObjects.Any())
{
for (int i = 0; i < Model.GenericObjects.Count; i++)
{
{ Html.RenderPartial("_GenericObject", Model, new ViewDataDictionary(this.ViewData) { { "GenericIndex", i } }); }
}
}
</div>


The _GenericObject partial view:

model Noodle.Presentation.Models.MashupViewModel

@{
var f = Html.Bootstrap().Misc().GetBuilderFor(new Form().Type(FormType.Horizontal).LabelWidthMd(3));
var index = (int)ViewBag.GenericIndex;
}

<div class="row generic">
<h4>
<span>Object</span>
@Html.Bootstrap().Button().Class("remove-generic pull-right").PrependIcon("glyphicon glyphicon-trash").Text("")
</h4>
@f.FormGroup().TextBoxFor(s => Model.GenericObjects[index].Name).Label().ShowRequiredStar(false)
@f.FormGroup().TextBoxFor(s => Model.GenericObjects[index].ForeignId).Label().ShowRequiredStar(false)
@f.FormGroup().TextBoxFor(s => Model.GenericObjects[index].Value).Label().ShowRequiredStar(false)

</div>


The GetGenericObject method for invoking the partial view:

public PartialViewResult GetGenericObject(int? index)
{
ViewBag.GenericIndex = index;
return PartialView("_GenericObject");
}


Here are the addition (works fine) and deletion jquery methods:

$('#mashup-form').on('click', '.add-generic-object-btn', function () {
var index = $('#genericObjects').children().length;
$('<div>').load(genericObjPath + '?index=' + index, function () {
$('#genericObjects').append($(this).find('.generic')[0].outerHTML);
revalidate();
});
});


AND

$('#mashup-form').on('click', '.remove-generic', function () {
$(this).parent().parent().remove();
});


As for the actual problem, let's say we have three generic objects:

TEST1
TEST2
TEST3

If TEST2 is deleted, the generic objects that come after it are not submitted (although they do appear on the page).

Essentially, what remains on page:

TEST1,
TEST3

What is submitted in the model:

TEST1

The rest of the code works just fine. There is no real point in showing the c# method call on submit, as it works perfectly, but receives a model, that does not have the TEST3 object in it.

Now, this seems to be an indexation problem, which I tried to fix by altering the code like this (the idea was taken from another StackOverflow thread [delete table row dynamically using jQuery in asp.net mvc):

$('#mashup-form').on('click', '.remove-generic', function () {
$(this).parent().parent().remove();
var index = 0;
var itemIndex = 0;
$('#genericObjects').each(function () {
var this_row = $(this);
this_row.find('input[name$=".Name"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].Name');
this_row.find('input[name$=".ForeignId"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].ForeignId');
this_row.find('input[name$=".Value"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].Value');
itemIndex++;
});
});


The result was rather bizarre. Again, if we have three Objects TEST1 TEST2 TEST3, by deleting TEST1 and submitting, I got only TEST2 and by only I mean the entire model was wiped, but this object (the model has more objects, this is just a fragment of the page). Also, if TEST2 is deleted, TEST1 is the only object in the view model (everything else is null). If the delete button on the generic objects is not pressed, everything works just fine.

So, if you have any ideas on how to fix this, please tell them. Also, if there is some other info or code you might need, feel free to ask. Admittedly, I might not be able to give it, but I will see what can be done. Have a good one!

AFTER SOLVING ISSUE EDIT:

I marked the answer below, which involves using EditorFor, as the accepted answer, because I find it is well written and could be useful to somebody, who reads this question and cannot make the partial views work at all. However, in my case, I could make the previous system work. I made a very basic stupid mistake, by only specifying the element outside the partial view. So this:

$('#mashup-form').on('click', '.remove-generic', function () {
$(this).parent().parent().remove();
var index = 0;
var itemIndex = 0;
$('#genericObjects').each(function () {
var this_row = $(this);
this_row.find('input[name$=".Name"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].Name');
this_row.find('input[name$=".ForeignId"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].ForeignId');
this_row.find('input[name$=".Value"]').attr('name', 'Model.GenericObjects[' + itemIndex + '].Value');
itemIndex++;
});
});


Essentially became this:

$('#mashup-form').on('click', '.remove-generic', function () {
$(this).parent().parent().remove();
var index = 0;
var itemIndex = 0;
$('#genericObjects div.row.generic div.col-md-12').each(function () {
var this_row = $(this);
this_row.find('input[name$=".Name"]').attr('name', 'GenericObjects[' + itemIndex + '].Name');
this_row.find('input[name$=".ForeignId"]').attr('name', 'GenericObjects[' + itemIndex + '].ForeignId');
this_row.find('input[name$=".Value"]').attr('name', 'GenericObjects[' + itemIndex + '].Value');
itemIndex++;
});
});


As for the nested objects, the indexation approach is very similar to the one in the accepted answer (in EditorFor system), but they have names. So, instead of [0].1.TESTAs[3], you would have TESTCs[0].TESTBs1.TESTAs[3] (this is for an object class TESTC, which containts TESTB class objects, which in turn contain TESTA class objects).

Sadly, I probably can't show the code I made for resetting the indexes (I think it was a rather nice reusable method), but I can outline the core concept. First of all, as long as .each refers to every HTML element created on the necessary, you only have to specify the root of the elements and just use .find, to find every input by their name and change the necessary index in them (you don't have to navigate to them, since you will want to reset all of them anyway).

As for the resetting, I was already using jquery, so I just used the .attr .replace method. The replacing was done via a regex, which I can hopefully somewhat share.

For the first hierarchical level, use this:

(.*?\[)(.*?)\]


and the string to replace by should be "$1" + index + "]"

enter image description here

Level 2 is simply copy paste of the first one:

(.*?\[)(.*?\[)(.*?)\], where (.*?\[) is a single instance of symbols till [


and the string to replace by should be "$1$2" + index + "]"

enter image description here

Further levels are achieved by just copying the (.*?[) whatever amount of times needed and adding accordingly "$1$2$3... group identifiers in the replacement string. This also seems pretty universal (doesn't matter what the object names actually are). You will of course need to specify all the input field names though. Have a good one. Hope this maybe helps someone somehow.

AFTER EDIT 2: I noticed in the comment by Stephen Muecke a rather nice way of dealing with the problem as well. In this particular situation I didn't use it though, as I want to avoid modifying the core system as much as possible. However, I would advise you to have a look at this solution, since this ultimately can make you avoid doing any index resetting whatsoever.

Answer

EDIT: On the html on the bottom you see that each html element that is part of a object inside a collection has a index number prepended to its name. You just need to be sure that when you submit the form all of the objects of a collection have consecutive indexes in the collection e.g. if you have elements 1, 2 and 3 and you remove element 2. When you submit the other two, the name attribute of the html elements must include consecutive indexes. Object 1 => "[0].property", Object 2(3 before you removed 2) => "[ 1].property" (for some reason I could not write the one inside square brackets without the space, but omit the space.

IMPORTANT: When you add a folder for Editor Templates, be sure to add it inside the Shared Folder and with the name: EditorTemplates (plural).

Have you tried using templates to display the GenericObjects instead of rendering a partialView for each element?

http://www.growingwiththeweb.com/2012/12/aspnet-mvc-display-and-editor-templates.html

That way you can use the EditorFor helper that handles the indexation of the collection of GenericObjects.

ASP.NET MVC 4 - EditorTemplate for nested collections

I wrote an example:

ViewModels

public class B
{
    public string Name { get; set; }
    public string ForeignId { get; set; }
    public int Value { get; set; }
}
public class A
{
    public B TestB1 { get; set; }
    public B[] TestB2 { get; set; }
}

Template for B

@model WebApplication1.Models.B

<div class="form-group">
    @Html.TextBoxFor(m => m.Name)
    @Html.TextBoxFor(m => m.ForeignId)
    @Html.TextBoxFor(m => m.Value)
</div>

Template for A

@model WebApplication1.Models.A

<div class="form-group">
    @Html.EditorFor(m => m.TestB1)
    @Html.EditorFor(m => m.TestB2)
</div>

Main View

@model IEnumerable<WebApplication1.Models.A>
@{
    Layout = null;
}

<div class="row">
    <div class="col-xs-12">
        @using (Ajax.BeginForm("Index", new AjaxOptions { HttpMethod = "POST" }))
        {
            @Html.EditorFor(m => m)
            <button type="submit">Submit</button>
        }
    </div>
</div>

Controller

public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var model = new List<A>
            {
                new A {
                        TestB1 = new B { Name = "a", ForeignId = "a1", Value = 1 },
                        TestB2 = new B[]
                        {
                            new B { Name = "b", ForeignId = "b2", Value = 2},
                            new B { Name = "c", ForeignId = "c3", Value = 3}
                        }
                     },
                new A {
                        TestB1 = new B { Name = "aa", ForeignId = "aa1", Value = 1 },
                        TestB2 = new B[]
                        {
                            new B { Name = "bb", ForeignId = "bb2", Value = 2},
                            new B { Name = "cc", ForeignId = "cc3", Value = 3}
                        }
                     },
                new A {
                        TestB1 = new B { Name = "aaa", ForeignId = "aaa1", Value = 1 },
                        TestB2 = new B[]
                        {
                            new B { Name = "bbb", ForeignId = "bbb2", Value = 2},
                            new B { Name = "ccc", ForeignId = "ccc3", Value = 3}
                        }
                     }
            };
            return View(model);
        }
        [HttpPost]
        public JsonResult Index(List<A> model)
        {
            return Json("a");
        }
    }

Rendered View

enter image description here

View Markup

<form id="form0" action="/" method="post" data-ajax-method="POST" data-ajax="true"><div class="form-group">
    <div class="form-group">
    <input name="[0].TestB1.Name" type="text" value="a">
    <input name="[0].TestB1.ForeignId" type="text" value="a1">
    <input name="[0].TestB1.Value" type="text" value="1" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
    <div class="form-group">
    <input name="[0].TestB2[0].Name" type="text" value="b">
    <input name="[0].TestB2[0].ForeignId" type="text" value="b2">
    <input name="[0].TestB2[0].Value" type="text" value="2" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div><div class="form-group">
    <input name="[0].TestB2[1].Name" type="text" value="c">
    <input name="[0].TestB2[1].ForeignId" type="text" value="c3">
    <input name="[0].TestB2[1].Value" type="text" value="3" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
</div><div class="form-group">
    <div class="form-group">
    <input name="[1].TestB1.Name" type="text" value="aa">
    <input name="[1].TestB1.ForeignId" type="text" value="aa1">
    <input name="[1].TestB1.Value" type="text" value="1" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
    <div class="form-group">
    <input name="[1].TestB2[0].Name" type="text" value="bb">
    <input name="[1].TestB2[0].ForeignId" type="text" value="bb2">
    <input name="[1].TestB2[0].Value" type="text" value="2" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div><div class="form-group">
    <input name="[1].TestB2[1].Name" type="text" value="cc">
    <input name="[1].TestB2[1].ForeignId" type="text" value="cc3">
    <input name="[1].TestB2[1].Value" type="text" value="3" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
</div><div class="form-group">
    <div class="form-group">
    <input name="[2].TestB1.Name" type="text" value="aaa">
    <input name="[2].TestB1.ForeignId" type="text" value="aaa1">
    <input name="[2].TestB1.Value" type="text" value="1" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
    <div class="form-group">
    <input name="[2].TestB2[0].Name" type="text" value="bbb">
    <input name="[2].TestB2[0].ForeignId" type="text" value="bbb2">
    <input name="[2].TestB2[0].Value" type="text" value="2" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div><div class="form-group">
    <input name="[2].TestB2[1].Name" type="text" value="ccc">
    <input name="[2].TestB2[1].ForeignId" type="text" value="ccc3">
    <input name="[2].TestB2[1].Value" type="text" value="3" data-val-required="The Value field is required." data-val-number="The field Value must be a number." data-val="true">
</div>
</div>            <button type="submit">Submit</button>
</form>