MVC model data that is rendered in the view is null when posted back - asp.net-mvc

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>
}

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)

Html.BeginForm call the right Action in Controller

There are a lot of topics related to this question but I still did't figured out what I'm doing wrong.
I have a database where I manage access of different users to folders. On my View the User can select Employees which should have access to certain folder. Then I want to pass the selected Employees to Controller, where the database will be updated.
My Problem is: The right Action in the Controller class didn't get invoked.(I have a breakpoint inside)
Here is the View
#model DataAccessManager.Models.EmployeeSelectionViewModel
#{
ViewBag.Title = "GiveAccessTo";
}
#using (Html.BeginForm("SubmitSelected", "FolderAccessController", FormMethod.Post, new { encType = "multipart/form-data"}))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.fr_folder_uid_fk)
<div class="form-horizontal">
<input type="submit" value="Save" id="submit" class="btn btn-default" />
<table id="tableP">
<thead>
<tr>
<th>Selection</th>
<th>Second Name</th>
<th>First Name</th>
<th>Department</th>
</tr>
</thead>
<tbody id="people">
#Html.EditorFor(model => model.People)
</tbody>
</table>
</div>
</div>
</div>
}
Here is the Controller reduced to the minimum
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SubmitSelected(EmployeeSelectionViewModel model)
{
return View();
}
More Details: I am not sure what is causing the problem, so here some more details.
The view is strongly typed to EmployeeSelectionViewModel, it represets the table with all Employees as a List here is the the code:
public class EmployeeSelectionViewModel
{
public List<SelectEmployeeEditorViewModel> People { get; set; }
public EmployeeSelectionViewModel()
{
this.People = new List<SelectEmployeeEditorViewModel>();
}
public Int64 fr_folder_uid_fk { get; set; }
public IEnumerable<string> getSelectedIds()
{
// Return an Enumerable containing the Id's of the selected people:
return (from p in this.People where p.Selected select p.fr_mavnr_fk).ToList();
}
}
The SelectEmployeeEditorViewModel represents one row of the table with all Employees.
public class SelectEmployeeEditorViewModel
{
public bool Selected { get; set; }
public string fr_mavnr_fk { get; set; }
public string firstName { get; set; }
public string secondName { get; set; }
public string dpt { get; set; }
}
And it has a View which create the checkboxes for each Employee
#model DataAccessManager.Models.SelectEmployeeEditorViewModel
<tr>
<td style="text-align:center">
#Html.CheckBoxFor(model => model.Selected)
#Html.HiddenFor(model => model.fr_mavnr_fk)
</td>
<td>
#Html.DisplayFor(model => model.secondName)
</td>
<td>
#Html.DisplayFor(model => model.firstName)
</td>
<td>
#Html.DisplayFor(model => model.dpt)
</td>
</tr>
The /FolderAccessController/SubmitSelected URL is called in the browser when I press the Submit button, but as mentioned the Action isn't invoked.
EDIT: I get the HTTP 404 not found error after pressing the button
Try removing the "Controller" word from your Html.BeginForm() second parameter, it's not needed.
#using (Html.BeginForm("SubmitSelected", "FolderAccess", FormMethod.Post, new { encType = "multipart/form-data"}))
Thiago Ferreira and haim770 Thanks a lot! The solution is to use the combination of your comments. So:
#using (Html.BeginForm("SubmitSelected", "FolderAccess", FormMethod.Post))
at the Controller

Post Multiple Data from View to Controller MVC

I want to post quantity property to Controller (It's an edit action). I'm editing OrderedProductSet which is connected with ProductSet in my SQL Database (I get the name and price from there). How to pass multiple data from the view to controller? How to write method in controller class to receive the data (I'm asking about method arguments in this specific case).
My view:
#model Shop.Models.ProductViewModel#{
ViewBag.Title = "Edycja zamówienia";
}
<h2>Edycja zamówienie</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<table class="table">
<tr>
<th>
<b>Nazwa produktu</b>
</th>
<th>
<b>Cena</b>
</th>
<th>
<b>Ilość</b>
</th>
<th></th>
</tr>
#foreach (var item in Model.orderedProductSet)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProduktSet.name)
</td>
<td>
#Html.DisplayFor(modelItem => item.ProduktSet.price)
</td>
<td>
#Html.EditorFor(model => item.quantity, new { htmlAttributes = new { #class = "form-control" } })
</td>
</tr>
}
</table>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Potwierdź zmiany" class="btn btn-default" />
</div>
</div>
}
<div>
#Html.ActionLink("Powrót", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
My model (in separated classes of course):
public class ProductViewModel
{
public OrderSet orderSet { get; set; }
public IEnumerable<OrderedProductSet> orderedProduktSet { get; set; }
}
public partial class OrderedProduktSet
{
public int orderNumber{ get; set; }
public int productNumber { get; set; }
public int ilosc { get; set; }
public virtual ProduktSet ProduktSet { get; set; }
public virtual OrderSet OrderSet { get; set; }
}
You need to construct controls for you collection in a for loop or use a custum EditorTemplate for OrderedProduktSet so that the controls are correctly named with indexers and can be bound on post back. Note the for loop approach required that the collection be IList.
#model Shop.Models.ProductViewModel
#using(Html.BeginForm())
{
....
for(int i = 0; i < Model.orderedProductSet.Count; i++)
{
#Html.DisplayFor(m => m.orderedProductSet[i].ProduktSet.name)
....
#Html.EditorFor(m => m.orderedProductSet[i].quantity, new { htmlAttributes = new { #class = "form-control" } })
}
<input type="submit" />
}
Controller (the model will be bound, including the collection of OrderedProductSet)
public ActionResult Edit(ProductViewModel model)
{
....
}
Alternatively, you can create an EditorTemplate
/Views/Shared/EditorTemplates/OrderedProduktSet.cshtml
#model OrderedProduktSet
#Html.DisplayFor(m => m.ProduktSet.name)
#Html.EditorFor(m => m.quantity, new { htmlAttributes = new { #class = "form-control" } })
and in the main view
#model Shop.Models.ProductViewModel
#using(Html.BeginForm())
{
....
#Html.EditorFor(m => m.orderedProductSet)
<input type="submit" />
}
Viewbag is your friend here. You normally pass data from View to Controller in MVC. You can access data set in a Viewbag in the controller in your View.
The simplest way to let your controller handle your view is to create an actionresult method in your controller with the same name as your view.
For example, your view is called Index, thus you would have the following method in your controller to handle the view data:
public ActionResult Index()
{
return View();
}
Accessing a list:
Use a Viewbag.
Controller
Viewbag.MyList = myList
View
#foreach (var item in Viewbag.MyList)
Here is good link for more info:
http://www.asp.net/mvc/overview/older-versions/getting-started-with-aspnet-mvc4/adding-a-view

How to render object collection as editable(checkboxes) list view

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.

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.

Resources