I want to do something similar to what is explained in this question: I want to edit a list of data.
The difference is that the base is not a list.
(I'm using VS 2013, so it is not old stuff.)
My view model:
public class SampleViewModel
{
// ... other properties for editing ...
// The list property
public List<SampleListItemViewModel> ItemList { get; set; }
}
public class SampleListItemViewModel
{
// For display only. It has an ID field to identify the row.
public MyEntity Item { get; set; }
// I want to modify this!
public bool IsChecked { get; set; }
}
My attempted View:
#model My.Namespace.SampleViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<!-- ... normal editing, MVC generated ... -->
<!-- BEGIN LIST EDIT -->
<table>
<tr>
<th>
Is Applicable
</th>
<!-- ... -->
</tr>
#foreach (var doc in Model.ItemList)
{
#Html.HiddenFor(modelItem => doc.Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => doc.IsChecked)
<!-- #Html.ValidationMessageFor(modelItem => doc.IsChecked) -->
</td>
<!-- ... other non-editable display fields, e.g. name ... -->
</tr>
}
<!-- END LIST EDIT -->
<!-- ... -->
}
When I create the view it shows everything as I want it, but when I click "Create" the item list is null.
EDIT - More Info
Controller:
public ActionResult Create(int? id)
{
var item = // ...populate...
// I confirmed that ItemList has values.
return View(item);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SampleViewModel item)
{
// This is null
var list = item.ItemList;
// ignoring all else for now
return View(item);
}
To bind complex objects, we need to provide an index for each item, rather than relying on the order of items. This ensures we can unambiguously match up the submitted properties with the correct object.
Replace the foreach loop with for loop:
#for (int i=0; i<Model.ItemList.Count; i++)
{
#Html.HiddenFor(modelItem => modelItem.ItemList[i].Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => modelItem.ItemList[i].IsChecked)
#Html.ValidationMessageFor(modelItem => modelItem.ItemList[i].IsChecked)
</td>
</tr>
}
NOTE:
Note that the index must be an unbroken sequence of integers starting at 0 and increasing by 1 for each element
Also for every Property in the list which you don't want user to edit like for example UserId etc add an #Html.HiddenFor(..) for that property otherwise it will posted null to the server and if hidden field created for them they will not be NULL.
For further details you can see HERE
Also refer Model Binding with List
Related
I am trying to bind the HTML table using the View Model of type Object on the Razor Page. Code is as below :
index.cshtml.cs
[BindProperty]
public List<object> TableData { get; set; }
public class Cainfo
{
public string Ca_id { get; set; }
public object Nca { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
List<object> tablerows = GetTableRows(tabledata);
TableData = tablerows;
}
public List<object> GetTableRows(GetTableRowsResponse getTableRowsResponse)
{
List<object> tableRows = new List<object>();
var tables = getTableRowsResponse.rows;
foreach (var table in tables)
{
var tab = JsonConvert.SerializeObject(table);
var row = JsonConvert.DeserializeObject<Cainfo>(tab);
tableRows.Add(row);
}
return tableRows;
}
index.cshtml
<table class="resultTable">
<thead class="grid-header">
<tr>
#foreach (var property in #Model.TableData.GetType().GetGenericArguments()[0].GetProperties())
{
<th class="col-lg-1">#property.Name</th>
}
</tr>
</thead>
<tbody class="grid-body">
#if (#Model.TableData != null)
{
#if ((#Model.TableData.Count != 0))
{
#foreach (var row in Model.TableData)
{
<tr>
#foreach (var property in #row.GetType().GetProperties())
{
<td class="col-lg-1">#property.GetValue(#row)</td>
}
</tr>
}
}
}
</tbody>
</table>
var tables = getTableRowsResponse.rows; return the JSON data. Problem is that table <th> is not getting bind. #Model.TableData.GetType().GetGenericArguments()[0].GetProperties() is getting empty. <td> is getting bind as expected. Maybe I am doing a mistake somewhere, I am new to the asp .net core. Now, I am using the Model Cainfo but in future, I need to set different models according to data and bind the same table. That's why I am giving View Model type as Object. Please help.
I would not use reflection like this when you can design common models for the view. That's the art of designing which makes things easier. However here assume that you want to stick with that solution anyway, I'll show where it's wrong and how to fix it and how it's limited.
First this Model.TableData.GetType().GetGenericArguments()[0] will return Type of object. And that Type of course contains no properties. That's why you get no <th> rendered. The generic argument type exactly reflects what's declared for Model.TableData which is a List<object>.
Now to fix it, assume that all the items in the List<object> are of the same type, you can get the first item's Type, like this:
#foreach (var property in #Model.TableData?.FirstOrDefault()
?.GetType()?.GetProperties()
?? Enumerable.Empty<PropertyInfo>())
{
<th class="col-lg-1">#property.Name</th>
}
That has a limit in case the Model.TableData contains no item. No <th> will be rendered. If that's acceptable (instead of rendering an empty table with headers, you will render nothing or just some message) then just go that way. Otherwise, you need to provide a Type for the element/row's Typevia your model, such as via a property like Model.RowType. Then you can use that instead of this:
Model.TableData?.FirstOrDefault()?.GetType()
The remaining code is just the same.
I have a view with a table that displays my model items. I've extracted the relevant portions of my view:
#model System.Collections.Generic.IEnumerable<Provision>
#using (Html.BeginForm("SaveAndSend", "Provision", FormMethod.Post))
{
if (Model != null && Model.Any())
{
<table class="table table-striped table-hover table-bordered table-condensed">
<tr>
...
// other column headers
...
<th>
#Html.DisplayNameFor(model => model.IncludeProvision)
</th>
...
// other column headers
...
</tr>
#foreach (var item in Model)
{
<tr>
...
// other columns
...
<td>
#Html.CheckBoxFor(modelItem => item.IncludeProvision)
</td>
...
// other columns
...
</tr>
}
</table>
<button id="save" class="btn btn-success" type="submit">Save + Send</button>
}
...
}
This works fine and the checkbox values are displayed correctly in the view depending on the boolean value of the IncludeProvision field for the given model item.
As per Andrew Orlov's answer, I've modified the view and controller and the SaveAndSend() controller method is now:
[HttpPost]
public ActionResult SaveAndSend(List<Provision> provisions)
{
if (ModelState.IsValid)
{
// perform all the save and send functions
_provisionHelper.SaveAndSend(provisions);
}
return RedirectToAction("Index");
}
However, at this point the passed in model object is null.
Including the Provision model object for completeness:
namespace
{
public partial class Provision
{
...
// other fields
...
public bool IncludeProvision { get; set; }
}
}
My question is, what is the best way to grab the checked/unchecked value from each checkbox and update the session IncludeProvision field for each model item when the 'SaveAndSend' button is clicked?
You cannot use a foreach loop to generate form controls for properties in a collection. It creates duplicate name attributes (in your case name="item.IncludeProvision") which have no relationship to your model and duplicate id attributes which is invalid html. Use either a for loop (you models needs to be IList<Provision>
for(int i = 0; i < Model.Count; i++)
{
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m[i].IncludeProvision)<td>
</tr>
}
or create an EditorTemplate for typeof Provision. In /Views/Shared/EditorTemplates/Provision.cshtml (note the name of the template must match the name of the type)
#model Provision
<tr>
<td>....</td>
<td>#Html.CheckBoxFor(m => m.IncludeProvision)<td>
</tr>
and in the main view (the model can be IEnumerable<Provision>)
<table>
#Html.EditorFor(m => m)
</table>
As #mattytommo said in comments, you should post your model to controller. It can be done with putting your checkbox inside a form. After clicking on button "Save and exit" all data from inputs inside this form will be serialized and sent to your controller where you can perform manipulations with session variables and so on. After that you can redirect wherever you like.
Model
public class YourModel
{
...
public bool IncludeProvision { get; set; }
...
}
View
#model YourModel
...
#using (Html.BeginForm("SaveAndSend", "Test", FormMethod.Post))
{
...
#Html.CheckBoxFor(model => model.IncludeProvision)
...
<button type="submit">Save and send</button>
}
...
Controller
public class TestController : Controller
{
...
[HttpPost]
public ActionResult SaveAndSend(YourModel model)
{
if (ModelState.IsValid)
{
// Some magic with your data
return RedirectToAction(...);
}
return View(model); // As an example
}
...
}
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
Here is my model:
public string CustomerNumber { get; set; }
public string ShipMethod { get; set; }
public string ContactPerson { get; set; }
public string ShipToName { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3{ get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
Here part of my view:
<table class="table table-condensed">
<thead>
<tr>
<td>Customer Number</td>
<td>Ship Method</td>
<td>Contact Person</td>
<td>Ship to Name</td>
<td>Address 1</td>
<td>Address 2</td>
<td>Address 3</td>
<td>City</td>
<td>State</td>
<td>Zip</td>
</tr>
</thead>
<tbody>
<tr>
<td>#Html.LabelFor(x => x.CustomerNumber, Model.CustomerNumber)</td>
<td>#Html.LabelFor(x => x.ShipMethod, Model.ShipMethod)</td>
<td>#Html.LabelFor(x => x.ContactPerson, Model.ContactPerson)</td>
<td>#Html.LabelFor(x => x.ShipToName, Model.ShipToName)</td>
<td>#Html.LabelFor(x => x.Address1, Model.Address1)</td>
<td>#Html.LabelFor(x => x.Address2, Model.Address2)</td>
<td>#Html.LabelFor(x => x.Address3, Model.Address3)</td>
<td>#Html.LabelFor(x => x.City, Model.City)</td>
<td>#Html.LabelFor(x => x.State, Model.State)</td>
<td>#Html.LabelFor(x => x.ZipCode, Model.ZipCode)</td>
</tr>
</tbody>
</table>
The data gets displayed in the view correctly, but when a post happens on my page, the data comes back null. I need the data in those LabelFors to be sent back to my controller, so I dont have to store it in a session. I thought MVC was supposed to bind to your model automagically with the labelfors. What am I doing wrong?
EDIT
I guess the reason I asked this question, is because I just moved from webforms to MVC, and I am pretty lost without the viewstate. I figured if the values in my model kept posting back to me from the view, I wouldn't have to store my model in a session object. On my page, I need to be able to persist my model during page cycles, so I can store my model data into some sql tables after the user clicks the save button. What are some options to persist your model in MVC?
It only binds input elements inside a form (because the browser posts these). Label's aren't POST'ed.
You can use HiddenFor.. since they are input elements:
#Html.HiddenFor(x => x.City)
..etc. You just can't use labels for sending back to the Controller.
LabelFor:
<label></label> <!-- Not POST'ed by the browser -->
HiddenFor:
<input type="hidden" /> <!-- POST'ed by the browser -->
MVC will work according to HTML standards which means it won't postback a label element.
Use a HiddenFor or TextboxFor with a readonly attribute.
But if you just display the values, why not putting them in a session before sending it to the page?
If you have like a wizard style form where you need to save changes inbetween steps before committing the data to a database for example your best bet would be to save the submitted values in a session which you read out again in the next step in the controller. Otherwise you can put hidden inputs on your form with the values but that is prone to manipulation by the user.
Like Simon and others have explained labels are not posted. A post request should be used when you want to change something. You don't need to store it in session if you're just viewing and editing your model.
In your view, you'll need a link to edit your model:
#Html.ActionLink("edit", "Edit", new {id = Model.CustomerNumber});
In your controller implement your Edit action method:
public ViewResult Edit(int customerNumber) {
var customer = _repository.Customers.FirstOrDefault(c => c.CustomerNumber == customerNumber);
return View(customer);
}
You'll need a view for the Edit action method and in it goes your form to post your update.
#using (Html.BeginForm()) {
<label>Contact Person:</label>
<input name="ContactPerson" value="#Model.ContactPerson" />
// the rest of your form
<input type="submit" value="Save" />
}
Implement the method to handle the post. Make sure to add the HttpPost attribute.
[HttpPost]
public ViewResult Edit(Customer customer) {
if (ModelState.IsValid) {
// Save your customer
return RedirectToAction("Index");
}
else {
return View(customer);
}
}
Hope this helps.
The base functionality I wish to achive is that the contents of a table are updated when a dropdownlist item is selected. This will update when the user makes a new selection and retrieve new information from the database and repopulate the table.
It's also worth noting that the DropDownListFor that I want the .change() to work with is not contained within the AjaxForm but appears elsewhere on the page (admittedly in another form)
To achieve this I looked at this question: Rendering partial view dynamically in ASP.Net MVC3 Razor using Ajax call to Action which does a good job of going part the way of what I want to do.
So far, I have a controller method which handles populating a customized viewmodel for the partial view:
[HttpPost]
public ActionResult CompanyBillingBandDetails(int id = 0)
{
var viewModel = new BillingGroupDetailsViewModel();
var billingGroupBillingBands =
_model.GetAllRecordsWhere<BillingGroupBillingBand>(x => x.BillingGroupId == id).ToList();
foreach (var band in billingGroupBillingBands)
{
viewModel.BillingBands.Add(band.BillingBand);
}
return PartialView("BillingGroupDetailsPartial", viewModel);
}
The ViewModel I wish to populate each call:
public class BillingGroupDetailsViewModel
{
public List<BillingBand> BillingBands { get; set; }
}
The strongly typed model I'm using as a model for the partial view
public class BillingBandsObject
{
public int BillingBandId { get; set; }
public int RangeFrom { get; set; }
public int RangeTo { get; set; }
public Decimal Charge { get; set; }
public int BillingTypeId { get; set; }
public bool Delete { get; set; }
}
The partial view it populates and returns:
#model xxx.xxx.DTO.Objects.BillingBandsObject
<tr>
<td>
#Html.DisplayTextFor(x => x.RangeFrom)
</td>
</tr>
<tr>
<td>
#Html.DisplayTextFor(x => x.RangeTo)
</td>
</tr>
<tr>
<td>
#Html.DisplayTextFor(x => x.Charge)
</td>
</tr>
The Razor code for this section of the page:
<table>
<thead>
<tr>
<th>
Range From
</th>
<th>
Range To
</th>
<th>
Charge
</th>
</tr>
</thead>
<tbody>
#using (Ajax.BeginForm("CompanyBillingBandDetails", new AjaxOptions() { UpdateTargetId = "details", id = "ajaxform" }))
{
<div id="details">
#Html.Action("CompanyBillingBandDetails", new { id = 1 })
</div>
}
</tbody>
</table>
and finally the function I lifted almost straight from Darin's answer:
$(function () {
$('#billinggrouplist').change(function () {
// This event will be triggered when the dropdown list selection changes
// We start by fetching the form element. Note that if you have
// multiple forms on the page it would be better to provide it
// an unique id in the Ajax.BeginForm helper and then use id selector:
var form = $('#ajaxform');
// finally we send the AJAX request:
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
success: function (result) {
// The AJAX request succeeded and the result variable
// will contain the partial HTML returned by the action
// we inject it into the div:
$('#details').html(result);
}
});
});
});
At the moment I have fought through a number of errors, currently I am faced with :
"Error executing child request for handler 'System.Web.Mvc.HttpHandlerUtil+ServerExecuteHttpHandlerAsyncWrapper'."
However, i feel my understanding of the problem as a whole may be lacking.
Any help appreciated!
This error means that there was an exception while rendering your child view. Probably something related to your data, ie. NulLReferenceException.
Just attach your debugger and set to to break when an exception is thrown.