0

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.

Community
  • 1
  • 1
Dahamsta
  • 97
  • 2
  • 9
  • 1
    All you need to do is include a hidden input for the Indexer which allows you to post back non-zero, non-consecutive indexed items. Refer [this answer](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) for an example –  Aug 15 '16 at 07:21

1 Answers1

1

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>
Community
  • 1
  • 1
Eric
  • 196
  • 1
  • 9
  • Haven't tried it yet. I guess I will look into it tomorrow, provided another option doesn't come up. The system wasn't actually programmed by me, so I am a bit weary about making too many changes, if it can be avoided. That being said, maybe it can't be. Also, will this approach work for objects within objects. Let's say I have two A class objects TESTA1 and TESTA2, that contain respectively TESTB1, TESTB2 and TESTB3, TESTB4 of B Class. Will I bee able to edit both the first and second level objects (both A and B class)? – Dahamsta Aug 11 '16 at 16:23
  • Sure, you create one template for each type, I will try to improve my answer later. – Eric Aug 11 '16 at 16:33
  • Looks nice, after my last attempt at making that idea work (which it probably won't), I will try doing this. Thanks mate. However I am not quite sure of two things. First, how to add objects is there a way to acquire an index like in the my previous system? Also, is deleting them the same (with $(this).remove();)? Also, from what I understand, the deletion that was described in the article I used before should work now, right? And finally, with this being a generic submit button, what does it submit to (how does it decide what method is called)? Cheers, mate. – Dahamsta Aug 12 '16 at 06:02
  • With a few adjustments it should work. The code in the article is reassigning new indexes to the remaining elements. But you should keep an eye on the name attribute for each generated input, because `Model.GenericObjects` is probably not generated by the Html helpers, so maybe that is why the code is not working. You need to be sure how the html elements are being named by your view. – Eric Aug 12 '16 at 07:00
  • In the case of forms for ASP.NET MVC you are submitting every input that is inside the form and the framework is trying to bind that collection to the model or any other parameter on the action that is handling the submit request on the controller side. – Eric Aug 12 '16 at 07:01
  • As for adding new elements, if you are creating them server-side you could request the new element with a AJAX call using Jquery and create the markup also with Jquery. The server only cares that when you are submitting the form the name attributes are related to the model that the action is expecting. So as long as the added elements have the same name attribute structure as the other inputs of the same type(object) with a contiguous index, it does not matter if you create the new inputs server or client side. – Eric Aug 12 '16 at 07:11