15

I have made changes below to the question, which is still the same but hopefully a lot clearer through the models and in regards to what I want to achieve and where I've come up against issues.

Below are shown two classes, Company and Employee, Company has a list of Employees.

This will be an input form so there will be no data in there to begin with.

Ultimately I want the user to be able to add as many Employee objects to the Company object model as they want and for the Employee objects to be updated

Am I on the right track with using BeginCollectionItem so I can add/remove as many Employee objects as I want? When I click on the Add button it takes it to the partial view on another page (with AjaxActionLink) but not with JavaScript.

Update Removed AjaxActionLink and used JavaScript instead.

Index

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

<h2>Company</h2>
<div>
    @Html.LabelFor(m => m.Name)
    @Html.EditorFor(m => m.Name)
</div>
<fieldset>
    <legend>Employees</legend>
    <div id="new-Employee">
        @foreach (var Employee in Model.Employees)
        {
            Html.RenderPartial("_Employee", Employee);
        }
    </div>
    <div>
        <input type="button" id="addemployee" name="addemployee" value="Add Employee"/>
        <br/>
    </div>
    <br/>
    @section Scripts
    {
        <script type="text/javascript">
            $('#addemployee').on('click', function () {
                $.ajax({
                    async: false,
                    url: '/Company/AddNewEmployee'
                }).success(function (partialView) {
                    $('#new-Employee').append(partialView);
                });
            });
        </script>
    }
</fieldset>
<div>
    <input type="submit" value="Submit" />
</div>

_Employee PartialView

    @model MvcTest.Models.Employee

@using (Html.BeginCollectionItem("Employees"))
{
    <div class="employeeRow">
        @Html.LabelFor(m => m.Name)
        @Html.EditorFor(m => m.Name)

        @Html.LabelFor(m => m.Telephone)
        @Html.EditorFor(m => m.Telephone)

        @Html.LabelFor(m => m.Mobile)
        @Html.EditorFor(m => m.Mobile)

        @Html.LabelFor(m => m.JobTitle)
        @Html.EditorFor(m => m.JobTitle)

        <a href="#" class="deleteRow">Delete</a>
    </div>
}

@section Scripts
{
$("a.deleteRow").live("click", function(){
    $(this).parents("div.employeeRow:first").remove();
return false;
});
}

Controller

public class CompanyController : Controller
    {
        // GET: Company
        public ActionResult Index()
        {
            var newCompany = new Company();
            return View(newCompany);
        }
        public ActionResult AddNewEmployee()
        {
            var employee = new Employee();
            return PartialView("_Employee", employee);
        }
    }

Model

public class Company
    {
        [Key]
        public int Id { get; set; }
        [Display(Name = "Company")]
        public string Name { get; set; }
        public List<Employee> Employees { get; set; }

        //public Company()
        //{
        //    Employees = new List<Employee>
        //    {
        //        new Employee{ Name = "Enter name"}
        //    };
        //}
    }
    public class Employee
    {
        [Key]
        public int Id { get; set; }
        [Display(Name="Employee")]
        public string Name { get; set; }
        public string Telephone { get; set; }
        public string Mobile {get;set;}
        [Display(Name="Job Title")]
        public string JobTitle {get;set;}
    }
SelrekJohn
  • 476
  • 2
  • 6
  • 21
  • 1
    I think you want to dynamically add/remove items in classb: https://github.com/danludwig/BeginCollectionItem – Dawood Awan Apr 21 '15 at 14:38
  • 1
    this sounds like it's ideal for ajax use :) – jbutler483 Apr 21 '15 at 14:41
  • I'm currently looking into AJAX use with it, never used AJAX before, I've looked at posts regarding BeginCollectionItem but didn't fully understand it, being dynamic isn't a top priority for now. – SelrekJohn Apr 21 '15 at 14:43
  • @SelrekJohn I have added a basic structure using BeginCollectionItem in my answer. you will have to clean it up – Dawood Awan Apr 21 '15 at 15:17
  • If your partial view is displaying on another page, then you have not included the `jquery.unobtrusive-ajax.js` file (or you have the scripts in the wrong order, or have duplicate scripts). You also have `UpdateTargetId = "Employees"` but there does not appear to be an element with `id="Employees"` (but there is a `
    ` which looks like its where you want to render the partial?). In any case, get rid of `Ajax.ActionLink()` and just use `jquery` instead.
    –  Apr 27 '15 at 11:51
  • Thanks, just removed the Ajax entirely and kept with the JS, which adds another partial. How does this work with the Ids though, I need to be able to delete the rows (partial views)? Ultimately I want these objects to go into a DB at the same time. So CompanyId could appear in multiple Employee rows within a database to show the relation. Not asking how to send to DB, just how it can keep track of Ids of objects and cope with deletion of partialviews which are objects? – SelrekJohn Apr 27 '15 at 12:19
  • What do you mean _"with the Ids"_? Your partial down not render anything indicating an `ID` property. Your 'delete' link needs to include at `data-` attribute to hold the models `ID` value - `Delete` Then it the script you can access it us `var id=$(this).data('id');`. If its zero, its a new item, so just remove the corresponding row from the DOM, otherwise, pass the value to a controller which deletes the item in the database and then in the ajax success function, remove the corresponding row. –  Apr 27 '15 at 23:53

4 Answers4

15

You do not need to use BeginCollectionItem in order to achieve this. From having to look into it myself and trying to use it for a similar issue, it appears it was created for problems of this nature with earlier versions of MVC.

Use Partial Views to display and update the list. One partial view to display and iterate through the list of objects, and another to create a new object which upon post back to update the list will show the newly created object in the partial view with the list.

I posted a similar question on here which should solve your issue, click here

Hope this helps.

Update The reason your delete doesn't work is because you can't call JS from Partial View, put it in the main view (@section Script). Also I think you got a bit muddled with your class and id keywords in your divs, have a look below.

So you should have:

Partial View

@model MvcTest.Models.Employee
    @using (Html.BeginCollectionItem("Employees"))
    {
        <div id="employeeRow" class="employeeRow">
            @Html.LabelFor(m => m.Name)
            @Html.EditorFor(m => m.Name)

            @Html.LabelFor(m => m.Telephone)
            @Html.EditorFor(m => m.Telephone)

            @Html.LabelFor(m => m.Mobile)
            @Html.EditorFor(m => m.Mobile)

            @Html.LabelFor(m => m.JobTitle)
            @Html.EditorFor(m => m.JobTitle)

            <a href="#" id="deleteRow" class="deleteRow" onclick="deleteFunction()">Delete</a>
        </div>
    }

Main View

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

<h2>Company</h2>
<div>
    @Html.LabelFor(m => m.Name)
    @Html.EditorFor(m => m.Name)
</div>
<fieldset>
    <legend>Employees</legend>
    <div id="new-Employee">
        @foreach (var Employee in Model.Employees)
        {
            Html.RenderPartial("_Employee", Employee);
        }
    </div>
    <div>
        <input type="button" id="addemployee" name="addemployee" value="Add Employee"/>
        <br/>
    </div>
    <br/>
    @section Scripts
    {
        <script type="text/javascript">
            $('#addemployee').on('click', function () {
                $.ajax({
                    async: false,
                    url: '/Company/AddNewEmployee'
                }).success(function (partialView) {
                    $('#new-Employee').append(partialView);
                });
            });

            $("#deleteRow").live("click", function () {
                $(this).parents("#employeeRow:first").remove();
                return false;
            });
        </script>
    }
</fieldset>
<div>
    <input type="submit" value="Submit" />
</div>
Community
  • 1
  • 1
PurpleSmurph
  • 2,055
  • 3
  • 32
  • 52
  • 2
    Thank you for your solution. – Thomas.Benz Feb 19 '17 at 16:50
  • 1
    No problem, glad it helped. – PurpleSmurph Feb 19 '17 at 20:26
  • Hi @PurpleSmurph The solution mentioned by you works well when add new employee but On FormSubmit of main view - it does not bind the employee list in Company model on form post and results in the 0 items in the Employee collection.. Could you look into this.. – Ashish Shukla Nov 09 '17 at 09:44
  • @AshishShukla I've replied to your comment on another of my questions, this solution definitely works as I have used it in my projects multiple times. If you can post the code you're using I'd be happy to look into it. You may be best posting a question as I won't be able to get back to you until Tuesday. – PurpleSmurph Nov 09 '17 at 13:00
  • @PurpleSmurph, Yes I tried to pass the IList model in the main or parent view and same I receive in the Post action then it works fine, but If I pass the Company Model in the main view and receive same Company model in Post action then Company model in post action contains 0 records for the List , i.e. form data is not bind for child models in that case. – Ashish Shukla Nov 10 '17 at 07:12
  • @AshishShukla I didn't provide an example of the Company model, that was OP's code, I just demonstrated how to do it with the Employee model, but it works the same as the Employee model. Provided you're doing the same thing, just make sure all the names are altered. – PurpleSmurph Nov 14 '17 at 15:09
2

Lets say ClassA is your ViewModel:

You only need one Partial View or View to Update both:

e.g. Edit.cshtml

@model ClassA

@Html.BeginForm(){

@Html.TextBoxFor(m => m.name)

for(int i = 0; i< Model.classB.Count; i++){

     @Html.TextBoxFor(m => Model.classB[i].name)
<button type="button"> Add </button>
<button type="button"> Remove </button>
}

<input type="submit"  value = "save"/>

}

Note: In your Partial view you are using foreach loop, The MVC Model Binder Requires the Input fields to be in the format:

list[0].prop1
list[0].prop2
list[0].prop3

list[1].prop1
list[1].prop2
list[1].prop3

So for this we use for loop

Then in controller:

[HttpPost]
public ActionResult Edit(ClassA model){


// HEre you will see model.ClassB list
// you can then save them one by one

foreach(var item in Model.classB){

save
}
return View(model);
}

If you want to dynamically Add or Remove Items from the List:

Create another Partial View classBs:

        @model List<classB>
        <div id="lists">
            foreach (var contact in Model)
            {
               @Html.Partial("ClassBRow", contact)
            }
    </div>

<button data-action-url="@Url.Action("ClassBRow","CONTROLLER")" class="btn btn-primary pull-right" id="addItem">Add</button>

<script>
 $("#addItem").click(function () {
            var btn = $(this);
            $.ajax({
                url: btn.data('action-url'),
                success: function (html) {
                    $("#lists").append(html);
                }
            });
            return false;
        });

</script>

Create another Partial View: ClassBRow.cshtml:

@model classB

 using (Html.BeginCollectionItem("classB"))
    {
            @Html.HiddenFor(m => m.isDeleted, new { data_is_deleted = "false" })
            @Html.TextBoxFor(m => Model.name, new { @class = "form-control" })

<span class="glyphicon glyphicon-trash" data-action="removeItem" title="remove" style="cursor:pointer"></span>

    }

In your controller:

public ActionResult ClassBRow()
{
    return PartialView(new classB());
}

AND Edit.cshtml becomes:

    @model ClassA

    @Html.BeginForm(){

    @Html.TextBoxFor(m => m.name)

@Html.Partial("classBs", model.classB)

    <input type="submit"  value = "save"/>

    }
Dawood Awan
  • 7,051
  • 10
  • 56
  • 119
1

Treat the views as independent. If your partial view was a full view, you would pass it a IEnumerable<ClassB>

so render with:

 @Html.Partial("ClassBView", Model.ClassB)

Also, you can't use foreach with EditorFor as there is insufficient information to create a name based on the index. Use a normal for loop instead. The expression parser can then convert it to name="name[1]" etc as the index is available in the expression:

@model IEnumerable<Project.Models.ClassB>

@if(Model != null)
{
    for (int i = 0; i < Model.Count; i++)
    {
    <tr>
        <td>
            @Html.EditorFor(modelItem => Model[i].Name)
            <input type="button" value="Clear" />
            <input type="submit" value="Create" />
        </td>
    </tr>
    }
}

There is more missing from your example (what Clear and Create connect to for instance), so if you can provide the rest of your controller code I will expand this.

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • Thanks a lot for the answer, +1 for you, will try it out, looks a sound idea, at the moment the two buttons due nothing, I've not created much in the controller I'm attempting to build everything from scratch. What I hope to do with them is to add and delete rows. – SelrekJohn Apr 21 '15 at 14:41
  • 1
    Use client-side code (e.g. Ajax calls) to do those. – iCollect.it Ltd Apr 21 '15 at 14:42
1

You can use (EditorTemplates) to view (ClassB) as following:

1- Create folder named (EditorTemplates) under the (Views/Home) folder (Assuming your controller name is Home):

2- Under the created (EditorTemplates) folder, create a view named (ClassB)

enter image description here

and add the following template for the (ClassB) view:

@model Project.Models.ClassB
@if(Model != null)
{

    <tr>
        <td>
            @Html.EditorFor(modelItem => Model.Name)
            <input type="button" value="Clear" />
            <input type="submit" value="Create" />
        </td>
    </tr>

}

and (ClassAView) should be as following:

@model Project.Models.ClassA

<tr>
<td>
@Html.EditorFor(m => m.name)
</td>
<td>
@Html.EditorFor(m => m.classB);
</td>
</tr>

The editor will automatically iterate through the list of objects rendering the view for each of them.

Ala
  • 1,505
  • 1
  • 20
  • 36
  • Cute trick. I am curious how it resolve the names in the `EditorFor` without the indexing context? – iCollect.it Ltd Apr 21 '15 at 14:56
  • The name of the view under (EditorTemplates) should be the same in (@Html.EditorFor(m => m.ClassB)) so the mapping will be correct. – Ala Apr 21 '15 at 14:58
  • 1
    I still don't see how the inner `@Html.EditorFor(modelItem => Model.Name)` can generate anything other than `name="Name"` (instead of `name="Name[n]"`, unless they do something different under the hood for templates. I will try this myself. Thanks for sharing it. – iCollect.it Ltd Apr 21 '15 at 15:04