11

In my my MVC 4 application, I have a Customer which can have multiple sites and can subscribe to multiple service packages. A short version of my view model looks like below

public class SubscriptionModel
{        
    public int MemberId { get; set; }
    public List<SitePackage> SitePackges { get; set; }
    public SubscriptionModel()
    {
        SitePackges=new List<SitePackage>();
    }
}

public class SitePackage
{
    public int SiteId { get; set; }
    public List<PackageDisplayItem> LstPackageDisplayItems { get; set; }
    public SitePackage()
    {
        LstPackageDisplayItems=new List<PackageDisplayItem>();

    }
}

public class PackageDisplayItem
{
    public int PackageId { get; set; }

    [Display(Name = "Package")]
    public string Name { get; set; }        

    [DataType(DataType.Date)]
    [Display(Name = "Start Date")]
    public DateTime? StartDate { get; set; }

}

In my controller I fill in the model and then pass to View Model for rendering

 @using (@Html.BeginForm("CalculateCost", "HelpDesk", FormMethod.Post, new { @class = "form", id = "PackageSubscription", name = "PackageSubscription" }))
 {
 @Html.HiddenFor(x=>x.MemberId)

 <table class="table">
  @foreach (var site in Model.SitePackges)
  {
    <input name="SiteId" id="SiteId" type="hidden" value=@site.SiteId.ToString() />
           <tr><td class="col-sm-3">@site.SiteId</td></tr>
           <tr>
               <th class="col-sm-3">
                   Name
               </th>

               <th class="col-sm-2">
                   Start Date
               </th>
           </tr>
           @Html.Partial("_Packages",site.LstPackageDisplayItems)               

       }

My partial view is like

@model List<PackageDisplayItem>

@for (int i = 0; i < Model.Count; i++)
{
    @Html.HiddenFor(x => x[i].PackageId)
    <tr id="@Model[i].PackageId">
        <td>
            @Html.DisplayFor(x => x[i].Name)
        </td>

        <td>
            @Html.TextBoxFor(x => x[i].StartDate, "{0:d MMM yyyy}", new { @class = "jquery_datepicker form-control", autocomplete = "off" })
        </td>
    </tr>

}

Every thing renders fine but on the form post the model binder is not binding SitePackges list and its count is always 0. My controller has the following signatures.

[HttpPost]
    public ActionResult CalculateCost(SubscriptionModel subscriptionModel )
    {
        var receivedModel = subscriptionModel;         
    }

Not sure if the model I have designed is the best approach to handle this requirement (The requirement is to show a single site and just below it show the packages and then 2nd site and packages and so on). The controls seems to have unique indexes generated.

Jquery Post

function SubmitForm() {
console.log($("#PackageSubscription").serialize());
$.ajax({
    url: '/HelpDesk/CalculateCost',
    cache: false,
    dataType: 'json',
    data: $("#PackageSubscription").serialize(),
    type: 'POST',
    success: function (data) {
    }

});

}

I will appreciate any help. thanks

tereško
  • 58,060
  • 25
  • 98
  • 150
rumi
  • 3,293
  • 12
  • 68
  • 109

2 Answers2

16

You current implementation is rendering inputs that look like:

<input ... name="[0].Name" .../>
<input ... name="[1].Name" .../>

but in order to bind to to you model they would need to look like this:

<input ... name="SitePackges[0].LstPackageDisplayItems[0].Name" .../>
<input ... name="SitePackges[0].LstPackageDisplayItems[1].Name" .../>
<input ... name="SitePackges[1].LstPackageDisplayItems[0].Name" .../>
<input ... name="SitePackges[1].LstPackageDisplayItems[1].Name" .../>

A: You either need to render the controls in nested for loops

for(int i = 0; i < Model.SitePackges.Count; i++)
{
  @Html.HiddenFor(m => m.SitePackges[i].SiteId)
  for(int j = 0; j < Model.SitePackges[i].LstPackageDisplayItems.Count; j++)
  {
    @Html.TextBoxFor(m => m.SitePackges[i].LstPackageDisplayItems[j].Name)
  }
}

B: or use custom EditorTemplates for your model types

Views/Shared/EditorTemplates/SitePackage.cshtml

@model SitePackage
@Html.HiddenFor(m => m.SiteId)
@Html.EditorFor(m => m.LstPackageDisplayItems)

Views/Shared/EditorTemplates/PackageDisplayItem.cshtml

@model PackageDisplayItem
@Html.TextBoxFor(m => m.Name)

and in the main view

@model SubscriptionModel
@using (@Html.BeginForm())
{
  @Html.HiddenFor(m => m.MemberId)
  @Html.EditorFor(m => m.SitePackges)
  <input type="submit" />
}
Trikaldarshiii
  • 11,174
  • 16
  • 67
  • 95
  • The binding works totally fine with the form submit but when I m trying to submit the form via jquery and serialize the form, the binding again does not work. I have added the jquery call back code in the question. The log on the console shows all the form values which are require to bind with the correct indexing. Any ideas? – rumi Mar 10 '15 at 13:53
  • 1
    Can't remember the right configuration of ajax options (will need to check later) but if you just use `$.post('@Url.Action("CalculateCost", "HelpDesk")', $('form').serialize(), function (data) { ...});` it should work fine (jquery `.post` works it all out for you) –  Mar 10 '15 at 23:22
  • 1
    Thanks Stephen. I tried it and few other ways but none of the solutions work. I have create a question on SO http://stackoverflow.com/questions/29013224/jquery-ajax-form-submit-wont-bind-a-complex-model-in-mvc-4 – rumi Mar 12 '15 at 16:21
  • Could you kindly comment on this question http://stackoverflow.com/questions/30510221/how-to-fix-the-field-must-be-a-date-on-a-datetime-property-in-mvc Apologies I could not find another way to request attention of experts like you for a question – rumi May 28 '15 at 21:42
  • @Mike, Then you have not done it correctly and I cannot guess what mistakes you made. Ask a new question showing your code so you can get an answer showing how to correct it. –  Jun 21 '17 at 22:01
  • commented as an answer. Wanted to give a visual. – Mike Jun 22 '17 at 15:11
  • This is a very helpful answer which I have upvoted, but changing from partials to editor templates did not help. My problem is that I have a very large and nested view model. The List property is several layers deep. It is generating name like this `LstPackageDisplayItems[j].Name`, but it should be like this `rootObject.LstPackageDisplayItems[j].Name`. Therefore I am forced to pass the rootObject into my template. – Jess Aug 02 '17 at 18:50
  • @Jess, You can use `EditorTemplates` for as many nested levels as you want. It its not generating the correct `name` attributes, then you not doing it correctly :) –  Aug 02 '17 at 22:09
0

I found that you need to have the ID to the Item in the containing parent panel.

int pos = Model.Products.Count - 1;
for (int j = 0, a = Model.Products.Count; j < a; j++) {
    // I exclude the last one because the users is adding it 
    // and will be displayed in another section.  else causes problems
    if (j != pos) {  
    <div>
        @Html.HiddenFor(m => m.Products[j].Id)

        <span>
            @Html.DisplayTextFor(m => m.Products[j].Description)
            @Html.HiddenFor(m => m.Products[j].Description)
        </span>
        <span>
            @Html.DisplayTextFor(m => m.Products[j].Style)
            @Html.HiddenFor(m => m.Products[j].Style)
        </span>
    </div>
    }
}

this did not work below

for (int j = 0, a = Model.Products.Count; j < a; j++) {
    if (j != pos) {
    <div>
        @Html.HiddenFor(m => m.Products[j].Id)
        @Html.HiddenFor(m => m.Products[j].Description)
        @Html.HiddenFor(m => m.Products[j].Style)
    </div>
    }
}

or this

for (int j = 0, a = Model.Products.Count; j < a; j++) {
    if (j != pos) {  
    <div>
        <span>
            @Html.HiddenFor(m => m.Products[j].Id)
        </span>

        <span>
            @Html.DisplayTextFor(m => m.Products[j].Description)
            @Html.HiddenFor(m => m.Products[j].Description)
        </span>
        <span>
            @Html.DisplayTextFor(m => m.Products[j].Style)
            @Html.HiddenFor(m => m.Products[j].Style)
        </span>
    </div>
    }
}
Mike
  • 623
  • 6
  • 26