3

I have a form on a razor view page where I am adding rows dynamically using jQuery. I want to bind the dynamically created fields to an array so that I can browse through the array one at a time and insert them to a table in the database. The problem is the fields appear in the "FormCollection" as individual fields rather than as an array.

Please see the attached image for view page: enter image description here

jQuery script to add new rows:

$(function() {
    var tableRowNum = 1;
    $("#add-work-row").click(function () {
        tableRowNum++;
        var tableRow = "<tr>";
        tableRow += "<td><input name='works[" + (tableRowNum - 1) + "].workCover' type='checkbox' class='text' /></td>";
        tableRow += "<td><input name='works[" + (tableRowNum - 1) + "].workTitle' type='text' class='text work-title caps' /></td>";
        tableRow += "<td><input name='works[" + (tableRowNum - 1) + "].workComposers' type='text' class='text work-composer caps' /></td>";
        tableRow += "<td><input name='works[" + (tableRowNum - 1) + "].workPerformances' type='text' class='text work-performances' /></td>";
        tableRow += "<td><input name='works[" + (tableRowNum - 1) + "].workDuration' type='text' class='text work-duration input-duration' /></td>";
        tableRow += "<td><a href='#' class='delete-row'></a></td>";
        tableRow += "</tr>";
        $("#worksTable").append(tableRow);
        return false;
    });
});

The controller action is:

public ActionResult CreateReport(FormCollection form)
{
    // works is null?
    var works = form["works"];           
    foreach (var work in works)
    {
        // Do something                
    }
    return null;
}
TejSoft
  • 3,213
  • 6
  • 34
  • 58
  • 2
    Do not use `FormCollection` In MVC you bind to your model. And you have not even shown how your generating the new rows or the model you need to bind to. –  Oct 11 '16 at 03:47
  • 1
    Instead of `FormCollection` use `IEnumerable`, where the `YourViewModel` represents a row in a table. See Phil Haack's artile on model binding: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ – sakura-bloom Oct 11 '16 at 04:07
  • @StephenMuecke, I have added the jquery script that is adding rows to the table. I can probably create a model with a property works[] and bind to it. But I read in another SO answer that if we name the fields as "works[0].fieldName" it will bind to an array named "works". The concept didn't work here. – TejSoft Oct 11 '16 at 04:09
  • No, it would bind to a model that contains a property named `Works` which is a collection of a model that contains properties named `WorkCover`, `WorkTitle` etc. but there are other problems with your code and it will never work if you deleted an item. –  Oct 11 '16 at 04:12
  • Refer [this answer](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) for 2 options for how to do this. –  Oct 11 '16 at 04:13

1 Answers1

1

The script for adding new rows should be as follows.

Notice the works prefix in input field names, it will be used in the MVC controller action.

Also, the worksPerformedCollection is the same as the name of the property in your viewmodel that contains rows for "Works Performed" section.

The index (specifically, works.worksPerformedCollection.index) is used to group inputs per row. It is not needed if tableRowNum is sequential, but needed if it's not. I am guessing your rows may not be sequentially numbered since you have the functionality in UI to remove rows.

$(function() {
    var tableRowNum = 1;
    $("#add-work-row").click(function () {
        tableRowNum++;
        var tableRow = "<tr>";
        // Index value for grouping (non-sequential) rows.
        tableRow += "<td><input type='hidden' name='works.worksPerformedCollection.index' value=" + (tableRowNum - 1) + " /></td>";
        // Checkbox.
        tableRow += "<td><input name='works.worksPerformedCollection[" + (tableRowNum - 1) + "].workCover' type='checkbox' class='text' value='true' />";
        tableRow += "<input type='hidden' name='works.worksPerformedCollection["+ (tableRowNum - 1) +"].workCover' value='false' /></td>";

        tableRow += "<td><input name='works.worksPerformedCollection[" + (tableRowNum - 1) + "].workTitle' type='text' class='text work-title caps' /></td>";
        tableRow += "<td><input name='works.worksPerformedCollection[" + (tableRowNum - 1) + "].workComposers' type='text' class='text work-composer caps' /></td>";
        tableRow += "<td><input name='works.worksPerformedCollection[" + (tableRowNum - 1) + "].performances' type='text' class='text work-performances' /></td>";
        tableRow += "<td><input name='works.worksPerformedCollection[" + (tableRowNum - 1) + "].duration' type='text' class='text work-duration input-duration' /></td>";
        tableRow += "<td><a href='#' class='delete-row'></a></td>";
        tableRow += "</tr>";
        $("#worksTable").append(tableRow);
        return false;
    });
});

The input fields in the "Performance Details" section should be named as works.performanceDate, works.performerName, etc.

On the server side you have the following classes.

public class PerformanceViewModel
{
    [Required]
    public DateTime PerformanceDate { get; set; }

    [Required]
    public string PerformerName { get; set; }

    // ...

    public IEnumerable<WorksPerformedViewModel> WorksPerformedCollection { get; set; }
}

public class WorksPerformedViewModel
{
    public bool WorkCover { get; set; }
    public string WorkTitle { get; set; }
    public string WorkComposers { get; set; }
    public int? Performances { get; set; }
    public TimeSpan? Duration { get; set; }
} 

And in the MVC controller you have this.

[HttpPost]
public ActionResult CreateReport(PerformanceViewModel works)
{
    // ...
}

Notice that the controller action accepts the viewmodel as works - this is the same prefix used when naming input fields on the client side.

sakura-bloom
  • 4,524
  • 7
  • 46
  • 61