Get Selected Value of Dropdownlist in ActionLink (MVC 5) - asp.net-mvc

I am trying to get the selected value of a dropdownlist in #Html.ActionLink but no luck so far. Requirement is to dynamically retrieve a table and have a dropdown list for actions that can be taken against the row. I need to select an action and then on hitting submit button, row ID and selected action value should be posted to the controller. Here is the piece of code I have in place.
#foreach (AdsViewModel ad in Model)
{
<tbody>
<tr>
<td>#ad.Row_Id</td>
<td class=" "> #ad.Description </td>
<td class=" "> #ad.Valid_To.ToShortDateString() </td>
<td><span class="label label-sm label-success label-mini"> #ad.Status </span></td>
<td>#Html.DropDownList("actions", ad.Actions) </td>
<td>#Html.ActionLink("Submit", "AdAction", new {adId = ad.Row_Id, action = ad.Actions.SelectedValue}) </td>
</tr>
</tbody>
}
On clicking the Submit ActionLink, I am getting the adId but no action is returned from the dropdownlist.
Your help is much appreciated.
Edit: Here is the AdsViewModel
public class AdsViewModel
{
public string Row_Id { get; set; } //Transaction Number
public string Description { get; set; } //Trasaction Description
public DateTime Valid_To { get; set; } //Expiry
public string Status { get; set; } //Object Status Code
public SelectList Actions { get; set; }
}
This is how the Select list is filled in Controller
List<SelectListItem> items = new List<SelectListItem>();
items.Add(new SelectListItem() { Text = "View", Value = "001" });
items.Add(new SelectListItem(){Text = "Modify", Value = "002"});
model.Actions = items;

This line
<td>#Html.ActionLink("Submit", "AdAction", new {adId = ad.Row_Id, action = ad.Actions.SelectedValue}) </td>
is setting the route value action to the selected value at the time the view is created on the server and before its sent to the browser (the user hasn't selected anything yet so its null). If you are wanting to set the value to "001" or "002" (the values of the dropdowns), then you need to use javascript update the href attribute of the link when the dropdown changes. An easier and more conventional solution would be to delete the dropdown and use 2 action links, one for Viewand one for Edit. Since they are 2 different actions, there should also be 2 seperate ActionResult methods in your controller. For example
#Html.ActionLink("View", "View", new { id = ad.Row_Id }) // calls the View method
#Html.ActionLink("Modify", "Modify", new { id = ad.Row_Id }) // calls the Modify method
Edit
To do this using javascript, delete the #Html.ActionLink and replace with a <button type="button"> or other element and handle its click event
var url = '#Url.Action("AdAction")';
$('button').click(function() {
var row = $(this).closest('tr');
var rowID = row.children('td').eq(0).text();
var actionID = row.find('select').val();
window.location.href = url + '?adId=' + rowID + '&action=' + actionID;
});
Note: You are creating invalid html with the #Html.DropDownList() method (all <selects> will have id="action")

This should fix it...
#for (int i = 0; i < Model.Count(); i++)
{
var ad = Model[i];
<tbody>
<tr>
<td>#ad.Row_Id</td>
<td class=" "> #ad.Description </td>
<td class=" "> #ad.Valid_To.ToShortDateString() </td>
<td><span class="label label-sm label-success label-mini"> #ad.Status </span></td>
<td>#Html.DropDownListFor("actions", m => m[i].Actions) </td>
<td>#Html.ActionLink("Submit", "AdAction", new {adId = ad.Row_Id, action = ad.Actions.SelectedValue}) </td>
</tr>
</tbody>
}

Thank you Everyone, I solved it by writing a Javascript function and calling that function on the onClink Event of the button.
<script type="text/javascript" language="javascript">
function ShowEditData() {
var url = '#Url.Action("AdAction")';
var rows = document.getElementById("mytable").rows;
for (var i = 0, ceiling = rows.length; i < ceiling; i++) {
rows[i].onclick = function () {
debugger;
var Row_Id = this.cells[0].innerHTML;
var actionID = this.cells[4].childNodes[0].value;
window.location.href = url + '?row_id=' + Row_Id + '&action_id=' + actionID;
}
}
}
</script>

Related

MVC DropDownList lagging

I am posting the id of a dropdownlist back to the index (index2 view). but is lagging behind. After a second time pressing Select it shows me the correct list.
http://www.jeroenchristens.be/CountriesWorld
(the first page is only for showing the complete list, after selecting from the dropdownlist,, it gets to index2, a shorter list) And then after choosing another Selection from the dropdownlist, you have to try this twice each time.
I successfully copied this from the id the value and pass this on, why is it lagging behind.
Index2 Viewpage
#using System.Collections
#using System.Web.UI.WebControls
#model IEnumerable<CVtje.Models.Countries>
<h2>Index</h2>
#using (Html.BeginForm("Index2", "CountriesWorld", new { #id = Request.Form["SelectedContinent"] }, FormMethod.Post))
{
<div class="form-group">
#Html.DropDownList("SelectedContinent",
new SelectList((IEnumerable) ViewData["continentsList"], "Continent", "Continentomschrijving"))
<button type="submit" class="btn btn-primary">Select</button>
</div>
}
<table id="countriesworld" class="table table-active table-hover">
<thead>
<tr>
<th>Vlag</th>
<th>
Code
</th>
<th>
Land
</th>
<th>Continent</th>
</tr>
</thead>
#foreach (var item in Model)
{
<tr>
<td>
<img src="#string.Format("../../images/countries/{0}.png", item.Code)" width="25" HEIGHT="15" />
</td>
<td>
#item.Code
</td>
<td>
#item.Country
#*#Html.ActionLink("Details", "Index", "ReizensDetails", new { id = item.ReizenId }, null)*#
#*|
#Html.ActionLink("Details", "Details", new { id = item.Id }) |
<button data-myprofile-id="#item.Id" class="btn-link js-delete">Delete</button>*#
</td>
<td>#item.Continents.Continentomschrijving</td>
</tr>
}
</table>
my controller:
public ActionResult Index(int? id)
{
List<Continents> continentsList = new List<Continents>();
continentsList = _context.Continents.ToList();
ViewData["continentsList"] = continentsList;
var countriesWorld = _context.Countries.OrderBy(e => e.Country).ToList();
return View(countriesWorld);
}
[HttpPost]
public ActionResult Index2(int id)
{
//return View(db.MyProfiles.ToList());
List<Continents> continentsList = new List<Continents>();
continentsList = _context.Continents.ToList();
ViewData["SelectedContinent"] = id.ToString();
ViewData["continentsList"] = continentsList;
var countriesWorld = _context.Countries.Where(e => e.Continent == id).OrderBy(e => e.Country).ToList();
return View(countriesWorld);
You have added a route value using new { #id = Request.Form["SelectedContinent"] } in your BeginForm() method.
Assuming the initial value is 0, then it generates action = "/CountriesWorld/Index2/0". Lets assume you select the option with value="1" and you now post the form. The id attribute is bound to 0 and you filter the Countries based on .Where(e => e.Continent == 0) - no where have you ever used the value of the selected option which is bound to a non-existent property named SelectedContinent.
Now you return the view and the forms action attribute is now action = "/CountriesWorld/Index2/1" (because Request.Form["SelectedContinent"] is 1). If you select the option with value="2", the same thing occurs - you ignore the value of the selected option and the filter the Countries based on .Where(e => e.Continent == 1) because the id parameter is 1.
Always bind to a model, which in your case will be
public class CountriesVM
{
public int? SelectedContinent { get; set }
public IEnumerable<SelectListItem> ContinentsList { get; set; }
public IEnumerable<Country> Countries { get; set; }
}
and in the view, strongly bind to your model (note the FormMethod.Get and the 3rd parameter in DropDownListFor())
#model CountriesVM
#using (Html.BeginForm("Index", "CountriesWorld", FormMethod.Get))
{
#Html.DropDownListFor(m => m.SelectedContinent, Model.ContinentsList, "All")
<button type="submit" class="btn btn-primary">Select</button>
}
<table ... >
....
#foreach(var country in Model.Countries)
{
....
}
</table>
and you need only one method
public ActionResult Index(int? selectedContinent)
{
var countries = _context.Countries.OrderBy(e => e.Country);
if (selectedContinent.HasValue)
{
countries = countries.Where(e => e.Continent == selectedContinent.Value);
}
continentsList = _context.Continents.Select(x => new SelectListItem
{
Value = x.Continent.ToString(),
Text = x.Continentomschrijving
});
var model = new CountriesVM
{
SelectedContinent = selectedContinent,
ContinentsList = continentsList,
Countries = countries
};
return View(model);
}
Note you might also want to consider caching the Continents to avoid repeated database calls assuming they do not change often (and invalidate the cache if their values are updated)

Searching always returns empty list in MVC?

I'm trying to implement filtering in a page that contains some data, the page shows three different entities: Branches, Items and Categories, so I used a view model:
public class WarehouseData
{
public IEnumerable<Item> Items { get; set; }
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Branch> Branches { get; set; }
}
in the controller:
public ActionResult Index(string sort, string search)
{
var warhouse = new WarehouseData();
warhouse.Items = db.Items.Include(c => c.Categories).ToList();
warhouse.Branches = db.Branches;
ViewBag.Search = search;
warhouse.Branches = db.Branches.ToList();
switch (sort)
{
case "q_asc":
warhouse.Items = warhouse.Items.OrderBy(c => c.Quantity).ToList();
ViewBag.Sort = "q_desc";
break;
case "q_desc":
warhouse.Items = warhouse.Items.OrderByDescending(c => c.Quantity).ToList();
ViewBag.Sort = "q_asc";
break;
default:
warhouse.Items = warhouse.Items.OrderBy(c => c.Name).ToList();
ViewBag.Sort = "q_asc";
break;
}
if (!string.IsNullOrEmpty(search))
{
warhouse.Items = warhouse.Items.Where(i => i.Name.Contains(search)).ToList();
ViewBag.Search = search;
}
return View(warhouse);
}
this is the Index view:
#model WarehouseManagementMVC.ViewModels.WarehouseData
#{
ViewBag.Title = "Index";
}
<h2 style="display:inline-block">Registered Branches | </h2> #Html.ActionLink("Add New Branch", "Create", controllerName: "Branch")
#foreach (var branch in Model.Branches)
{
<ul>
<li>#Html.ActionLink(branch.Location, "Details", "Branch", routeValues: new { id = branch.Id }, htmlAttributes: null)</li>
</ul>
}
<hr />
<h2>All Items Available</h2>
<div>
#using (#Html.BeginForm("Index", "Warehouse", FormMethod.Get))
{
<input type="text" name="search" value="#ViewBag.Search"/>
<input type="submit" value="Filter" />
}
</div>
<table class="table table-bordered">
<tr>
<th>Product</th>
<th>#Html.ActionLink("Quantity", "Index", new { sort = ViewBag.Sort, search = ViewBag.Search })</th>
<th>Categories</th>
</tr>
#foreach (var item in Model.Items)
{
<tr>
<td>
#Html.ActionLink(item.Name, "Details", "Item", routeValues: new { id = item.Id }, htmlAttributes: null)
</td>
<td>
<span>#item.Quantity</span>
</td>
<td>
#{foreach (var cat in item.Categories)
{
#cat.Name <br />
}
}
</td>
</tr>
}
</table>
But when I search, the result is always empty list, why?
There's only two things that would cause Items to be empty here:
There's no items in the DB.
None of those items have Name values that contain the search term. Be advised in particular here that Contains is case-sensitive. So if the name is Foo and you searched for foo it will not match. If you want to do case-insensitive search, then you should cast both Name and search to either lowercase or uppercase before comparing:
Where(m => m.Name.ToLower().Contains(search.ToLower()))
While we're here. A search should ideally filter at the database level. Currently, you're selecting all items in the database and then filtering them in-memory, which is highly-inefficient. As soon as you you call ToList() in the second line of the action, the query has been sent, but you have to do so in order to set warhouse.Items. Instead, you should store your items in a temporary variable, i.e. IQueryable items = db.Items.Include(c => c.Categories); and do all your conditional ordering and filtering on that. Then, finally, set warhouse.Items = items.ToList();.
I found that the search is case sensitive, this solved it:
if (!string.IsNullOrEmpty(search))
{
warhouse.Items = warhouse.Items.Where(i => i.Name.ToLower().Contains(search.ToLower())).ToList();
ViewBag.Search = search;
}
If you have another suggestion for the code , I appreciate sharing it.

How to Add new Row Dynamically in ASP.Net MVC 5

I am looking for help on how to add a new row of LineItems to an Invoice in a create Razor view of an ASP.Net MVC 5 application. I have read almost all similar questions but none have addressed what I thought was a simple use case.
Here is my Invoice model class
public class Invoice
{
public int Id { get; set; }
public int InvoiceNumber { get; set; }
public List<LineItem> LineItems { get; set; }
public Client Customer { get; set; }
public DateTime DateCreated { get; set; }
public decimal Total { get; set; }
public Invoice()
{
LineItems = new List<LineItem>();
}
Take note that this invoice contains a List of LineItems and each line Item is a simple object. And a List of line items is created in the Invoice constructor. Here is the LineItem model class
public class LineItem
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
}
The generated ASP.Net MVC 5 Razor views did not recognize the LineItems list of the object and did not create any entry for it. I want to dynamically add a row to the table below and I want to make that row an instance of Line items.
Here is the Table showing the invoice
<table class="table table-condensed" id="invoiceTable">
<thead>
<tr id="invoiceTableHead">
<td><strong>Item Name</strong></td>
<td class="text-center"><strong>Item Description</strong></td>
<td class="text-center"><strong>Item Price</strong></td>
<td class="text-center"><strong>Item Quantity</strong></td>
<td class="text-right"><strong>Total</strong></td>
</tr>
</thead>
<tbody>
And here is my attempt at using JQuery to append a row to this table dynamically and I that is where I am stuck, any help or pointers that will be greatly appreciated.
<script type="text/javascript">
$("#lineItemButton").click(function () {
debugger;
// Create elements dynamically
var newRow = "<tr><td>'#Html.TextBoxFor(x => x.LineItems, new { ??? What do int public here)'</td></tr>";
// Add the new dynamic row after the last row
$('#invoiceTable tr:last').after(newRow);
});
</script>
You can create dynamic rows, but from my experience they will not bind to the model. I have a drop down that the user selects an asset number and clicks an 'Add' button that adds a new row, dynamically, to the table.
What I did was create a hidden row in a table to use a template.
<table class="table table-bordered table-condensed table-hover" id="lineItemTable" name="assetTable">
<thead>
<tr>
<th class="text-center">Item #</th>
<th class="text-center">Asset</th>
<th class="text-center">Condition</th>
<th class="text-center">Description 1</th>
<th class="text-center">Description 2</th>
<th class="text-center">Inventory Num</th>
<th class="text-center">Serial Number</th>
</tr>
</thead>
<tbody>
<tr hidden>
<td>
<label id="row"></label>
</td>
<td>
<input asp-for="TransferLineItem.AssisAsset" class="form-control" value=#ViewBag.AssisAsset />
</td>
<td>
<select asp-for="TransferLineItem.Condition" class="form-control" asp-items="#ViewBag.Conditions"></select>
</td>
<td>
<input asp-for="TransferLineItem.AssetDescription1" class="form-control" value=#ViewBag.AssetDescription1 />
</td>
<td>
<input asp-for="TransferLineItem.AssetDescription2" class="form-control" value=#ViewBag.AssetDescription2 />
</td>
<td>
<input asp-for="TransferLineItem.InventoryNum" class="form-control" />
</td>
<td>
<input asp-for="TransferLineItem.SerialNumber" class="form-control" value=#ViewBag.SerialNum />
</td>
</tr>
</tbody>
</table>
When the add button is clicked I use jQuery to clone the hidden table row and append the table with the new row. I append the id of each control with '_[row number]' so that each control had a unique id number.
//clones the first row of the table
var newRow = $("#lineItemTable tbody tr").first().clone();
//removes the 'hidden' attribute so it will be visible when added to the table
newRow.removeAttr("hidden");
//add/append new row to the table
$("tbody").append(newRow);
//get row number which will be appended to the id of each control in this row
//for example if this were the second row then the id of the asset field would be something like asset_2.
//note that since there is already a hidden row in the table, we subtract 1 from the row number
var rowNum = "_" + ($("#lineItemTable tbody tr").length-1);
//loop through the input controls and add the new id value
newRow.find("input").each(function () {
// get id of the input control
var ctrl = $(this).attr("id");
//concatenate the row number to the id
var newId = ctrl + rowNum;
//assign new id to control
$(this).attr("id", newId);
});
To save the data in the html table, I use jQuery to create an array of name-value pairs for each row, and pass that to a function in the controller.
//get table
var tbl = document.getElementById("lineItemTable");
//array to hold the json objects
var jsonArray = [];
//iterate through the fields and put values in the json object
for (var i = 1, r = tbl.rows.length-1; i < r; i++)
{
var jsonObj = {
asset: $("#TransferLineItem_AssisAsset_" + i).val(),
condition: $("#TransferLineItem_Condition_" + i).val(),
assetDescription1: $("#TransferLineItem_AssetDescription1_" + i).val(),
assetDescription2: $("#TransferLineItem_AssetDescription2_" + i).val(),
InventoryNum: $("#TransferLineItem_InventoryNum_" + i).val(),
serialNumber: $("#TransferLineItem_SerialNumber_" + i).val()
};
//put json object in array
jsonArray.push(jsonObj);
}
//pass json array to controller function to save line items
$.ajax({
type: "GET",
url: "Create?handler=SaveTransferLineItems",
contentType: "application/json; charset=utf-8'",
data: { jsonObj: JSON.stringify(jsonArray) },
success: function () {
showModal("btn-success", "Form Saved", "Your new transfer form was successfully saved.");
},
failure: function () {
showModal("btn-danger", "Save Failed", "Your form could not be saved, please contact site support");
}
});
In the controller function, I convert the name value pairs to a list of type 'TransferLineItem', a bound model. I can iterate over the list and use context to save to the database.
dynamic _json = JsonConvert.DeserializeObject<List<TransferLineItem>>(jsonObj);
foreach (TransferLineItem item in _json)
{
try
{
_context.TransferLineItem.Add(item);
int x = await _context.SaveChangesAsync();
if (x != 1)
{
ModalMessage = "Could not save items, starting at " + TransferLineItem.Asset;
return Page();
}
}
catch (Exception ex)
{
ModalType = "btn-danger";
ModalTitle = "Save Failed";
ModalMessage = ex.Message;
return Page();
}
}
I would not do this sort of thing dynamically by modifying the dom in the manner you describe. My preference would be to generate all of the necessary code in the razor view as if it was always there and then simply toggle the visibility of the row itself. This way the textbox is rendered properly as a form element when the view is generated, and you still have full access to modify the table with jQuery pending any AJAX requests.
On a side note, the behavior you're describing makes it sound like you're attempting to add more client side behaviors and reduce the number/size of the round trips to the server. If this is true, I would suggest exploring a JavaScript MVVM framework like Knockout, Ember or Angular.

Passing the selected value of the radiobutton to action link

I am very new to MVC, I have to display the value coming from the database in a table format and show the radio buttons in front of each display so that user can select whatever option they want to choose, I need to post that option to the controller. Below is what I am doing.
#model IList<CertificateModel>
#{
ViewBag.Title = "RefreshCertificates";
}
<h2>
RefreshCertificates</h2>
#using (Html.BeginForm())
{
<table cellpadding="1" style="text-align: center; border: 5 px">
<tr>
<td>
</td>
<td>
Name
</td>
<td>
Issuer
</td>
</tr>
#for (var i = 0; i <= Model.Count - 1; i++) {
#Html.HiddenFor(x=>x[i].Subject)
<tr>
<td>
#Html.RadioButtonFor(x => x[i].Subject, true, new { #name = "optionsRadios", #id = "rbtrue" })
</td>
<td>
#Html.DisplayFor(x => x[i].Subject)
</td>
<td>
#Html.DisplayFor(x => x[i].Issuer)
</td>
</tr>
}
</table>
}
<table>
<tbody>
<tr>
<td>
#Html.ActionLink("STANDARD", "SelectCertOk", "LogIn", new { Type = "STANDARD", }, new { #class = "button" })
</td>
<td>
</td>
</tr>
</tbody>
</table>
Below is my model
public class CertificateModel
{
public string Subject
{
get { return cert.Subject; }
}
public string Issuer
{
get { return cert.Issuer; }
}
public bool validCert { get; set; }
}
My Controller that is putting the data on the screen code is below:
public ActionResult RefreshCertificates()
{
certificates = new List<CertificateModel>();
// some code here to fill up the list
return View(certificates );
}
The output that is displayed on the page is like this(RB is a radio button)
Subject Issuer
RB Coffee Test1
RB Tea Test2
From the current database only two are output on the screen. the user is only supposed to select only one of the radio button and then hit the actionLink button.
My problem is that right now both the buttons are selected, i want only one of the radio button to be selected and also, I also want the value of that radio button to be posted to the controller.
so for e.g if the user selects Coffee and Test1 radio button then I want to pass
#Html.ActionLink("STANDARD", "SelectCertOk", "LogIn", new { Type = "STANDARD", }, new { #class = "button" })
Type=STANDARD and SubjectIssue Coffee,Test1 to the controller. My controller signature is like this
public void SelectCertOk(string Type, string SubjectIssue)
{
}
any help will be greatly appreciated.
You are doing the right thing, but unfortunately we can't override the name attribute like the id.
Update your view to include multiple radiobuttons with same 'name'.
#Html.RadioButton('Subject', x[i].Subject)
STANDARD
<script>
function SelectCertOk(){
var subject = $("input:radio[name=Subject]")
$.post("#Url.Content("~/ControllerName/SelectCertOk")", { SubjectIssue : subject}, function (data) {}
});
}
</script>
But it won't solve your purpose.
So i would prefer to use javascript to check/uncheck radiobuttons.

Can I display a list of complex objects in an MVC 4 view with add/remove capability?

I've been banging my head on this issue for several days now and can't figure it out. I need to create a view that allows users to add and remove complex hierarchical objects. Bogus example:
Models
public class ParentModel
{
public int ParentId { get; set; }
public string ParentText { get; set; }
public IList<ChildModel> Children { get; set; }
}
public class ChildModel
{
public int ChildId { get; set; }
public string ChildText { get; set; }
public IList<GrandChildModel> GrandChildren { get; set; }
}
public class GrandChildModel
{
public int GrandChildId { get; set; }
public string GrandChildText { get; set; }
}
Now, assuming I have an action that populates a ParentModel and it's child properties, a simple view could look like this:
#model EditorTemplateCollectiosns.Models.ParentModel
#using (#Html.BeginForm())
{
<div>
#Html.TextBoxFor(m => m.ParentId)
#Html.TextBoxFor(m => m.ParentText)<br/>
Children:<br/>
#for (var i = 0; i < Model.Children.Count; i++)
{
#Html.TextBoxFor(m => Model.Children[i].ChildId)
#Html.TextBoxFor(m => Model.Children[i].ChildText)
<div>
Grandchildren:<br/>
<table id="grandChildren">
<tbody>
#for (var j = 0; j < Model.Children[i].GrandChildren.Count; j++)
{
<tr>
<td>#Html.TextBoxFor(m => #Model.Children[i].GrandChildren[j].GrandChildId)</td>
<td>#Html.TextBoxFor(m => #Model.Children[i].GrandChildren[j].GrandChildText)</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
<button type="button" id="addGrandchild">Add Grandchild</button>
<input type="submit" value="submit"/>
}
<script type="text/javascript">
$(document).ready(function() {
$("#addGrandchild").click(function() {
$("#grandChildren tbody:last").append('<tr><td><input type="text" name="grandChildId" /></td><td><input type="text" name="grandChildText" /></td></tr>');
});
});
</script>
This view works fine unless I add a grandchild and submit the form, in which case it does not bind to the list of GrandChildModel. Fiddle of the POST:
So I tried using an editor template for GrandChildModel, just to see if it would work. This functions nicely for simply displaying the objects, but I have no idea how I would add a grandchild to the list and have it display in the editor template. I also tried nesting partial views loaded with $.ajax calls, but that turned into a logistical nightmare.
In the actual application, the hierarchy is deeper than this example, and each successive child object needs to have this add/remove capability. Is this even possible without a full post of the form every time the user wants to add or remove an object? With the approach in the example above, is it possible to have newly added grandchildren bind to the IList on POST? Alternatively, is there a way I can add an object on the client-side and have it display in an editor template and bind to the IList on POST? If neither option is the right approach, what is?
UPDATE
I've figured out something hacky that might work, but I would still love to know if there's an easier way to do this. I changed the above view code to this:
#model EditorTemplateCollectiosns.Models.ParentModel
#using (#Html.BeginForm())
{
<div>
#Html.TextBoxFor(m => m.ParentId)
#Html.TextBoxFor(m => m.ParentText)<br/>
Children:<br/>
<button type="button" id="addChild" onclick="addChildRow()">Add Child</button>
<table id="children">
<tbody class="childTableBody">
#for (var i = 0; i < Model.Children.Count; i++)
{
<tr>
<td colspan="3">Child #i</td>
</tr>
<tr class="childInfoRow">
<td>
#Html.TextBoxFor(m => Model.Children[i].ChildId)
</td>
<td>
#Html.TextBoxFor(m => Model.Children[i].ChildText)
</td>
<td>
<button type="button" id="removeChild#(i)" onclick="removeChildRow(this)">Delete</button>
</td>
</tr>
<tr>
<td colspan="3">
Grandchildren:<br/>
<button type="button" id="addGrandchild#(i)" onclick="addGrandchildRow(this)">Add Grandchild</button>
<table id="grandChildren#(i)">
<tbody>
#for (var j = 0; j < Model.Children[i].GrandChildren.Count; j++)
{
<tr>
<td>#Html.TextBoxFor(m => #Model.Children[i].GrandChildren[j].GrandChildId)</td>
<td>#Html.TextBoxFor(m => #Model.Children[i].GrandChildren[j].GrandChildText)</td>
<td><button type="button" id="removeGrandchild#(i)_#(j)" onclick="removeGrandchildRow(this)">Delete</button></td>
</tr>
}
</tbody>
</table>
</td>
</tr>
}
</tbody>
</table>
</div>
<input type="submit" value="submit"/>
}
Then, I added the following javascript. I can't use a class and jQuery click event handler as re-binding the handler after each add/remove doesn't seem to work right:
<script type="text/javascript">
function addChildRow() {
var newRowIndex = $("#children tr.childInfoRow").length;
var newRow = '<tr><td colspan="3">Child ' + newRowIndex + '</td></tr><tr class="childInfoRow"><td><input type="text" name="Children[' + newRowIndex + '].ChildId" /></td>' +
'<td><input type="text" name="Children[' + newRowIndex + '].ChildText" /></td>' +
'<td><button type="button" id="removeChild' + newRowIndex + '" onclick="removeChildRow(this)">Delete</button></td></tr>' +
'<td colspan="3">Grandchildren: <br/><button type="button" id="addGrandchild' + newRowIndex + '" onclick="addGrandchildRow(this)">Add Grandchild</button>' +
'<table id="grandChildren' + newRowIndex + '"><tbody></tbody></table></td></tr>';
$("#children tbody.childTableBody:last").append(newRow);
}
function addGrandchildRow(elem) {
var childIndex = $(elem).attr('id').replace('addGrandchild', '');
var newRowIndex = $("#grandChildren" + childIndex + " tr").length;
var newRow = '<tr><td><input type="text" name="Children[' + childIndex + '].GrandChildren[' + newRowIndex + '].GrandChildId" /></td>' +
'<td><input type="text" name="Children[' + childIndex + '].GrandChildren[' + newRowIndex + '].GrandChildText" /></td>' +
'<td><button type="button" id="removeGrandchild' + childIndex + '_' + newRowIndex + '" onclick="removeGrandchildRow(this)">Delete</button></td></tr>';
$("#grandChildren" + childIndex + " tbody:last").append(newRow);
}
function removeChildRow(elem) {
$(elem).closest('tr').prev().remove();
$(elem).closest('tr').next().remove();
$(elem).closest('tr').remove();
}
function removeGrandchildRow(elem) {
$(elem).closest('tr').remove();
}
</script>
So far, this is working, and the array values seem to be binding correctly. However, I'm not a huge fan of the huge pile of stuff required to add a row to either table. Does anyone have a better idea?
This is easier to accomplish if you stick with just one model. Instead of having ParentModel, ChildModel and GrandChildModel just have a HierarchichalPersonModel.
That way you would just need one view and one custom editor template. Ajax would be a little harder, but not the logistical nightmare you described.
Update: Added Examples
Person Model:
public class Person
{
public int Id { get; set; }
public string Text { get; set; }
public List<Person> Children { get; set; }
}
Fake Repository:
public static List<Person> PersonCollection { get; set; }
public static Person FindById(int id, List<Person> list = null)
{
if (list == null)
list = PersonCollection;
foreach (var person in list)
{
if (person.Id == id)
return person;
if (person.Children != null)
return FindById(id, person.Children);
}
return null;
}
I ended up using the solution in the edit to my original post. It's a ton of javascript and took a long time to test, but it works and is the only way I could find. If anyone is interested in the full solution, comment here and I will post it.

Resources