How to capture selected row ID - MVC Table - asp.net-mvc

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>

Related

How to make a CREATE view & controller method for a model that has a list field?

I have these 2 models:
public class Invoice
{
public string InvoiceID {get; set; }
public List<InvoiceElement> InvoiceElements {get; set;}
[...other fields...]
}
public class InvoiceElement
{
public string InvoiceElementID {get; set; }
[ForeignKey("Invoice")]
public string InvoiceID { get; set; }
public virtual Invoice Invoice { get; set; }
public string Item {get; set;}
[...other fields...]
}
I am unable to make a CREATE view for new Invoices that lets me add InvoiceElements.
I want to have a "CurrentInvoiceElements" table where to dinamically add rows.
Just trying to making it simple. You can use the name attribute (the attribute that asp.net uses for modal binding) and post a list along with other properties of the class. You can use javaScript to append new elements to your form. Using the above modals you've provided, I have written a simple example using simple jQuery functions.
Razor View:
<button class="btn btn-success" id="add_btn">Add Invoice Element</button>
#using (#Html.BeginForm("SaveInvoice", "Invoice", FormMethod.Post))
{
<!--Other modal attributes inputs goes here -->
<!--You can use normal html helper extensions -->
<table id="element_table">
<thead>
<tr>
<td>Element Id</td>
<td>Item</td>
</tr>
</thead>
<tbody>
<tr>
<td><input name="invoice.InvoiceElements[0].Item" id="InvoiceElements[0].Item" /></td>
<td><input name="invoice.InvoiceElements[0].InvoiceElementID" id="InvoiceElements[0].InvoiceElementID" /></td>
</tr>
</tbody>
</table>
<input type="submit" />
}
JavaScript:
<script type="text/javascript">
$("#add_btn").on('click', function (e) {
var table = $("#element_table");
var idx = $(table).find("tbody>tr").length;
var htmlToAppend = `<tr>
<td><input name="invoice.InvoiceElements[${idx}].Item" id="InvoiceElements[${idx}].Item" /></td>
<td><input name="invoice.InvoiceElements[${idx}].InvoiceElementID" id="InvoiceElements[${idx}].InvoiceElementID" /></td>
</tr>`;
$(table).find("tbody").append(htmlToAppend);
});
</script>
Controller / Action:
[HttpPost]
public ActionResult SaveInvoice(Invoice invoice)
{
/* your logic here */
if(ModelState.IsValid)
_invoiceBusiness.SaveInvoice(invoice);
return View();
}
Please make sure the variable name in the parameter of the action method matches the name used in the name attribute of the input tag. i.e. name = "invoice.***" public ActionResult SaveInvoice(Invoice invoice)
I followed this solution: https://github.com/danludwig/BeginCollectionItem some years ago, and worked fine.
If I'm not mistaken, at the time I managed to do it using only the HTML Helper: HtmlPrefixScopeExtensions. Then just make sure the name you give on your View when you do Html.BeginCollectionItem("name") is exactly the same as your collection on your ViewModel.
That's for binding back to the controller.
With this, you can dynamically add rows using AJAX per example.
I hope it's clear enough. If you don't understand it I may make a Github repository with this.

How do I select an Id from a SelectList using asp.net core 3?

I'm iterating through a list and populating a table with the last column having a edit button to edit that Id specific request.
Right now no matter what button i click it always takes me to the edit page of the first Id in the list, also the url has each Id listed in it like this.
/EditRequest?SelectedId=127&SelectedId=128
why is the SelectedId set to all values in the list? and how do I only pass the one Id of the one selected?
Here's my model
public class MyRequestsViewModel
{
public MyRequestsViewModel()
{
this.MyRequests = new List<SelectListItem>();
}
public List<SelectListItem> MyRequests;
public int SelectedId { get; set; }
}
I'm iterating through MyRequests and want to send SelectedId to the controller
<form method="get" asp-controller="Home" asp-action="EditRequest">
<table id="SortRequestsTable" class="table table-striped">
<thead>
<tr>
<th>SortID</th>
<th>SortCriteria</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.MyRequests)
{
<tr>
<td>#item.Value</td>
<td>#item.Text</td>
<td>
<input asp-for="SelectedId" type="hidden" value="#item.Value" />
<button>#item.Value<span class="sap-icon"></span></button>
</td>
</tr>
}
</tbody>
</table>
</form>
And my controller keeps saying that SelectedId is 0
public IActionResult EditRequest(MyRequestsViewModel requests)
I got it working with this. But I don't want to display the Id value in the button.
<input asp-for="SelectedId" type="submit" value="#item.Value" /><span class="sap-icon icon-16"></span>
I've also tried using asp-route-SelectedId tag helper but I'm not entirely sure how to implement that.
Try code below to achieve Edit from Index Page.
<a asp-action="Edit" asp-route-id="#item.ID">Edit</a> |
And Controller for accept request
// GET: Tickets/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var ticket = await _context.Tickets.FindAsync(id);
if (ticket == null)
{
return NotFound();
}
return View(ticket);
}

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.

Modified view-model value in post not showing up in DropDownListFor

I'm trying to implement a button to adding and removing a row using static HTML. I saw this question that seems to be what I want, but I found that the example doesn't work for drop-down lists. None of the drop-down lists' options ever are marked as selected. If I don't clear the model state, all of the old values are kept. How can I keep my changes to the view-model?
// Controller action
[HttpPost]
public virtual ActionResult DoSomething(DoSomethingViewModel viewModel)
{
if (viewModel != null)
{
if (viewModel.ButtonPressed != null)
{
if (viewModel.ButtonPressed.Trim() == "Cancel")
{
return Redirect(ApplicationUtilities.CancelRequestUrl);
}
else if (viewModel.ButtonPressed.Trim() == "AddRow")
{
ModelState.Clear();
// This only covers non-JavaScript users.
// One for the newest one.
viewModel.FieldOneValues.Add(String.Empty);
viewModel.FieldTwoValues.Add(String.Empty);
viewModel.FieldThreeValues.Add(null);
return View(viewModel);
}
else if (viewModel.ButtonPressed.Trim().StartsWith("Remove"))
{
ModelState.Clear();
String[] split = viewModel.ButtonPressed.Split('-');
if (split.Length == 2)
{
Int32 indexToRemove;
Regex regex = new Regex(#"\[([0-9]+)\]");
Match match = regex.Match(split[1]);
if (match.Success && Int32.TryParse(match.Groups[1].Value, out indexToRemove))
{
viewModel.FieldOneValues.RemoveAt(indexToRemove);
viewModel.FieldTwoValues.RemoveAt(indexToRemove);
viewModel.FieldThreeValues.RemoveAt(indexToRemove);
}
}
return View(viewModel);
}
}
}
if (ModelState.IsValid)
{
return WhateverIsDoneOnSuccess(viewModel);
}
else
{
return View(viewModel);
}
}
// View Model
public class DoSomethingViewModel
{
public DoSomethingViewModel()
{
this.FieldOneValues = new List<String>();
this.FieldTwoValues = new List<String>();
this.FieldThreeValues = new List<Int32?>();
}
public virtual IList<String> FieldOneValues { get; set; }
public virtual IList<String> FieldTwoValues { get; set; }
public virtual IList<Int32?> FieldThreeValues { get; set; }
public virtual String ButtonPressed { get; set; }
}
<!-- Spark View -->
<tr each="var fieldOneValue in Model.FieldOneValues">
<td headers="FieldOneTh">${Html.TextAreaFor(m => m.FieldOneValues[fieldOneValueIndex])}</td>
<td headers="FieldTwoTh">${Html.TextAreaFor(m => m.FieldTwoValues[fieldOneValueIndex])}</td>
<td headers="FieldThreeTh">
${Html.TextBoxFor(m => m.fieldOneValueIndex], new { disabled="disabled", #readonly="readonly" })}
${Html.DropDownListFor(
m => m.FieldThreeValues[fieldOneValueIndex]
, ApplicationUtilities.FieldThreeSelectListItems
, " "
)}
</td>
<td headers="AddRemoveTh">
<button name="${Html.NameFor(m => m.ButtonPressed)}" class="Remove" type="submit" value="Remove-[${fieldOneValueIndex}]">Remove</button>
<button if="fieldOneValueIsLast" name="${Html.NameFor(m => m.ButtonPressed)}" class="Add" type="submit" value="AddRow">Add</button>
</td>
</tr>
<!-- HTML Output -->
<tr>
<td headers="FieldOneTh"><textarea cols="20" id="FieldOneValues_0_" name="FieldOneValues[0]" rows="2">
</textarea></td>
<td headers="FieldTwoTh"><textarea cols="20" id="FieldTwoValues_0_" name="FieldTwoValues[0]" rows="2">
</textarea></td>
<td headers="FieldThreeTh">
<input data-val="true" data-val-number="The field Nullable`1 must be a number." disabled="disabled" id="FieldThreeValues_0_" name="FieldThreeValues[0]" readonly="readonly" type="text" value="0" />
<select id="FieldThreeValues_0_" name="FieldThreeValues[0]"><option value=""> </option>
<option value="0">Option 1</option>
<option value="1">Option 2</option>
<option value="2">Option 3option>
</select>
</td>
<td headers="AddRemoveTh">
<button name="ButtonPressed" class="Remove" type="submit" value="Remove-[0]">Remove</button>
<button name="ButtonPressed" class="Add" type="submit" value="AddRow">Add</button>
</td>
</tr>
Plus I'm curious; I think there should be a way to do this.
There is, but you have to handle the post correctly. It's situations like these why PRG (Post-Redirect-Get) is recommended. When you click something like a remove button for a particular item, it's not appropriate to save all the other fields and do whatever else would happen when the whole form is actually submitted. All the user indicated was that they wanted to remove this one item.
Therefore, when you get the post, you remove that item from the database or wherever it's persisted and then you redirect back to the original form if that's what you want. The redirect process updates the page state so that the item is now gone and the rest of the form can then be edited without carrying around stale data. What you're trying to do is remove the item, but then just return the view directly which still has the posted item in the data backing it. That's where your problem is.
I think you went down this path because you're trying to maintain any edits the user made to other areas of the form, but that's simply not going to be possible. However, you do have some options:
Don't actually have a button that removes the item right this minute. Instead, provide a checkbox or something that indicates the item should be deleted when the user posts the entire form. Then you can save the all the form data, remove the indicated items, and redirect afterwards like you should.
Use local storage to save the user's edits on the client-side, and then read them back from local storage after the page loads again, following the redirect. However, this requires JS.
Use AJAX to submit the request to remove the item, and then remove the row from the DOM. However, this requires JS.
Also, remember that it's entirely possible to progressively enhance your form. So, you can implement #1 and #3, and then if JS isn't available, #1 still serves as a fallback.

ASP.NET MVC FormCollection TextArea

I have a textarea that represents a description field. The descriptions have commas so when trying to split the field's descriptions the data is not parsed correctly. How can I get each row's description correctly.
var DescList = FormValues["Item.Description"].Split(',').Select(item => item).ToList<string>();
//will not work for obvious reasons. Comma delimited FormCollection has commas to identify separate row data.
It seems like Microsoft designed the FormsCollection without the textarea control in mind. A text area with commas will not work when trying to access each value. What is interesting is that the _entriestables property has it in the perfect format but they chose to make it a private property. Very frustrating.
`
Here is the important part of my viewmodel.
public class TenantViewModel
{
public Tenant Tenant { get; set; }
public Site Site { get; set; }
}
My view is populated like this:
if (Model != null && Model.Tenant != null && Model.Tenant.Site != null && Model.Tenant.Site.Count() > 0)
{<div class="detailsbox_view">
<table id="tblTenantSites">
<tr>
<th>#Html.LabelFor(item => item.Site.Title)</th>
<th>#Html.LabelFor(item => item.Site.Description)</th>
</tr>
#foreach (var Item in Model.Tenant.Sites)
{
<tr>
#Html.HiddenFor(modelItem => Item.SiteId)
<td>
#Html.EditorFor(modelItem => Item.Title)
</td>
<td>
#Html.TextAreaFor(modelItem => Item.Description, new {#width="400" })
</td>
</tr> }
</table>
As you see this site table is a child of Tenant object. This child record does not get automatically updated using this method but the Tenant data does automatically get updated. This is the reason I tried the FormColelction instead.
Is there something I am missing to make this work?
try with this useful function
ValueProviderResult Match=FormCollection.GetValue("ValueProvider");
When you have multiple fields with the same name attribute, they'll come back into your FormCollection as an array. So upon posting a view like this:
<form action="/Home/MyAction">
<textarea id="row_one_description" name="description">
First row's description
</textarea>
<textarea id="row_two_description" name="description">
Second row's description
</textarea>
<input type="submit" value="Submit" />
</form>
you could do something like this in your action
[HttpPost]
public ActionResult MyAction(FormCollection collection)
{
var descriptionArray = collection["description"];
string firstRowDescription = descriptionArray[0];
string secondRowDescription = descriptionArray[1];
}
I must note that this is not the recommended way of dealing with posted data. You should instead be building your view using data from a view model and using strongly typed html helpers to render your controls. That way when you post, your action can take the ViewModel as a parameter. Its properties will be automatically bound and you will have a nice object to play with.
[HttpPost]
public ActionResult MyAction(MyViewModel viewModel)
{
foreach (var row in viewModel.Rows)
{
string description = row.Description;
}
}
EDIT
I'm still assuming a lot about your ViewModel but perhaps try this:
<table id="tblTenantSites">
<tr>
<th>#Html.LabelFor(model => model.Site.Title)</th>
<th>#Html.LabelFor(model => model.Site.Description)</th>
</tr>
#for (var i = i < Model.Tenants.Sites.Count(); i++) {
<tr>
#Html.HiddenFor(model => model.Tenants.Sites[i].SiteId)
<td>
#Html.EditorFor(model => model.Tenants.Sites[i].Title)
</td>
<td>
#Html.TextAreaFor(model => model.Tenants.Sites[i].Description, new { #width="400" } )
</td>
</tr>
}
</table>
You could also try ,
string Match=FormCollection.GetValue("ValueProvider").AttemptedValue;

Resources