MVC 5 BeginCollectionItem with Partial CRUD
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;}
}
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>
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"/>
}
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.
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)
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.