How to Add new Row Dynamically in ASP.Net MVC 5 - asp.net-mvc

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.

Related

ASP.NET Core - MVC. Drop down populate issue

I'm using vs 2017 and chrome.
I have an ASP.Net Core MVC app. It has a drop down that isn't showing the items correctly. The items displayed should be: Comedy, Western and Romantic.
Here's the debug image. The items shows the array and the text {MvcMovie.Modesl.Genre} and as I click each, the item text shows as I want.
I select the genres from the database and return them as as List and cast it to a SelectList as that is what is it is rendered as HTML a element with the collection of SelectListItem objects.
However, when I look at the Raw View, I only see the text {MvcMovie.Modesl.Genre} which is what is being shown in the drop down.
I populate the movieGenreVM and return it as a view in the Index action method.
public IActionResult Index(string movieGenre, string searchStringEntered)
{
// A list of genre objects.
SelectList genresList;
// A list of movie objects.
List<Movie> moviesList;
// Instantiate the View model.
var movieGenreVM = new MovieGenreViewModel();
// Get a list of genres from the database.
genresList = new SelectList(GetGenres(movieGenre));
// Get a list of movies from the database.
moviesList = GetMovies(searchStringEntered);
// Sets the models property which is then used in the dropdown of Genres.
movieGenreVM.genres = genresList;
// Creates a List object.
// Movie is populated from the database and used to generate an HTML table of loaded movies.
movieGenreVM.movies = moviesList;
// Passing the MovieGenreViewModel.
// Return the IActionResult - the Index.cshtml. A view template to generate an HTML response to the browser.
return View(movieGenreVM);
}
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
// A list of movies.
public List<Movie> movies;
// A SelectList containing the list of genres.
public SelectList genres;
// Contains the selected genre.
public string movieGenre { get; set; }
}
}
The genre models is:
public class Genre
{
public string MovieGenre { get; set; }
// Constructor.
public Genre()
{
}
public Genre(string a_MovieGenre)
{
MovieGenre = a_MovieGenre;
}
}
Here's the population of the genre list code:
// Create a list of genres.
private List<Genre> _genre = new List<Genre>();
public List<Genre> Genre
{
get
{
return _genre;
}
}
public List<Genre> GetGenres(string movieGenre)
{
Boolean errorSw = false;
// Declare the reader and initialize.
SqlDataReader GenresDataReader = null;
try
{
// Open the connection.
dbFunc.OpenDB();
// Get the list of distinct Genres by executing a stored procedure.
SqlCommand GenresCmd = new SqlCommand("dbo.SelectGenres", dbFunc.objConn);
GenresCmd.Parameters.Clear();
GenresCmd.CommandType = CommandType.StoredProcedure;
GenresCmd.Parameters.Add("#SearchText", SqlDbType.VarChar).Value = movieGenre;
// Set the reader.
GenresDataReader = GenresCmd.ExecuteReader();
// Loop thru the results returned.
while (GenresDataReader.Read())
{
// Add to the list of genres - creates a new row for the collection.
Genre.Add(new Genre(GenresDataReader["Genre"].ToString()));
}
}
catch (Exception ex)
{
errorSw = true;
}
finally
{
if (GenresDataReader != null)
{
GenresDataReader.Close();
}
dbFunc.CloseDB();
}
// Return the list of genre objects.
return Genre;
}
Here's the view:
#model MvcMovie.Models.MovieGenreViewModel
#{
ViewData["Title"] = "Index";
}
<h2>List of Movies</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
#* A dropdown. *#
<select asp-for="movieGenre" asp-items="Model.genres">
<option value="">All</option>
</select>
Title: <input type="text" name="searchStringEntered">
<input type="submit" value="Filter" />
</p>
</form>
#* Shows the list of movies. *#
<table class="table">
<thead>
<tr>
<th>
#Html.DisplayNameFor(model => model.movies[0].Title)
</th>
<th>
#Html.DisplayNameFor(model => model.movies[0].ReleaseDate)
</th>
<th>
#Html.DisplayNameFor(model => model.movies[0].Genre)
</th>
<th>
#Html.DisplayNameFor(model => model.movies[0].Price)
</th>
<th>
#Html.DisplayNameFor(model => model.movies[0].Rating)
</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.movies) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.Title)
</td>
<td>
#Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
#Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
#Html.DisplayFor(modelItem => item.Price)
</td>
<td>
#Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
<a asp-action="Edit" asp-route-id="#item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="#item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="#item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
This tutorial is using Entity Framework but I am converting it to using ADO.net and stored procedures. The EF version of the drop down works fine.
Here is the the EF versions Index action method. The genre is just the text - no Id associated with it.
public async Task<IActionResult> Index(string movieGenre, string searchStringEntered)
{
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchStringEntered))
{
movies = movies.Where(s => s.Title.Contains(searchStringEntered));
}
if (!String.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
// Instantiate the model.
var movieGenreVM = new MovieGenreViewModel();
// The SelectList of genres is created by projecting the distinct genres.
// Sets the models property which is then used in the dropdown of Genres.
movieGenreVM.genres = new SelectList(await genreQuery.Distinct().ToListAsync());
// Creates a List object.
// Movie is populated from the database and used to generate an HTML table of loaded movies.
movieGenreVM.movies = await movies.ToListAsync();
return View(movieGenreVM);
}
From your screenshot, it is clear that your SelectList is missing the DataTextField and DataValueField. So the SELECT tag helper do not know which property should be used for the text and which should be used for Value.
The solution is to specifiy it when you create the SelectList object.Since your Genre entity class has only one property MovieGenre, you can use that for both Text and Value
var generesList = new SelectList(GetGenres(movieGenre),"MovieGenre","MovieGenre");
var vm=new MovieGenreViewModel { genres= generesList};
return View(vm);
Now in your view, you can use Select tag helper with this genres property
#model MovieGenreViewModel
<select asp-for="movieGenre" asp-items="#Model.genres"></select>
The select tag helper will use the DataValueField property value for the option value attribute and DataTextField property value for the option text.
Or Since you only have one property in your enter code hereGenre class, you can pass the list of strings to the SelectList constructor. The below will also work.
var gList=GetGenres(movieGenre);
var generesList = new SelectList(gList.Select(a=>a.MovieGenre));
var vm=new MovieGenreViewModel { genres= generesList};
return View(vm);
Here is a post explaining different options to use the SELECT tag helper, for your reference
Select Tag Helper in ASP.NET Core MVC
This is an example of how I do it:
var items = GetItemsFromDB();
var vals = new List<SelectListItem> {new SelectListItem {Selected = true, Text = "--Select an Item--", Value = "0"}};
vals.AddRange(items.Select(item => new SelectListItem
{
Selected = false, Text = item.Type, Value = item.Id.ToString()
}));
var result = new SelectList(vals, "Value", "Text");
As you can see when i convert it to a SelectList I specify the Value and Text as dataValueField and dataTextField.
In your case you need properties for these values and specify them when you "cast it" using the SelectList constructor.
In my view I do this:
<select asp-for="Model.ItemId" asp-items="Model.MyItemSelectList"></select>
Where Model.MyItemSelectList can be a List of SelectListItem
Another way to bind it to a collection is like this:
<select asp-for="Model.MyItemId">
<option value="">-- Select Item --</option>
#{
foreach (var item in Model.MyItems)
{
<option value="#item.Key">#item.Value</option>
}
}
</select>
Where item has Key and Value properties. The properties can be names whatever you want. In this case "item" has those properties.

Trying to Post a Model back to the Controller, but it creates a new model instead

Tpa class is my base model.
public class Tpa
{
public bool selected { get; set; }
public int Id { get; set; }
}
Data class creates a list of Tpa objects.
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
this.Tpas = new List<Tpa>();
this.Tpas.Add(new Tpa()
{
selected = false ,
Id = 1,
});
this.Tpas.Add(new Tpa()
{
selected = false,
Id = 2,
});
this.Tpas.Add(new Tpa()
{
selected = true,
Id = 3,
});
}
}
This is my Get.
[HttpGet]
public virtual ActionResult Index()
{
var model = new Data();
return View(model);
}
This is my view.
#model TpaUpload_2.Models.Data
#using (Html.BeginForm(MVC.TpaUpload_2.Home.ReceiveID(), FormMethod.Post))
<table class="table">
<tr>
#for (int count = 0; count < Model.Tpas.Count; count++)
{
var item = Model.Tpas[count];
<tr>
<td>
<input type="checkbox"
name=#Html.Raw("'s" + count + "CheckBox'")
id=#Html.Raw("'s" + count + "CheckBox'")
#*checked="#(item.selected == true)"*# />
<label for=#Html.Raw("'s" + count + "CheckBox'" )></label>
<input type='hidden'
id=#Html.Raw("'s" + count + "CheckBox'" )
name='item.selected'
value=#Html.Raw("'"+item.selected+"'")/>
</tr>
</table>
<input type="submit" value="Submit" />
This my Post.
[HttpPost]
public virtual ActionResult ReceiveID(Data myData)
{
...
}
I'm trying to use the checkbox value to change the "selected" on the model, and post back the model.
The problem is after the Form is submitted to the Post, the program will construct a new Data object, instead of using the Data model passed to the controller.
What did I do wrong? Any help will be greatly appreciated.
Your constructing html with name attributes that have absolutely no relationship to you model. When you submit, the DefaultModelBinder first initializes your Data model (which means that 3 new Tpa objects are added to its Tpas property. It then tries to find name/value pairs in the form collection that match you model properties but there are none.
First you need to modify you constructor to include only the initialization of the list, and remove the adding of the new items
public class Data
{
public List<Tpa> Tpas { set; get; }
public Data()
{
Tpas = new List<Tpa>();
}
}
And add the items in the GET method
public virtual ActionResult Index()
{
var model = new Data();
model.Tpas .Add(new Tpa(){ selected = false, Id = 1 });
// add other items
return View(model);
}
Then you need to construct you view correctly using the strongly typed html helpers so that your form controls are correctly named in relationship to your model
<table class="table">
#for (int i = 0; i < Model.Tpas.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.Tpas[i].Id)
#Html.CheckBoxFor(m => m.Tpas[i].selected)
#Html.LabelFor(m => m.Tpas[i].selected)
</td>
</tr>
}
</table>
This give your controls the correct name attribute for model binding, for example
<input type="hidden" name="Tpas[0].Id" ... />
<input type="hidden" name="Tpas[1].Id" ... />
<input type="hidden" name="Tpas[2].Id" ... />
I suggest you compare that with what your currently generating to understand the difference.
Note also your current html is invalid - you have multiple <tr> elements inside a <tr> elements and you need to include the hidden input for the Id property or else this will not post back and you will end up with 3 Tpa objects all with id = 0.

Get Selected Value of Dropdownlist in ActionLink (MVC 5)

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>

How to capture selected row ID - MVC Table

I have table layout in MVC (see below code), on each table row I have a submit button. Each Submit button post to same controller method 'TableSample'. How to capture the selected row id and post it?
public class TableSample
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public int Property3 { get; set; }
public List<Things> Things;
}
#Html.TextBoxFor(m => m.Property1)
#Html.TextBoxFor(m => m.Property2)
#Html.TextBoxFor(m => m.Property3)
<table>
<tbody>
#foreach (var thing in Model.Things)
{
<tr>
<td>#thing.ID</td>
<td>#thing.Name</td>
<td><input type="submit" value="Select" name="Command" /></td>
</tr>
}
</tbody>
</table>
[HttpPost]
public ActionResult TableSample(TableSample sample, string Command)
{
if (Command == "Select")
{
//How to capture selected row ID?
}
if (Command == "Other")
{
}
}
Use javascript to catch the submit button click and place the row id in a hidden field, that way it will be submitted with the rest of the fields.
If the row id will not be part of your model you can simply add a parameter to the action method with the same name as the hidden field.
Let me know if you need more details. I have done basically the same thing in one of my mvc applications.
Basically 3 steps:
1) Add the hidden input. We'll just use straight HTML and not helpers since the field will not be part of the model. Place this somewhere in the form:
<input type="hidden" id="rowId" name="rowId" />
2) Modify the action method signature to include the new parameter (I assume it is an integer but you can change the type accordingly if it is not):
public ActionResult TableSample(TableSample sample, string Command, int rowId)
3) Add the javascript to catch the submit button click and place the row id in the hidden field. I prefer jQuery and I assume you have access to it since it's pretty standard for MVC 4:
$(function () {
$('input[name="command"]').click(function () {
// because there is a command button on each row it is important to
// retrieve the id that is in the same row as the button
$('#rowId').val($(this).parents('tr:first').children('td:first').html());
});
});
It would be a little easy if you noted what you mean by rowID, bacause it is absent in you code. But for all I understand you mean id from first of the row.
In Controller:
[HttpPost]
public ActionResult TableSample(TableSample sample, string Command, int rowid)
{
if (Command == "Select")
{
rowid
}
if (Command == "Other")
{
}
}
In View:
<script>
$('input[name=Command]').click(function(){
var rowID = $(this).closest('tr').find(".rowid").val()
$post('/Home/TableSample?rowid='+rowID+ '&Command=Select')
});
</script>
<table>
<tbody>
#foreach (var thing in Model.Things)
{
<tr>
<td class="rowid">#thing.ID</td>
<td>#thing.Name</td>
<td><input type="submit" value="Select" name="Command" /></td>
</tr>
}
</tbody>
</table>

View Models and Check Boxes in View

I have a newbie question, which I have tried to understand for the past few days. Hopefully someone can be kind enough to help me understand the programming flow.
Assuming I have a model, the information is stored in the database:
public class Student
{
public int studentID { get; set; }
public string studentName { get; set; }
public strin studentGrade {get; set; }
}
public class StudentDBContext : DbContext
{
public DbSet<Student> Students { get; set; }
}
and I want to display it into the view, with additional checkbox so I can select which students to be promoted into the next grade. I read that one way to do it is by putting into view model:
public class StudentViewModel
{
public bool promoted { get; set; }
public Student stu { get; set; }
}
But I am stuck on is this the way to do it? and if yes, how do you put into the view where it will display all the students with a checkbox next to it. Afterwards, I want to update all the grade for the students whose checkboxes are ticked. For example:
Student A, Student B, Student D promoted from Grade 1 to Grade 2. So I want to display the students, tick Student A, B and D and submit to update the Grade.
Step by step example will be much appreciated.
Update 1:
Controller:
[HttpGet]
public ViewResult CheckBox()
{
var studentViewModels = db.Students.Select(m => new StudentViewModel()
{
stu = m
}).ToList();
return View(studentViewModels);
}
[HttpPost]
public ActionResult CheckBox(IList<studentViewModel> list)
{
foreach (var stuUpdate in list.Where(m => m.promoted))
{
var stuRow = db.Students.Find(stuUpdate.stu.studentID);
stuRow.studentName = stuRow.studentName + "1";
db.Entry(stuRow).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("CheckBox");
}
return RedirectToAction("CheckBox");
}
View:
#model IList<School.ViewModels.StudentViewModel>
#using (Html.BeginForm())
{
<table>
<tr>
<th>
</th>
<th>
student ID
</th>
<th>
student name
</th>
<th>
student grade
</th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.CheckBoxFor(modelItem => item.promoted)
#Html.HiddenFor(modelItem => item.stu.studentID)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentID)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentName)
</td>
<td>
#Html.DisplayFor(modelItem => item.stu.studentGrade)
</td>
</tr>
}
</table>
<input type="submit" value="save" />
}
However currently hit by the following error:
Value cannot be null.
Parameter name: source
Source Error:
foreach (var stuUpdate in list.Where(m => m.promoted))
A very basic "step by step" (done in SO, so I probably did a few mistakes, but you've got the idea).
You have always a few ways to do these kind of things, so... just really take it as a sample, and find other examples to get other ideas.
well, first, in your controller, you will have a GET action (to see the list).
[HttpGet]
public ViewResult StudentList() {
//retrieve all students. With the Select, we create a new instance of StudentViewModel for each student.
//assuming StudentDbContext being a property of your controller, if it's not, you can instantiate a new one (in a using clause)
var studentViewModels = StudentDbContext.Students
.Select(m => new StudentViewModel() {
stu = m
//we don't say nothing about promoted :
//it will be there as "false" by default, which is probably what we want.
}).ToList();
//now we've got a list of StudentViewModel. This will be the model of our view
return View(studentViewModels);
}
Then we've got a view, StudentList.cshtml
in this view, we will display a table, with a line for each student : the studentId (hidden in this case), the name (display only), the grade (display only), and a checkbox.
We need a for loop (not a foreach) to get fine model binding.
#model IList<StudentViewModel>
#using (Html.BeginForm()) {
<table>
<tr>
<th>Student name</th>
<th>Student grade</th>
<th>Promote</th>
</tr>
#for (var i = 0; i < Model.Count(); i++) {
<tr>
<td>#Html.HiddenFor(m => Model[i].Student.studentID)
#Html.DisplayFor(m => Model[i].Student.studentName)
</td>
<td>#Html.DisplayFor(m => Model[i]Student.studentGrade)</td>
<td>#Html.CheckBoxFor(m => Model[i].promoted)</td>
</tr>
}
</table>
<input type="submit" value="save" />
}
This form will lead to another POST action (same name as the GET one, depending of what you have in your Html.BeginForm)
[HttpPost]
public ActionResult StudentList(IList<StudentViewModel> list) {
//we treat only the lines where checkbox has been checked
foreach (var studentViewModel in list.Where(m => m.promoted) {
var student = StudentDBContext.GetById(studentViewModel.Student.studentID);//get student entity instance from context
student.studentGrade ="<new value>";
StudentDBContext.SaveChanges();//save changes, you must have such a method somewhere.
}
return Action("StudentList");
}
Little detail :
Try to respect some really basic "usual" practices : for example in c#, Properties should begin by an uppercase letter (so StudentGrade, StudentName, Promoted, etc).

Resources