9

What is the best way to add/delete rows to a table when a button is clicked? I need rows created from ChildClass properties (child class is a list within the main class/model).

Currently have a View (model is MyMain) which references a Partial View using RenderPartial.

The partial view displays the properties of the model, a class called MyChild which is an object list within MyMain.

I want to have add and delete buttons to dynamically add the rows which are held within the partial view.

So adding MyChild repeatedly for more rows on the list. Is this possible? Or should I not be using partial views for this?

Updated Code

Below are the current classes and views I'm working with, I've been trying to implement the BeginCollectionItem helper but I'm getting null ref where I'm trying to load the partial view despite the if statement saying to create a new instance of the child class if doesn't exist - why is this being ignored?

Main View

    @using (Html.BeginForm())
{
    <table>
        <tr>
            <th>MyMain First</th>
            <th>MyChild First</th>
        </tr>
        <tr>
            <td>
                @Html.EditorFor(m => m.First)
            </td>
            <td>
                @if (Model.child != null)
                {
                    for (int i = 0; i < Model.child.Count; i++)
                    {
                        Html.RenderPartial("MyChildView");
                    }
                }        
                else
                {
                    Html.RenderPartial("MyChildView", new MvcTest.Models.MyChild());
                }       
            </td>
        </tr>
        @Html.ActionLink("Add another", "Add", null, new { id = "addItem" })
    </table>
}

Partial View

@model MvcTest.Models.MyChild

@using (Html.BeginCollectionItem("myChildren"))
{
    Html.EditorFor(m => m.Second);
}

Models

public class MyMain
{
    [Key]
    public int Id { get; set; }
    public string First { get; set; }
    public List<MyChild> child { get; set; }
}

public class MyChild
{
    [Key]
    public int Id { get; set; }
    public string Second { get; set; }
}

Controller

public class MyMainsController : Controller
{
    // GET: MyMains
    public ActionResult MyMainView()
    {
        return View();
    }

    [HttpPost]
    public ActionResult MyMainView(IEnumerable<MyChild> myChildren)
    {
        return View("MyMainView", myChildren);
    }

    public ViewResult Add()
    {
        return View("MyChildView", new MyChild());
    }
}
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
PurpleSmurph
  • 2,055
  • 3
  • 32
  • 52
  • 3
    Some options for dynamically adding and deleting objects [here](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) and [here](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) –  Apr 17 '15 at 13:28
  • Something like this: https://github.com/danludwig/BeginCollectionItem – Dawood Awan Apr 17 '15 at 13:43
  • Thanks @StephenMuecke I've just been reviewing what you suggested - is that BeginCollectionItem mentioned in one of the answers the best way? Looks extremely confusing. Basically I want to be able to add child model properties as a row as the client wants (there will be further validation but at a later date). Is using a partial view the best way to achieve this? Cheers for your help. – PurpleSmurph Apr 17 '15 at 15:34
  • 1
    I would recommend the `BeginCollectionItem` method. Its quite simple - just a partial with the helper. Existing items are generated in a `foreach` loop. New items are added using ajax to call a method that returns the partial.. –  Apr 17 '15 at 22:35
  • Hi @StephenMuecke, I attempted to use that over the weekend but couldn't get it working. I've updated the initial question to show a different potential approach, is this viable or even possible? Thanks. – PurpleSmurph Apr 21 '15 at 11:35
  • No its not possible if you want to be able to dynamically add and delete items in the view. –  Apr 21 '15 at 11:39

2 Answers2

12

Updated Answer - The original code is NOT true to 'dynamic', however it allows for everything I needed to do within the question parameters.

Initially, I couldn't get Stephen's BCI suggestion in the question comments working, since then I have and it's brilliant. The code below in the updated section will work if you copy + paste, but you will need to either manually download BCI from GIT or use PM> Install-Package BeginCollectionItem with Package Manager Console in Visual Studio.

I had some issue with various points using BCI due to the complexity and not having done MVC before - here is more information on dealing with accessing class.property(type class).property(type class).property.

Original answer - I've gone with a more clear example below than in my question which quickly got too confusing.

Using two partial views, one for the list of employees and another for the creation of a new employee all contained within the viewmodel of companyemployee which contains an object of company and a list of employee objects. This way multiple employees can be added, edited or deleted from the list.

Hopefully this answer will help anyone looking for something similar, this should provide enough code to get it working and at the very least push you in the right direction.

I've left out my context and initialiser classes as they're only true to code first, if needed I can add them.

Thanks to all who helped.

Models - CompanyEmployee being the view model

public class Company
{
    [Key]
    public int id { get; set; }
    [Required]
    public string name { get; set; }
}

public class Employee
{
    [Key]
    public int id { get; set; }
    [Required]
    public string name { get; set; }
    [Required]
    public string jobtitle { get; set; }
    [Required]
    public string number { get; set; }
    [Required]
    public string address { get; set; }
}

public class CompanyEmployee
{
    public Company company { get; set; }
    public List<Employee> employees { get; set; }
}

Index

@model MMV.Models.CompanyEmployee
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>
<fieldset>
    <legend>Company</legend>
    <table class="table">
        <tr>
            <th>@Html.LabelFor(m => m.company.name)</th>
        </tr>
        <tr>
            <td>@Html.EditorFor(m => m.company.name)</td>
        </tr>
    </table>
</fieldset>
<fieldset>
    <legend>Employees</legend>

        @{Html.RenderPartial("_employeeList", Model.employees);}

</fieldset>
<fieldset>
    @{Html.RenderPartial("_employee", new MMV.Models.Employee());}
</fieldset>
<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <input type="submit" value="Submit" class="btn btn-default" />
    </div>
</div>

PartialView of List of Employees

@model IEnumerable<MMV.Models.Employee>
@using (Html.BeginForm("Employees"))
{
    <table class="table">
        <tr>
            <th>
                Name
            </th>
            <th>
                Job Title
            </th>
            <th>
                Number
            </th>
            <th>
                Address
            </th>
            <th></th>
        </tr>
        @foreach (var emp in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => emp.name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => emp.jobtitle)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => emp.number)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => emp.address)
                </td>
                <td>
                    <input type="submit" formaction="/Employees/Edit/@emp.id" value="Edit"/>
                    <input type="submit"formaction="/Employees/Delete/@emp.id" value="Remove"/>
                </td>
            </tr>
        }
    </table>
}

Partial View Create Employee

@model MMV.Models.Employee

@using (Html.BeginForm("Create","Employees"))
{
    <table class="table">

        @Html.ValidationSummary(true, "", new { @class = "text-danger" })

        <tr>
            <td>
                @Html.EditorFor(model => model.name)
                @Html.ValidationMessageFor(model => model.name, "", new { @class = "text-danger" })
            </td>
            <td>
                @Html.EditorFor(model => model.jobtitle)
                @Html.ValidationMessageFor(model => model.jobtitle)
            </td>
            <td>
                @Html.EditorFor(model => model.number)
                @Html.ValidationMessageFor(model => model.number, "", new { @class = "text-danger" })
            </td>
            <td>
                @Html.EditorFor(model => model.address)
                @Html.ValidationMessageFor(model => model.address, "", new { @class = "text-danger" })
            </td>
        </tr>
    </table>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Create" class="btn btn-default" />
        </div>
    </div>
}

Controller - I used multiple but you can put them all in one

public class CompanyEmployeeController : Controller
{
    private MyContext db = new MyContext();

    // GET: CompanyEmployee
    public ActionResult Index()
    {
        var newCompanyEmployee = new CompanyEmployee();
        newCompanyEmployee.employees = db.EmployeeContext.ToList();
        return View(newCompanyEmployee);
    }

    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Employee employee = db.EmployeeContext.Find(id);
        db.EmployeeContext.Remove(employee);
        db.SaveChanges();
        return RedirectToAction("Index", "CompanyEmployee");
    }

    [HttpPost]
    public ActionResult Create([Bind(Include = "id,name,jobtitle,number,address")] Employee employee)
    {
        if (ModelState.IsValid)
        {
            db.EmployeeContext.Add(employee);
            db.SaveChanges();
            return RedirectToAction("Index", "CompanyEmployee");
        }

        return View(employee);
    }
}

Updated Code - using BeginCollectionItem - dynamic add/delete

Student Partial

@model UsefulCode.Models.Person
<div class="editorRow">
    @using (Html.BeginCollectionItem("students"))
    {
        <div class="ui-grid-c ui-responsive">
            <div class="ui-block-a">
                <span>
                    @Html.TextBoxFor(m => m.firstName)
                </span>
            </div>
            <div class="ui-block-b">
                <span>
                    @Html.TextBoxFor(m => m.lastName)
                </span>
            </div>
            <div class="ui-block-c">
                <span>
                    <span class="dltBtn">
                        <a href="#" class="deleteRow">X</a>
                    </span>
                </span>
            </div>
        </div>
    }
</div>

Teacher Partial

@model UsefulCode.Models.Person
<div class="editorRow">
    @using (Html.BeginCollectionItem("teachers"))
    {
        <div class="ui-grid-c ui-responsive">
            <div class="ui-block-a">
                <span>
                    @Html.TextBoxFor(m => m.firstName)
                </span>
            </div>
            <div class="ui-block-b">
                <span>
                    @Html.TextBoxFor(m => m.lastName)
                </span>
            </div>
            <div class="ui-block-c">
                <span>
                    <span class="dltBtn">
                        <a href="#" class="deleteRow">X</a>
                    </span>
                </span>
            </div>
        </div>
    }
</div>

Register Controller

public ActionResult Index()
{
    var register = new Register
    {
        students = new List<Person>
        {
            new Person { firstName = "", lastName = "" }
        },
        teachers = new List<Person> 
        {
            new Person { lastName = "", firstName = "" }
        }
    };

    return View(register);
}

Register and Person Model

public class Register
{
    public int id { get; set; }
    public List<Person> teachers { get; set; }
    public List<Person> students { get; set; }
}

public class Person
{
    public int id { get; set; }
    public string firstName { get; set; }
    public string lastName { get; set; }
}

Index

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@model UsefulCode.Models.Register
<div id="studentList">
@using (Html.BeginForm())
{
    <div id="editorRowsStudents">
        @foreach (var item in Model.students)
        {
            @Html.Partial("StudentView", item)
        }
    </div>
    @Html.ActionLink("Add", "StudentManager", null, new { id = "addItemStudents", @class = "button" });
}
</div>

<div id="teacherList">
@using (Html.BeginForm())
{
    <div id="editorRowsTeachers">
        @foreach (var item in Model.teachers)
        {
            @Html.Partial("TeacherView", item)
        }
    </div>
    @Html.ActionLink("Add", "TeacherManager", null, new { id = "addItemTeachers", @class = "button" });
}
</div>


@section scripts {
    <script type="text/javascript">
    $(function () {
        $('#addItemStudents').on('click', function () {
            $.ajax({
                url: '@Url.Action("StudentManager")',
                    cache: false,
                    success: function (html) { $("#editorRowsStudents").append(html); }
                });
                return false;
            });
            $('#editorRowsStudents').on('click', '.deleteRow', function () {
                $(this).closest('.editorRow').remove();
            });
            $('#addItemTeachers').on('click', function () {
                $.ajax({
                    url: '@Url.Action("TeacherManager")',
                    cache: false,
                    success: function (html) { $("#editorRowsTeachers").append(html); }
                });
                return false;
            });
            $('#editorRowsTeachers').on('click', '.deleteRow', function () {
                $(this).closest('.editorRow').remove();
            });
        });
    </script>
}

StudentManager Action:

public PartialViewResult StudentManager()
{
    return PartialView(new Person());
}
Community
  • 1
  • 1
PurpleSmurph
  • 2,055
  • 3
  • 32
  • 52
  • How about the post method should be ? – Kelum Nov 15 '16 at 10:47
  • Sorry I don't know what you mean? – PurpleSmurph Nov 21 '16 at 09:35
  • When I post the main view Index with the Register model, On post it returns the 0 items in the teachers and students properties. – Ashish Shukla Nov 09 '17 at 10:03
  • @AshishShukla I don't have the code to hand, it was almost a year ago. However if you can show me your code I'll be happy to look into it. – PurpleSmurph Nov 09 '17 at 12:52
  • I just completed a similar form but using a lot of ajax calls and jquery for removal and duplicate handling. Works well, I used BCICore. I had some issues with null submits and it was due to names not matching up, also I forgot to add @Html.HiddenFor() for the properties I wanted to submit. – perustaja Mar 18 '20 at 19:20
3

You can use partial views for it, but the problem is that generating the Id properties with EditorFor causes duplicates to be generated.

What worked for me was using var view = @Html.Raw(@Html.Partial('Your partial')) inside javascript in my main view.

This variable will then have the view with the default ids, I then replace the id property and name property with id=YourMainListName_{ItemNumber}__Property and name=YourMainListName_{ItemNumber}__Property

Where {ItemNumber} will be the index of the new item in the list.

It takes lots of javascript to get this working correctly, but as long as you have the Id and Name properties set correctly everything should work correctly including model binding when posting back your page.

3dd
  • 2,520
  • 13
  • 20