I have a model bind to view. I would like to add a checkbox which allows user to change select and submit the selected items for another process. User also can change the value of NumberOfCopies if needed.
I am passing the ManufacturingJobEditModel to the controller. I can see all the items in the PrintErrors collection in the controller. However, I have 2 problems here
ManufacturingJob always NULL in ManufacturingJobEditModel in the controller
Only IsSelected and NumberOfCopies have values. The rest of the properties show NULL values.
Is that anything that I am missing here?
Model
public class ManufacturingJobProductEditModel
{
public ManufacturingJob ManufacturingJob{ get; set;}
public IList<PrintError> PrintErrors { get; set; }
}
public class PrintError
{
public bool IsSelected { get; set; }
public int ProductId { get; set; }
public string ISBN { get; set; }
public string ProductName { get; set; }
public int Sequence { get; set; }
public int NumberofCopies { get; set; }
}
MainView
<table>
<tr>
<td class="display-label valign-top">Products</td>
<td class="display-field white-space-reset"
colspan="3">
<table class="formDisplayTable">
<colgroup>
<col class="width05" />
<col class="width10" />
<col class="width10" />
<col class="width35" />
<col class="width05" />
<col class="width20" />
</colgroup>
<thead>
<tr>
<th></th>
<th>ISBN</th>
<th>Product ID</th>
<th>ProductName</th>
<th>Sequence Number</th>
<th>No of Copies</th>
</tr>
</thead>
<tbody>#foreach (var product in Model.ManufacturingJob.ManufacturingJobProducts.OrderBy(c => c.Sequence))
{
Html.RenderPartial("_PrintErrorDetails", product);
}</tbody>
</table>
</td>
</tr>
</table>
_PrintErrorDetails.cshtml
#model Bolinda.Matrix.Data.Domain.ManufacturingJobProduct
#{Html.RegisterFormContextForValidation();}
<tr class="valign-top">
#using (Html.BeginCollectionItem("PrintErrors"))
{
<td>
<div class="editor-field">#Html.CheckBox("IsSelected")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ISBN")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ManufacturingProduct.Product.ProductId")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ManufacturingProduct.Product.Name")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("Sequence")</div>
</td>
<td>
<div class="table-editor-field">#Html.Editor("NumberOfCopies")</div>
</td>
}
</tr>
Controller
[HttpPost]
public ActionResult PrintError(ManufacturingJobProductEditModel editModel)
{
var id = editModel.ManufacturingJob.ManufacturingJobId;
ManufacturingJob manufacturingJob = _unitOfWork.ManufacturingJob
.GetWhere(j => j.ManufacturingJobId == id, null, "ManufacturingJobProducts")
.FirstOrDefault();
if (manufacturingJob == null)
{
return new HttpNotFoundResult(String.Format("Manufacturing Job with id {0} was not found.", id));
}
// _service.RequeueErrorCorrection(manufacturingJob, printErrorCorrection, autoCdErrorCorrection, manualCdErrorCorrectionSequenceNumbers);
return RedirectToAction("Details", new { id = manufacturingJob.ManufacturingJobId });
}
ManufacturingJob always NULL in ManufacturingJobEditModel in the controller
The view you have shown does not generate any form controls for any properties so no values are posted back and bound to your model. From the code in your POST method, you appear to only need the ManufacturingJobId property so you need to include
#Html.HiddenFor(m => m.ManufacturingJob.ManufacturingJobId)
Only IsSelected and NumberOfCopies have values. The rest of the properties show NULL values
Again, you have not included form controls for any properties other than the IsSelected and NumberOfCopies of each PrintError object in the collection. If you want the other properties to be bound, use
<td>
<div class="table-display-field">#Html.Display("ISBN")</div>
#Html.HiddenFor(m => m.ISBN)
</td>
or
<td>
<div class="table-display-field">#Html.TextboxFor(m => m.ISBN, new { #readonly = "readonly" })</div>
</td>
Side note: Since you are not dynamically adding or deleting PrintError items in the view, there is no need to use the extra overhead of BeginCollectionItem(). Either use a for loop or a custom EditorTemplate for type of PrintError and in the main view use #Html.EditorFor(m => m.PrintErrors) (refer this answer for an example of using an EditorTemplate). I would also recommend that you populate your models PrintError collection on the server before you pass it to the view (including the .Order() clause) rather that trying to 'fake' it as you are doing.
This is because you are not rendering html input controls for the rest of the model properties other than "IsSelected" and "NumberOfCopies".
"#Html. Display" just render data without any html input control. You can check using page view source.
To render these control you can use below html helper methods. #Html. TextBox, #Html. DropDown, #Html. TextArea and others.
To submit all properties that you required for further processing, you must need to render html input control corresponding to that property. Only then you can able to submit those properties.
Please let me know if problem still persist.
Related
I'm having a challenge updating from Razor View table and not sure if my approach is right. Instead of displaying the data (to be edited) in DIV, I'm displaying in a TABLE. I can understand how the hidden property is used to update the row. In my case I am displaying multiple rows on the Edit page, but unable to update the database for only rows that have updates.
My view page is like this
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<table class="table">
<thead>
<tr>
<th> #Html.DisplayNameFor(model => model.Fields[0].Name) </th>
<th> #Html.DisplayNameFor(model => model.Fields[0].Value) </th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Fields)
{
<tr>
<td> <input type="hidden" asp-for=#item.ID /> </td>
<td> #Html.DisplayFor(modelItem => item.Name) </td>
<td contenteditable='true'> #Html.DisplayFor(modelItem => item.Value) </td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
And the code behind is this. The Fields is showing 0 in the OnPostAsync method.
[BindProperty]
public IList<FieldInfo> Fields { get; set; }
public async Task<IActionResult> OnGetAsync(string id)
{
Fields = await _context.FieldInfo.Where(m => m.GUID == id).ToListAsync();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
foreach (var p in Fields) // In this approach Fields shows as count=0
{
_context.Attach(p.Value).State = EntityState.Modified;
}
_context.Attach(Fields).State = EntityState.Modified; // In this approach Exception: The entity type 'List<FieldInfo>' was not found.
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If you want to work with a List or collection, you must use an index to identify individual elements. https://www.learnrazorpages.com/razor-pages/model-binding#binding-complex-collections
In your case, I would use an explicit index:
#foreach (var item in Model.Fields)
{
<tr>
<td> <input type="hidden" asp-for="Fields[item.ID].ID" /> <input type="hidden" name="Fields.Index" value="Fields[item.ID].ID" /></td>
<td> #item.Name</td>
<td><textarea asp-for="Fields[item.ID].Value">#Fields[item.ID].Value">#Fields[item.ID].Value</textarea> </td>
</tr>
}
Note that the value assigned to a td designated as contenteditable is not posted as part of a form. You should use a suitable form control like an input or textarea instead.
I got it working after changing the type as below, from IList to List
[BindProperty]
public List<FieldInfo> Fields { get; set; }
I am using Atata Framework and working on the following scenario—There's a table with checkbox within TD element. I want to be able to invoke Click() method on the checkbox, but couldn't get it work correctly.
The truncated HTML is as following:
<table data-v-c4547572="" class="invGrid">
<tr data-v-c4547572="" row-id="3ed5bcf4-473d-43ae-991a-ffe36d5e0a53" class="row-index-0">
<td data-v-c4547572="" class="column-index-0 checkbox-col">
<input data-v-c4547572="" type="checkbox" element-id="3ed5bcf4-473d-43ae-991a-ffe36d5e0a53" class="">
<label data-v-c4547572="" for="3ed5bcf4-473d-43ae-991a-ffe36d5e0a53"></label>
</td>
<td data-v-c4547572="" class="column-index-1">
<span data-v-c4547572="" class="val-name">Some text</span>
<span data-v-c4547572="" class="arrow pull-right dsc"></span>
</td>
</tr>
</tbody>
</table>
The code I'm using is:
// The page class:
[FindByCss(".invGrid")]
public Table<GroupsRow, Page> Inventory { get; set; }
// The row class:
public class GroupsRow : TableRow<Page>
{
[FindByIndex(0)]
public CheckBox<Page> CheckBox { get; set; }
[FindByCss(".val-name")]
public Text<Page> Text { get; set; }
}
As an additional note, invoking Exists() on the checkbox yields false:
inv.CheckBox.Exists(); // false
Any idea how to make the checkbox to be operational?
I can guess that your check box is actually hidden, and <label> is used as a wrapper for custom rendering. As almost all controls in Atata are looking for visible elements by default, you can specify Visibility:
[FindByIndex(0, Visibility = Visibility.Any)]
public CheckBox<Page> CheckBox { get; private set; }
It should find the check box. But if click on it will not work (as it can be hidden), you can add a property for label and click it:
[FindFirst]
public Label<Page> CheckBoxLabel { get; private set; }
How do I get the entered values of textboxes to post to my controller with values. Thought of using arrays but have no idea where to start. Textboxes are dynamic due to each have counter number
#{
var counter = 0;
foreach (var addedProduct in Model)
{
counter++;
<tr class="default" data-ng-controller="deleteItemsController">
<td>#addedProduct.name</td>
<td>#addedProduct.price</td>
<td>
<input type="text" id="quantity_#counter" name="quantity_#counter"/>
</td>
</tr>
}
}
<tr>
<td><b>Total:</b>{{#ViewBag.total}}</td>
<td>
#using (Html.BeginForm("Index", "Payment"))
{
//how do i get quantity values//
<input type="submit" value="pay">
}
</td>
my model:
public class ProductVar
{
public int productID { get; set; }
public string name { get; set; }
public int price { get; set; }
public int quantity { get; set; }
public int total { get; set; }
}
I have not implemented method yet, trying to figure out how to post all values first.
You are pretty close. Read this blog post from Phil Haack and things will be clear how your input fields should be named. You will understand how model binding works in ASP.NET MVC as well.
Also you should place your <input> fields inside the HTML <form> or nothing will ever be sent to the server when this form gets submitted.
So start by modifying your view code so that you use strongly typed HTML helpers for generating your input fields. They will take car of properly naming them:
#using (Html.BeginForm("Index", "Payment"))
{
for (var i = 0; i < Model.Count; i++)
{
<tr class="default" data-ng-controller="deleteItemsController">
<td>#Html.DisplayFor(x => x[i].name)</td>
<td>#Html.DisplayFor(x => x[i].price)</td>
<td>
#Html.EditorFor(x => x[i].quantity)
</td>
</tr>
}
<tr>
<td>
<b>Total:</b>
#ViewBag.total
</td>
<td>
<input type="submit" value="pay" />
</td>
</tr>
}
Now you can have your controller action that will be triggered when the form is submitted:
[HttpPost]
public ActionResult Index(ProductVar[] model)
{
...
}
In this example only the quantity field of the model will be sent to the server because that's the only input field present inside the HTML form and so the only values sent to the server. If you want to bind the other values you might include them as hidden fields as well:
#Html.HiddenFor(x => x[i].name)
#Html.HiddenFor(x => x[i].price)
Of course in most cases this is bad practice because the user can manipulate those values. The best approach is to retrieve them from the db in your POST action using the same id that you used to retrieve them in the GET action that you used to render this form.
I have got this problem that I am having a difficulty to solve. I am creating a page where the user will be presented with a list of items (Product Types). Each item will have a dropdown list next to it so that the user can make appropriate selection to create a mapping. After making selection then the user submits the form, and the value will be written to the database.
The problem is that when it is submitted, I am not getting any values back. Specifically, 'Mappings' is empty in the model that is returned by the POST action. The GET action works fine. The following is the essence of what I have written:
Model:
public class ProductTypeMappingViewModel
{
//this is empty in the POST object
public List<ProductTypeMapping> Mappings { get; set; }
public ProductTypeMappingViewModel()
{
Mappings = new List<ProductTypeMapping>();
}
public ProductTypeMappingViewModel(string db)
{
//use this to populate 'Mappings' for GET action
//works fine
}
public void UpdateDB()
{
//to be called on the object
//returned from POST action
foreach(var mapping in Mappings)
{
//Mappings is always empty after POST
//Suppose to add to db
}
}
}
public class ProductTypeMapping
{
public string ProductTypeName { get; set; }
public int SelectedStandardProductTypeKey { get; set; }
public SelectList StandardProductTypes { get; set; }
public ProductTypeMapping()
{
StandardProductTypes = new SelectList(new List<SelectListItem>());
}
public int GetSelectedProductTypeKey() { //return selected key}
public string GetSelectedProductTypeName() { //return selected name}
}
View:
#model CorporateM10.Models.ProductTypeMappingViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true)
<table class="table">
#foreach (var dept in Model.Mappings)
{
<tr>
<td>
#Html.DisplayFor(model => dept.ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
<div>
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
}
Any insight will be greatly appreciated.
foreach here causes select element in final HTML to have incorrect name attribute. Thus nothing is posted to the server. Replace this with for loop:
<table class="table">
#for (int i=0; i<Model.Mappings.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(model => model.Mappings[i].ProductTypeName, new { })
</td>
<td>
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey, model.Mappings[i].StandardProductTypes, "(Select Department)", new { })
</td>
</tr>
}
</table>
As #Andrei said the problem relies on the name attribute.
But to add a little bit to his answer, here's the parameter names in the request that the default model binder expects for your case.
Mappings[0].SelectedStandardProductTypeKey
Mappings[1].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
...
Without any breaks in the numbering, i.e.:
Mappings[0].SelectedStandardProductTypeKey
Mappings[2].SelectedStandardProductTypeKey
Won't work because of the missing Mapping[1]...
When you use the dropdown helper like this:
#Html.DropDownListFor(model => dept.SelectedStandardProductTypeKey, dept.StandardProductTypes, "(Select Department)", new { })
It generates an input with name="SelectedStandardProductTypeKey" (you need it to be Mappings[0].SelectedStandardProductTypeKey)
If you use a for loop and use the dropdown helper like this:
#Html.DropDownListFor(model => model.Mappings[i].SelectedStandardProductTypeKey
You'll get the input with the correct name.
Any parameter in the request for which the model binder cannot find a property in the model, it will ignore, that's why the Mappings property is null in your case.
Here are two great resource that explain all this (and that provide alternative ways to represent collections that might be useful if you can't a the for loop to generate a numbered index without breaks):
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
I'm writing a view that displays a list of managers. The managers have checkboxes next to their name to select them to be removed from the manager list. I am having problems binding the form submission back to my view model. Here's what the page looks like:
Here's the ViewModel for the page.
public class AddListManagersViewModel
{
public List<DeleteableManagerViewModel> CurrentManagers;
}
And here's the sub-ViewModel for each of the DeleteableManagers:
public class DeleteableManagerViewModel
{
public string ExtId { get; set; }
public string DisplayName { get; set; }
public bool ToBeDeleted { get; set; }
}
This is the code for the main View:
#model MyApp.UI.ViewModels.Admin.AddListManagersViewModel
<div class="row">
<div class="span7">
#using (Html.BeginForm("RemoveManagers","Admin"))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>System Managers</legend>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.CurrentManagers)
</tbody>
</table>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Delete Selected</button>
</div>
}
</div>
</div>
And this is the EditorTemplate I've created for DeleteableManagerViewModel:
#model MyApp.UI.ViewModels.Admin.DeleteableManagerViewModel
<tr>
<td>#Html.DisplayFor(model => model.DisplayName)</td>
<td>
#Html.CheckBoxFor(model => model.ToBeDeleted)
#Html.HiddenFor(model => model.ExtId)
</td>
</tr>
But when I submit the form to the controller the model comes back null! this is what I want it to do:
[HttpPost]
public virtual RedirectToRouteResult RemoveManagers(AddListManagersViewModel model)
{
foreach (var man in model.CurrentManagers)
{
if (man.ToBeDeleted)
{
db.Delete(man.ExtId);
}
}
return RedirectToAction("AddListManagers");
}
I tried following along this post: CheckBoxList multiple selections: difficulty in model bind back but I must be missing something....
Thanks for your help!
Hmm. I think this is ultimately the problem; here's what you're posing:
CurrentManagers[0].ToBeDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].ExtId=X00405982144
Your model is an AddListManagersViewModel that has a collection of CurrentManagers. So, you're posting an array of DeleteableManagerViewModel, which isn't getting bound to the "wrapper" model. You can try changing the model parameter to
params DeleteableManagerViewModel[] model
I don't ever use the EditorFor extensions, though, so I'm just guessing...