2

Scenario :

The user can enter his/her name, grade and tutor's name. and then needs to enter subject marks. The subject form field is dynamic, in other words, one user may have 5 subjects and the other one may have 3 only.

Current Fix:

Currently, i have fixed this with spaghetti code, making use of jQuery's append on button click event. and passing the form data as FormCollection and then changed the formcollection to model.

What i need:

I need to make use of Model instead of form collection. That is, i need to pass the form data as model not formcollection.

Here's what I did:

Model:

public class StudentModel
{
    public string Name { get; set; }
    public string Grade { get; set; }
    public string Tutor { get; set; }
    public List<SubjectModel> Subjects { get; set; }
}

public class SubjectModel
{
    public string Subject { get; set; }
}

Controller:

public class HomeController : Controller
{
    ....
    [HttpPost]
    public ActionResult Index(FormCollection form)
    {
        var subjects = form["Subject"].Split(',');
        var modelsubjects = new List<SubjectModel>();
        foreach (var subject in subjects)
        {
            modelsubjects.Add(new SubjectModel() { Subject = subject });
        }

        var model = new StudentModel();
        model.Name = form["Name"];
        model.Grade = form["Grade"];
        model.Tutor = form["Tutor"];
        model.Subjects = modelsubjects;

        //Here I get the model from a dynamic form

        return View();
    }
}

View:

<div class="jumbotron">
    @using (Html.BeginForm("Index", "Home", FormMethod.Post))
    {
        <div class="row">
            <div class="col-sm-3">
                <label for="Name">Name</label>
                <input type="text" id="Name" name="Name" />
            </div>
            <div class="col-sm-3">
                <label for="Grade">Grade</label>
                <input type="text" id="Grade" name="Grade" />
            </div>
            <div class="col-sm-3">
                <label for="Tutor">Tutor</label>
                <input type="text" id="Tutor" name="Tutor" />
            </div>
            <div class="col-sm-1">
                <a class="btn btn-primary" id="addSubject">Add Subject</a>
            </div>
            <div class="col-sm-1">
                <input class="btn btn-primary" type="submit" value="Submit" />
            </div>
        </div>
        <div id="subjectInfo" style="padding-top:10px;">
        </div>
    }
</div>

<script type="text/javascript">
    var count = 0;
    $("#addSubject").click(function () {
        $("#subjectInfo").append("<div class='row'>" +
                "<div class='col-sm-8'>" +
                    "<label for='Subject_" + count + "'>Subject " + count + "</label>" +
                    "<input type='text' name='Subject' id='Subject_" + count + "' />" +
                "</div>" +
            "</div>");
        count++;
    });
</script>

Any advice would be helpful. Thank you.

Ranjith Varatharajan
  • 1,596
  • 1
  • 33
  • 76

2 Answers2

1

Since SubjectModel contains only one simple property, then this will be far easier if you just use a view model containing a property List<string> Subjects and then you inputs just need to have `

public class StudentVM
{
    public string Name { get; set; }
    public string Grade { get; set; }
    public string Tutor { get; set; }
    public List<string> Subjects { get; set; }
}

The Subjects property will be an array of the textbox values and then you can map to your data model in the POST method

[HttpPost]
public ActionResult Index(StudentVM model)
{
    var student = new StudentModel();
    student.Name = model.Name;
    ....
    foreach (var subject in model.Subjects)
    {         
        student.Subjects.Add(new SubjectModel() { Subject = subject });
    }
    // Save and redirect

If SubjectModel contained more than one property, then you would need to generate inputs with name prefix and indexer (refer this answer for some options)

Side note: You should be generating you inputs using the strongly types HtmlHelper methods so that you get correct 2-way model binding and client and server side validation.

@Html.LabelFor(m => m.Name)
@Html.TextBoxFor(m => m.Name)
@Html.ValidationMessageFor(m => m.Name)
Community
  • 1
  • 1
  • Thanks for the reply. Is there any way using only the `StudentModel` not `StudentVM` ? – Ranjith Varatharajan Feb 03 '17 at 04:45
  • @Stephen Muecke I think OP wants how to bind dynamically generated fields to `List` in model. – Mairaj Ahmad Feb 03 '17 at 04:46
  • Only if your controls had `name="Subjects[0].Subject"`, `name="Subjects[1].Subject"` etc (indexers must start at zero and be consecutive). But you should NEVER use a data model in a view, especially when editing so it should be irrelevant –  Feb 03 '17 at 04:47
  • @Mairaj, Oops - forgot to add the link :) But in this case, that would be a crazy to do to that trouble –  Feb 03 '17 at 04:49
  • @StephenMuecke yes that's what i was expecting. Now i have gone through another link and that looks more easy to me http://techiesweb.net/2012/09/17/asp-net-mvc3-dynamically-added-form-fields-model-binding.html – Mairaj Ahmad Feb 03 '17 at 04:53
  • @Mairaj, The link in your previous is a poor solution because it would fail if one of the collection items was deleted in the view –  Feb 03 '17 at 04:57
  • @StephenMuecke Oh really. I didn't know that thanks for info. – Mairaj Ahmad Feb 03 '17 at 04:59
1

I think you expecting this solution

Model:

 public class StudentModel
    {
        [Required]
        public string Name { get; set; }
        public string Grade { get; set; }
        public string Tutor { get; set; }
        public List<SubjectModel> Subjects { get; set; }
    }

    public class SubjectModel
    {
        [Required]
        public string Subject { get; set; }
    }

Controller:

 public class HomeController : Controller
    {
        ....

        [HttpPost]
        public ActionResult Index(StudentModel stu)
        {            
            if (ModelState.IsValid)
            {
                //db operations
                return RedirectToAction("Index");
            }
            return View(stu);
        }
        public ActionResult Subjects(int count)
        {
            ViewBag.count = count;
            return PartialView("SubjectPV");
        }

View:

@{
    ViewBag.Title = "Index";
}
@model DynamicModelForm.Models.StudentModel
<div class="jumbotron">
    @using (Html.BeginForm("Index", "Home", FormMethod.Post))
    {
        <div class="row">
            <div class="col-sm-3">
                <label for="Name">Name</label>
                @*<input type="text" id="Name" name="Name" />*@
                @Html.TextBoxFor(x => x.Name)
                @Html.ValidationMessageFor(x => x.Name)
            </div>
            <div class="col-sm-3">
                <label for="Grade">Grade</label>
                @*<input type="text" id="Grade" name="Grade" />*@
                @Html.TextBoxFor(x => x.Grade)
            </div>
            <div class="col-sm-3">
                <label for="Tutor">Tutor</label>
                @*<input type="text" id="Tutor" name="Tutor" />*@
                @Html.TextBoxFor(x => x.Tutor)
            </div>
            <div class="col-sm-1">
                <a class="btn btn-primary" id="addSubject">Add Subject</a>
            </div>
            <div class="col-sm-1">
                <input class="btn btn-primary" type="submit" value="Submit" />
            </div>
        </div>
        <div id="subjectInfo" style="padding-top:10px;">
            @{
                if (Model.Subjects != null)
                {
                    var count = 0;
                    foreach (var sub in Model.Subjects)
                    {
                        <div class='row'>
                            <div class='col-sm-8'>
                                <label for="Subject_@count">Subject @count</label>
                                @*@Html.TextBoxFor(x => x.Subject, new { id = "subject_" + count })*@
                                <input type="text" class="SubjectsSubject" name="Subjects[@count].Subject" id="subject_@count" value="@sub.Subject" />
                                @Html.ValidationMessageFor(x => x.Subjects[count].Subject)                                
                            </div>
                        </div>
                        count++;
                    }
                }
            }
        </div>
                }
</div>

<script type="text/javascript">
    //var count = 0;
    $("#addSubject").click(function () {
        var count = $('.SubjectsSubject').length
        $.ajax({
            url: '@Url.Action("Subjects","Home")' + "?count=" + count,
            success: function (result) {
                $("#subjectInfo").append(result);
                //count++;
            },
            error: function (req, status, error) {
                alert(error);
            }
        });
    });
</script>

Partial View For Dynamic:

@model DynamicModelForm.Models.SubjectModel

    <div class='row'>
        @{
            var count = ViewBag.count;
        }
        <div class='col-sm-8'>
            <label for="Subject_@count">Subject @count</label>
            @*@Html.TextBoxFor(x => x.Subject, new { id = "subject_" + count })*@
            <input type="text" class="SubjectsSubject" name="Subjects[@count].Subject" id="subject_@count"/>
            @Html.ValidationMessageFor(x => x.Subject)
            @*<span class="field-validation-valid" data-valmsg-for="Subjects[@count].Subject" data-valmsg-replace="true"></span>*@
        </div>
    </div>

Complete project in github

https://github.com/rajsram/DynamicModelForms