How to render object collection as editable(checkboxes) list view - asp.net-mvc

First, I'd like to say that I did a lot of researches, tried many ways but none of it worked. I'd like to avoid:
writing my own model binder
packing form into json
reading directly from FormCollection object
My model class looks like that:
public class ListViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsChecked { get; set; }
}
I'm passing it to the view as a IEnumerable collection.
I'm trying to have all the form data packed into IEnumerable, like here
[HttpPost]
[Authorize(Roles = "User")]
public ActionResult EditVisitLists(List<ListViewModel> model)
{
//...
}
Unfortunately, my every attemp fails, I'm receiving null as a model (probably model binder doesn't recognize the form the way I'd like it to)
Here is my latest attemp:
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(m => item.Name)
</td>
<td>
#Html.CheckBoxFor(m => item.IsChecked, new { id = "[" + item.Id + "].Name" })
#Html.HiddenFor(m => item.Name)
</td>
</tr>
}

Just use a for loop instead of a foreach to get correct binding
#for (var i = 0; i < Model.Count; i++) {
#Html.Displayfor(m => Model[i].Name
#Html.CheckBoxFor(m => Model[i].IsChecked)
#Html.HiddenFor(m => Model[i].Name)
}
Look at the html generated, you will now have different name and id attributes, which should bind fine.

Related

MVC using IEnumerable<Model> with Razor is null after post to controller

My Model
public class ActivityModel
{
public int Id { get; set; }
public String Nick { get; set; }
public String TripName { get; set; }
[DisplayFormat(DataFormatString = "{0:hh\\:mm}", ApplyFormatInEditMode = true)]
public TimeSpan? FromTime { get; set; }
[DisplayFormat(DataFormatString = "{0:hh\\:mm}", ApplyFormatInEditMode = true)]
public TimeSpan? ToTime { get; set; }
public String FromPlace { get; set; }
public String ToPlace { get; set; }
public String activityType { get; set; }
[DisplayFormat(DataFormatString = "{0:dd.MM.yyyy}", ApplyFormatInEditMode = true)]
public DateTime? Timestamp { get; set; }
public String Weather { get; set; }
public int Difficulty { get; set; }
public bool Reviewed { get; set; }
}
My View row looks like this
#model IList<ActivityModel>
#for (int i = 0; i < Model.Count; i++)
{
if (!(bool)Model[i].Reviewed)
{
<tr>
#using (Html.BeginForm("Accept", "Overview", FormMethod.Post, new { activityModel = Model }))
{
#Html.AntiForgeryToken()
<td>
#Html.DisplayFor(m => Model[i].Timestamp)
#Html.HiddenFor(m => Model[i].Timestamp)
</td>
<td>
#Html.DisplayFor(m => Model[i].Nick)
#Html.HiddenFor(m => Model[i].Nick)
</td>
<td>
#Html.DisplayFor(m => Model[i].TripName)
#Html.HiddenFor(m => Model[i].TripName)
</td>
<td>
#Html.DisplayFor(m => Model[i].FromTime)
#Html.HiddenFor(m => Model[i].FromTime)
</td>
<td>
#Html.DisplayFor(m => Model[i].FromPlace)
#Html.HiddenFor(m => Model[i].FromPlace)
</td>
<td>
#Html.DisplayFor(m => Model[i].ToTime)
#Html.HiddenFor(m => Model[i].ToTime)
</td>
<td>
#Html.DisplayFor(m => Model[i].ToPlace)
#Html.HiddenFor(m => Model[i].ToPlace)
</td>
<td>
#Html.TextBoxFor(m => Model[i].Difficulty, new { id = "Difficulty" })
<--!#Html.HiddenFor(m => Model[i].Difficulty)--!>
<div style="visibility:hidden">
#Html.HiddenFor(m => Model[i].Id)
#Html.HiddenFor(m => Model[i].Reviewed)
#Html.HiddenFor(m => Model[i].activityType)
#Html.HiddenFor(m => Model[i].Weather)
</div>
</td>
<td>
<input type="submit" name="btn_accept" value="Accept" />
</td>
}
</tr>
}
}
And my Controller is this
public ActionResult Index()
{
List<ActivityModel> activities_web = new List<ActivityModel>();
//someLogicForFilling
return View(activities_web);
}
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult Accept(ActivityModel activityModel)
{
return RedirectToAction("Index");
}
And the problem is that in ActionResult Accept is ActivityModel always NULL empty 0 ...
I really spent whole day on it for now and I don't know where is the problem i tried almost 20 solutions and nothing worked for me..
Rendering the page and showing all values is OK but when I try to POST them like nothing is posted.
Model binding will work when the posted data matches with the property structure of the class you are using as your HttpPost action method parameter.
Your current view code is generating HTML code like this
<input name="[0].Id" type="hidden" value="101">
<input name="[1].Id" type="hidden" value="202">
Assuming you have 2 items in the collection you are passing to the view.
You can see that the name attribute value is [0].Id and [1].Id. But your Accept action method's parameter is a single ActivityModel object and the property names are like Id, FromPlace etc. For model binding to work, the names of your input and the property name should match.
You can rewrite your view code to generate the correct name attribute value for your hidden inputs. You may simply use the pure HTML code to create the hidden input and set the value from your model item or use the Html.HiddenFor helper method.
#foreach (var item in Model.Where(a => a.Reviewed == false))
{
<input type="hidden" name="FromPlace" value="#item.FromPlace" />
#Html.Hidden("ToPlace", item.ToPlace)
#Html.Hidden(nameof(ActivityModel.Id), item.Id)
}
Here is the full code (excluded some properties to save space).
#model IList<ActivityModel>
<table class="table table-striped">
#foreach (var item in Model.Where(a => a.Reviewed == false))
{
using (Html.BeginForm("Accept", "Process", FormMethod.Post))
{
#Html.AntiForgeryToken()
<tr>
<td>
#Html.DisplayFor(m => item.FromPlace)
#Html.Hidden("FromPlace", item.FromPlace)
</td>
<td>
#Html.DisplayFor(m => item.ToPlace)
#Html.Hidden("ToPlace", item.ToPlace)
</td>
<td>
#Html.Hidden(nameof(ActivityModel.Id),item.Id)
<input type="submit" name="btn_accept" value="Accept" />
</td>
</tr>
}
}
</table>
But ideally, if you are trying to update the status of a single entity /record, all you need is the Id (primary key) of that record. that means, you really do not need all the properties populated. In that case, you need the hidden input element for the Id property value. You can always query the db to get the full entity using this Id if needed. ( Never trust data coming from client)

Why the model is null in Action method in ASP.NET MVC?

my problem is that after click on button "Save", I get the model passed to controller from view is null.
Here my code of the View and Controller.
Do you know where I am doing wrong?
Thank you so much.
View Certificazioni.cshtml
#model List<ElencoCertificazioniItem>
...
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
...
<tbody>
#{
for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>#Html.LabelFor(m => Model[i].Id) </td>
<td>#Html.LabelFor(m => Model[i].description)</td>
<td>#Html.EditorFor(m => Model[i].Field1.Value)</td>
<td>#Html.EditorFor(m => Model[i].Field2.Value)</td>
</tr>
}
}
</tbody>
}
....
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
[HttpParamAction]
public ActionResult SaveItems(List<ElencoCertificazioniItem> model)
{
//the items here is null!!! ;(
return saveItems(model);
}
Model
public class ElencoCertificazioniItem
{
public int Id { get; set; }
public string description { get; set; }
public bool? Field1 { get; set; }
public bool? Field2 { get; set; }
}
I use HttpParamAction to manage calls to different methods controller (I have 2 button in the same form).
public class HttpParamActionAttribute : ActionNameSelectorAttribute
{
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
return true;
var request = controllerContext.RequestContext.HttpContext.Request;
return request[methodInfo.Name] != null;
}
}
The list is not null and contains right count elements, but the items within are null and Id properties is always 0!
Your ID and description will always be 0 and null because you don't use any input to post them in form. If you want return them, you have to use input hiddent to pass this values
<tr>
<td>
#Html.LabelFor(m => Model[i].Id)
#Html.HiddenFor(m => Model[i].Id)
</td>
<td>
#Html.LabelFor(m => Model[i].description)
#Html.HiddenFor(m => Model[i].description)
</td>
<td>#Html.EditorFor(m => Model[i].Field1)</td>
<td>#Html.EditorFor(m => Model[i].Field2)</td>
</tr>

MVC model data that is rendered in the view is null when posted back

I have seen similar questions to this and followed the routine answer which is to ensure all model data is rendered in the HTML.
I have done that and the model is rendered in the view with #Html.HiddenFor() but when the posting back to the controller there are no items in the list ?
The view will happily render multiple items in the list, but List<Item> Items in the posted data is always an empty list (not null)
Model
public class ItemCollection
{
public List<string> AvailiableActions { get; set; }
public List<Item> Items { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string SelectedAction { get; set; }
}
View
#model ItemCollection
#using (Html.BeginForm("myAction", "myController", FormMethod.Post))
{
<fieldset>
<div>
#Html.HiddenFor(m => Model.Items)
#Html.DisplayNameFor(x => x.AvailiableActions)
<table>
#{
foreach (var item in Model.Items)
{
#Html.HiddenFor(m => item)
#Html.HiddenFor(s => item.Id)
<tr>
<td>#item.Name</td>
<td>#Html.DropDownList(item.SelectedAction, new SelectList(Model.AvailiableActions))</td>
</tr>
}
}
</table>
</div>
</fieldset>
}
Controller
[HttpPost]
private ActionResult myAction(ItemCollection model)
{
if (model.Items.Count() == 0)
{
// this is true.. something is wrong......
}
}
You cannot use a foreach loop to render controls for a collection. It renders duplicate id and name attributes without the necessary indexers to bind to a collection. Use a for loop
for (int i = 0; i < Model.Items.Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.Items[i].Id)
#Html.DisplayFor(m => m.Items[i].Name)
</td>
<td>#Html.DropDownList(m => m.Items[i].SelectedAction, new SelectList(Model.AvailiableActions))</td>
</tr>
}
Note your view also included #Html.HiddenFor(m => Model.Items) and #Html.HiddenFor(m => item) which would also have failed because item is a complex object and you can only bind to value types. You need to remove both.
Instead of iterating over all items to make sure the index is added to the generated output, you may consider using EditorTemplates (an example on an other site).
EditorTemplates allow you to specify a template for a single Item in \Views\Shared\EditorTemplates\Item.cshtml:
#model Item
#{
var options= (List<string>)ViewData["Options"];
}
<tr>
<td>
#Html.HiddenFor(m => m.Id)
#Html.DisplayFor(m => m.Name)
</td>
<td>#Html.DropDownList(m => m.SelectedAction, new SelectList(options))</td>
</tr>
Then you may simply change your view to:
#model ItemCollection
#using (Html.BeginForm("myAction", "myController", FormMethod.Post))
{
<fieldset>
<div>
<table>
#Html.EditorFor(m => m.Items, new {Options = Model.AvailiableActions })
</table>
</div>
</fieldset>
}

How do I make my list based editor template bind properly for a POST action?

I have a model, ApplicantBranchList, that is used as a property in a larger model as follows:
[Display(Name = "Where would you want to work?")]
public ApplicantBranchList PreferedBranches { get; set; }
ApplicantBranchList:
public class ApplicantBranchList : ViewModel
{
public ApplicantBranchItem HeaderItem { get; set; }
public ApplicantBranchList()
{
HeaderItem = new ApplicantBranchItem();
}
public void MapFromEntityList(IEnumerable<ApplicantBranch> applicantBranches)
{
var service = new BranchService(DbContext);
var selectedIds = applicantBranches.Select(b => b.BranchId);
Items = service.ReadBranches()
.Where(i => !i.IsDeleted)
.Select(p => new ApplicantBranchItem { BranchName = p.Name, WillWorkAt = selectedIds.Contains(p.Id) });
}
public IEnumerable<ApplicantBranchItem> Items { get; set; }
}
ApplicantBranchList has its own editor template, and an inner editor template for each item in ApplicantBranchList:
Views/Shared/EditorTemplates/ApplicantBranchList.cshtml:
#model Comair.RI.UI.Models.ApplicantBranchList
<table>
<tr>
<th style="display: none;"></th>
<th>
#Html.DisplayNameFor(model => model.HeaderItem.BranchName)
</th>
<th>
#Html.DisplayNameFor(model => model.HeaderItem.WillWorkAt)
</th>
</tr>
#foreach (var item in Model.Items)
{
#Html.EditorFor(m => item)
}
</table>
Views/Shared/EditorTemplates/ApplicantBranchItem.cshtml:
#model Comair.RI.UI.Models.ApplicantBranchItem
<tr>
<td style="display: none;">
#Html.HiddenFor(m => m.BranchId)
</td>
<td>
#Html.DisplayFor(m => m.BranchName)
</td>
<td>
#Html.EditorFor(m => m.WillWorkAt)
</td>
</tr>
This editor renders properly in the view, but in the post action:
public ActionResult Create(ApplicantProfileModel model)
{
if (ModelState.IsValid)
{
var branches = model.PreferedBranches;
PreferedBranches.Items is null.
What am I doing wrong?
The problem is that ASP.NET can't figure out how to bind to Model.Items property.
To to fix it replace:
public IEnumerable<ApplicantBranchItem> Items { get; set; }
with this:
public List<ApplicantBranchItem> Items { get; set; }
and instead of:
#foreach (var item in Model.Items)
{
#Html.EditorFor(m => item)
}
use this one:
#for (var i = 0; i < Model.Items.Count; i++)
{
#Html.EditorFor(model => model.Items[i]) // binding works only with items which are accessed by indexer
}
With MVC and editor templates you don't need to manually move through a list and call #HTMLEditorFor.
Doing this:
#Html.EditorFor(model => model.Items)
is the same as:
#for (var i = 0; i < Model.Items.Count; i++)
{
#Html.EditorFor(model => model.Items[i]) // binding works only with items which are accessed by indexer
}
MVC will handle the iteration through your items and generate your editor template once per item. As is noted in the comments your template must be named the same as your model. Also, your model definition should be a singular representation of your model, not of type IEnumerable. Lastly, as noted in the comments, if you specify the template name parameter in your call to #Html.EditorFor() you will not have the benefit of the automatic iteration over your collection. You will need to manually iterate as is demonstrated above.

ViewModel collection property lost values after posting back to controller action in MVC 3

I have my view models :
public class POReceiptViewModel
{
public virtual int PONumber { get; set; }
public virtual string VendorCode { get; set; }
public virtual IList<POReceiptItemViewModel> POReceiptItems { get; set; }
public POReceiptViewModel()
{
POReceiptItems = new List<POReceiptItemViewModel>();
}
}
public class POReceiptItemViewModel
{
public virtual string ItemCode { get; set; }
public virtual string ItemDesription { get; set; }
public virtual decimal OrderedQuantity { get; set; }
public virtual decimal ReceivedQuantity { get; set; }
public virtual DateTime ReceivedDate { get; set; }
public POReceiptItemViewModel()
{
ReceivedDate = DateTime.Now;
}
}
Then my controller has two actions, one get and one post:
public ActionResult CreatePOReceipt(int poNumber)
{
PurchaseOrder po = PurchasingService.GetPurchaseOrder(poNumber);
POReceiptViewModel poReceiptViewModel = ModelBuilder.POToPOReceiptViewModel(po);
return View("CreatePOReceipt", poReceiptViewModel);
}
[HttpPost]
public ActionResult CreatePOReceipt(POReceiptViewModel poReceiptViewModel)
{
// Here the problem goes. The items in the poReceiptViewModel.POReceiptItems has lost. the count became zero.
return View("Index");
}
And in my View, I can display the model properly and by using #Html.HiddenFor<> I can persist view model data as I wanted to. But not on the List<> navigation property.
#model POReceiptViewModel
#using (Html.BeginForm())
{
<fieldset>
<legend>Purchase Order</legend>
<label>For PO # :</label>
#Html.HiddenFor(m => m.PONumber)
#Html.DisplayTextFor(m => m.PONumber)
<label>Vendor Code :</label>
#Html.HiddenFor(m => m.VendorCode)
#Html.DisplayTextFor(m => m.VendorCode)
</fieldset>
<fieldset>
<legend>Received Items</legend>
<table class="tbl" id="tbl">
<thead>
<tr>
<th>Item Code</th><th>Item Description</th><th>OrderedQuantity</th><th>Received Quantity</th><th>Received Date</th>
</tr>
</thead>
<tbody>
#Html.HiddenFor(m => m.POReceiptItems) // I'm not really sure if this is valid
#if (Model.POReceiptItems.Count > 0)
{
foreach (var item in Model.POReceiptItems)
{
<tr>
<td>#Html.DisplayTextFor(i => item.ItemCode)</td>#Html.HiddenFor(i => item.ItemCode)
<td>#Html.DisplayTextFor(i => item.ItemDesription)</td>#Html.HiddenFor(i => item.ItemDesription)
<td>#Html.DisplayTextFor(i => item.OrderedQuantity)</td>#Html.HiddenFor(i => item.OrderedQuantity)
<td>#Html.TextBoxFor(i => item.ReceivedQuantity)</td>
<td>#Html.TextBoxFor(i => item.ReceivedDate)</td>
</tr>
}
}
</tbody>
</table>
</fieldset>
<input type="submit" name="Received" value="Received" />
}
PROBLEM:
POReceiptItems lost when the form submitted. As much as possible I don't want to use TempData["POReceiptItems"] = Model.POReceiptItems but even if I use it, the value entered into ReceivedQuantity and ReceivedDate are not save into the TempData.
Thanks in advance!
try
#for (int i = 0; i < Model.POReceiptItems.Count(); i++)
{
<tr>
<td>#Html.DisplayTextFor(m => m.POReceiptItems[i].ItemCode)</td>#Html.HiddenFor(m => m.POReceiptItems[i].ItemCode)
<td>#Html.DisplayTextFor(m => m.POReceiptItems[i].ItemDesription)</td>#Html.HiddenFor(m => m.POReceiptItems.ItemDesription) <td>#Html.DisplayTextFor(m => m.POReceiptItems[i].OrderedQuantity)</td>#Html.HiddenFor(m => m.POReceiptItems[i].OrderedQuantity)
<td>#Html.TextBoxFor(m => m.POReceiptItems[i].ReceivedQuantity)</td>
<td>#Html.TextBoxFor(m => m.POReceiptItems[i].ReceivedDate)</td>
</tr>
}
also read this blog post to understand how model binding to a list works
You lose your list because MVC don't handle the List the way you think.
You should use BeginCollectionItem look at this post
I had a similar problem, the "List" attribute returned without values(count = 0), I tried different ways and answers and nither works.
Then I tried by myself and now it is working, this is my solution:
I send an object with some normal attributes and a "List", after that I used the normal attributes and my "list" in a For.
In my controller (Post ActionResult), in the parameters section I added two parameters, my original object and my "List" as second parameter and it works!!!
I hope this helps you and others with similar problems.

Resources