MVC 5 Viewmodel binding works but post back is partial filled - c#

I have a parameterless Index for the HttpGet which works. But when I post it the HttpPost version of Index is invoked and the viewmodel object is passed in, but there is only the value of the dropdown in it. The rest is null (products, title)
[HttpPost]
public ActionResult Index(ProductsViewModel pvm)
{
// breakpoint on line 36, shows that pvm.Title is null and Products too.
return View(pvm);
}
My compilable and running example can be downloaded from my OneDrive http://1drv.ms/1zSsMkr
My view:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect"})
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}

If I have this view model:
public class ViewModel
{
public string Name {get;set;}
public string SelectedLocation {get;set;}
public IEnumerable<SelectListItem> Locations {get;set;}
}
And your actions look like this:
public ActionResult MyForm()
{
var vm = new ViewModel
{
Locations = context.Locations.ToList() // Some database call
}
return View(vm);
}
[HttpPost]
public ActionResult MyForm(ViewModel vm)
{
vm.Locations // this is null
}
It is null because the model binder can't find a form control that is setting its data.
The <form> must set some data in the view for the model binder to pick it up.
<form>
Name: <input type="text" id="name" />
</form>
This will set the Name property on the view model, because the model bind can see the id of the form control and uses that to know what to bind to.
So in terms of your view, you need to make sure you wrap any content that you want to post back to the server with #using(Html.BeginForm())
Anyway this is my guess.

Well, you seem to be confused as to how [HttpPost] and form tags interact with eachother.
You see, when .NET MVC binds your parameters in your controller actions, it tries to derive that data from the request. For [HttpGet] it does this by looking at the query string.
For [HttpPost] calls, it also looks at the Request.Form. This variable is populated with the values of all input fields that were inside the form you submitted.
Now, this is your view:
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#foreach (var item in Model.Products)
{
#Html.DisplayFor(i => item.Name);
}
</div>
}
}
You only have one select tag (generated by Dropdownlistfor) but no other inputs. That's why .NET MVC cannot infer any other data for your view model.
If you change your view to this:
#model KleinKloteProductOverzicht.Models.ProductsViewModel
#using (Html.BeginForm("Index", "Products"))
{
<h2>#Html.DisplayFor(m => m.Title)</h2>
<input type="submit" value="post dit" /><br/>
<div class="row">
<div class="col-lg-2 col-md-2">
#Html.DropDownListFor(x => x.CurrentSort, EnumHelper.GetSelectList(typeof(SortOptions)), new { #class = "multiselect" })
</div>
</div>
if (Model.Products.Count() > 0)
{
<div class="row">
#for (var i = 0; i < Model.Products.Count; i++)
{
#Html.DisplayFor(model => model.Products[i].Name)
#Html.HiddenFor(model => model.Products[i].ID)
}
</div>
}
}
You'll see I've added a hidden input (<input type="hidden">) for the product id. Note that the product name still will be null.
I would suggest you follow a tutorial on .NET MVC and read up on some of the concepts behind it, because the very fact that you ask this question reveals that you have much to learn.
Best of luck!
P.S. One last tip: #Html.Blablabla writes directly to your view. You usually don't need that ";" at the end, because it will be inside your generated html.

Your property is not associated with a "postable" control, therefore it will not be submitted along with the form data. If your really want to get the value in your Title property, just set it as a hidden input.
#Html.HiddenFor(m => m.Title)
A label will not be posted when submitting a form but an input will. This is exactly what HiddenFor does; it creates a hidden input element which will be picked up by the form submit.

Related

MVC Html.HiddenFor in a loop passing model back to Controller

I am looping through an IEnumerable of my model:
#model IEnumerable<Testing.Models.ProductItem>
#{
ViewBag.Title = "Buy Products";
}
<div class="row">
#foreach (var product in Model)
{
using (Html.BeginForm())
{
#Html.HiddenFor(Model => product)
... More Controls and stuff...
<input type="submit" value="Add To Kart" class="btn btn-info">
}
}
</div>
and on submit I want to pass the selected instance of my model back to my controller:
[HttpPost]
public ActionResult Index(ProductItem product)
{
... Do Stuff ...
return View();
}
However I have tried a few things but always seem to be getting null passed into the controller... Please could someone please help point me in the right direction?
EDIT
I dont actually need to the full model instance as I can get this within the controller from the ID - so I have tried the following:
#model IEnumerable<Testing.Models.ProductItem>
#{
ViewBag.Title = "Buy Products";
}
<div class="row">
#foreach (var product in Model)
{
using (Html.BeginForm())
{
#Html.HiddenFor(Model => product.ID)
#Html.TextBox("qty", "1", htmlAttributes: new { #style = "width: 30px;" })
... More Controls and stuff...
<input type="submit" value="Add To Kart" class="btn btn-info">
}
}
</div>
which posts to the controller:
[HttpPost]
public ActionResult Index([Bind(Include = "ID")] int? ID, [Bind(Include = "qty")] int? qty)
{
return null;
}
The textbox is not part of the model as it is user input - this value is passed nicely into the actions parameter, however I am still getting a null for the ID in the HiddenFor control. Is this to do with the naming of the control? I dont seem to be able to add a name to the HiddenFor control.
I know this puts a different light on the original question but I am hoping you may still be able to help.
I take the note about the BeginForm being inside the loop - creating for each item in the list... Is there an easy alternative to this (note I haven't tried anything yet).
It sounds like you're trying to use HiddenFor on a complex type and that won't work. You'll need to use a property of ProductItem like ProductId or something like that, which will most likely be an int or Guid.
Now that you have cleared up the complex binding to a simple field, you'll notice that your name is being set to product.id and that is why it is always null in your controller. You can't override the name attribute with Hidden for, so you'll want to change your code to:
#foreach (var product in Model)
{
using (Html.BeginForm())
{
#Html.Hidden("ID", product.ID)
#Html.TextBox("qty", "1", htmlAttributes: new { #style = "width: 30px;" })
<input type="submit" value = "Add To Kart" class="btn btn-info">
}
}
I have managed to arrive at my desired functionality (rightly or wrongly) with the following:
#model List<ShoppingKartTest.Models.ProductItem>
#{
ViewBag.Title = "Buy Products";
}
#foreach (var item in Model)
{
using (Html.BeginForm())
{
<input type="hidden" value="#item.ID" name="ID" />
#Html.TextBox("qty", "1", new { #style = "width: 30px;" })
<input type="submit" value="Add To Kart" class="btn btn-info">
}
}
Which correctly submits the Hidden ID and the contents of the Textbox to the Controller Action:
[HttpPost]
public ActionResult Index(int ID, int qty)
{
//... Do stuff with parameters...
return View();
}
I would be interested to hear any comments on this. I know that I was told above that I shouldn't have my BeginForm within the loop... But it just works for me.
Instead of Model => product.Id, try p=> product.Id
#model IEnumerable<Testing.Models.ProductItem>
#{
ViewBag.Title = "Buy Products";
}
<div class="row">
using (Html.BeginForm())
{
#foreach (var product in Model)
{
#Html.HiddenFor(p => product.ID)
#Html.TextBox("qty", "1", htmlAttributes: new { #style = "width:
30px;" })
... More Controls and stuff...
}
<input type="submit" value="Add To Kart" class="btn btn-info">
}
</div>

HTML rendering model as string

This is a fork from another question. I am passing a list into a partial view which is part of a main view. However, when I view the page, I see:
System.Collections.Generic.List`1[...Models.OutcomeArea] List
Now, the list is being rendered correctly, but I am not sure why the above line is showing in the html.
Index.cshtml:
#using (Html.BeginForm("Index", "Search", FormMethod.Get, new { #class = "form-inline" }))
{
#Html.Action("_Checklist")
}
_Checklist.cshtml:
#Model List<....Models.OutcomeArea>
<div class="row">
<div class="col-md-4">
<h4 class="text-center">Outcomes</h4>
<p>
#foreach (var list in Model)
{
<input type="checkbox" id="#list.ID" name="#list.ID" /> #list.Category <br />
}
</p>
</div>
</div>
SearchController:
public PartialViewResult _Checklist()
{
var outcomeAreas = db.OutcomeArea.Where(oa => oa.Category != "").GroupBy(oa => oa.Category).Select(oa => oa.FirstOrDefault());
return PartialView("_Checklist", outcomeAreas.ToList());
}
Nevermind, I have it working. I needed to use lowercase #model for the _Checklist.cshtml.
#model List<....Models.OutcomeArea>
Wow. Took a while to spot the bug. Tricky one, this. Your model declaration is the problem:
#Model List<....Models.OutcomeArea>
That should be #model, lowercase. You're literally telling to print out Model at the moment.

MVC submit isn't returning all data

I'm writing an MVC app which ends up accessing a SQL database. On my edit page, I previously had every item available to be edited that is in the model. Recently I was asked to no longer allow the user to edit the primary keys. I did a quick change to change the primary key fields (in this example, there are 2 of them) from an EditorFor to a DisplayFor. The new view is this:
#model App.Data.Item
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Item</h4>
<hr />
#Html.ValidationSummary(true)
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
</div>
</div>
<div class="form-group">
<strong>Description:</strong>
<div class="col-md-10">
<p>#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)</p>
</div>
</div>
<br />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-primary">Submit <i class="fa fa-caret-right"></i></button>
</div>
</div>
</div>
}
It used to work with the full editing. Now the data is displayed properly, as expected. However, when submit is pressed, Null values are passed back to the controller for the values that are displayed.
These are the edit functions in my controller. ItemService.Edit() just saves the data to the server. It works correctly.
[Authorize]
public ActionResult Edit(string id)
{
if (id == null)
{
//return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
string[] vals = id.Split('|');
ItemAttribute itemAttribute = itemAttributeService.Find(int.Parse(vals[0]), vals[1]);
if (itemAttribute == null)
{
return HttpNotFound();
}
return View(itemAttribute);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Edit([Bind(Include = "ID,ID2,Description")]
Item item)
{
if (item.Description == null)
{
ModelState.AddModelError("Description", "Description cannot be null.");
}
if (ModelState.IsValid)
{
itemService.Edit(item);
return RedirectToAction("../Home/Index/");
}
return View(item);
}
Lastly, my data model:
public class Item
{
public int ID { get; set; }
public string ID2 { get; set; }
public string Description { get; set; }
}
Why is the data no longer being passed back to the second function, and how do I get it to pass correctly so that I can save it to the database?
You need to have an input element generated for the items that you want returned. Currently, you are only displaying two of your model elements and have no associated input with them. As a result, they will not be POSTed back to the server.
To get them to post to the server and not "editable" from a textbox, add a Html.HiddenFor() helper for each of the items that you need returned.
<div class="form-group">
<strong>ID:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
<div class="form-group">
<strong>ID2:</strong>
<div class="col-md-10">
<p>#Html.DisplayFor(model => model.ID2)</p>
<p>#Html.HiddenFor(model => model.ID)</p>
</div>
</div>
However, keep in mind that anyone can edit the HTML using Firebug or Chrome console tools and submit any value that they want for any field, even if you did not want to change it. Be sure that when you are persisting the changes to the database, you are NOT including these fields as part of the update.
Try this, just before this line of code:
#Html.DisplayFor(model => model.ID)
put in this for debugging:
#Html.LabelFor(model => model.ID)
Tell us what you see...
If you see the label then check your controller, in particular the parameter it takes on the post. It should take and Item of type ITEM per your model.
Before the controller receives the data MVC has to try to populate the data... It converts name/value pairs to model types with values secretly. If you don't see any data after you are in the controller it's usually because the names were not found!

Why does DbContext.Entry(IEnumerable<MedicalProduct>).State produce an ArguementNullException in my code?

In my MedicalProductController, I am trying to make my Edit action able to edit multiple objects on one page. To do that, I plan on my HTTPPOST edit action method receiving an IEnumerable<MedicalProduct> instead of the MedicalProduct that the scaffolding set up for me.
When I click save to submit some changes, I get an ArguementNullException unhandled on the line: _db.Entry(productList).State = EntityState.Modified; and I don't understand why it is null.
MedicalProductController:
public class MedicalProductController : Controller
{
private MvcMedicalStoreDb _db = new MvcMedicalStoreDb();
// some code omitted for brevity
public ActionResult Edit(int id = 0)
{
MedicalProduct product = _db.Products.Find(id);
if (product == null)
{
return HttpNotFound();
}
var productList = new List<MedicalProduct> { product };
var viewModel = GetMedicalProductViewModelList(productList);
return View(viewModel);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(IEnumerable<MedicalProduct> productList)
{
if (ModelState.IsValid)
{
_db.Entry(productList).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index");
}
//var productList = new List<MedicalProduct> { product };
var viewModel = GetMedicalProductViewModelList(productList);
return View(viewModel);
}
}
Edit.cshtml:
#model IEnumerable<MvcMedicalStore.Models.MedicalProductViewModel>
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>MedicalProduct</legend>
#foreach (var modelItem in Model)
{
#Html.HiddenFor(item => modelItem.ID)
<div class="editor-label">
#Html.LabelFor(item => modelItem.Name)
</div>
<div class="editor-field">
#Html.EditorFor(item => modelItem.Name)
#Html.ValidationMessageFor(item => modelItem.Name)
</div>
<div class="editor-label">
#Html.LabelFor(item => modelItem.Price)
</div>
<div class="editor-field">
#Html.EditorFor(item => modelItem.Price)
#Html.ValidationMessageFor(item => modelItem.Price)
</div>
<div class="editor-label">
#Html.LabelFor(item => modelItem.BrandName)
</div>
<div class="editor-field">
#Html.DropDownListFor(item => modelItem.BrandName, modelItem.BrandSelectListItem)
#Html.ValidationMessageFor(item => modelItem.BrandName)
</div>
}
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
It looks to me like the model binder isn't able to bind to your collection, which would cause it to be null. The reason it's doing that is because you're not specifying an index for each of the elements. That means MVC has no way to determine how to bind them correctly.
Edit
I've figured out why the last revision of this answer didn't work. Firstly, IEnumerable<T> doesn't have a direct indexer. Instead you would use Model.ElementAt(i).ID to access the ID property. However, this actually wouldn't solve the problem with the model binding issue as, for some reason, this doesn't generate the proper indices on the name attributes for the generated <input> fields. (More on this below.)
There are two ways to fix that. The first way would be to pass a List to the view, instead of IEnumerable, then accessing the fields as I showed earlier. However, the better way would be to create an EditorTemplate instead. This will be easier because it saves you having to change your existing methods which are generating your view model. So you'll need to follow these steps:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
Create a strongly-typed view in that directory with the name that matches your model (e.g in this case the view would be called MedicalProductViewModel).
Move the bulk of your original view into that new template.
You'll end up with the following:
#model MedicalProductViewModel
#Html.HiddenFor(item => Model.ID)
<div class="editor-label">
#Html.LabelFor(item => Model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(item => Model.Name)
#Html.ValidationMessageFor(item => Model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(item => Model.Price)
</div>
<div class="editor-field">
#Html.EditorFor(item => Model.Price)
#Html.ValidationMessageFor(item => Model.Price)
</div>
<div class="editor-label">
#Html.LabelFor(item => Model.BrandName)
</div>
<div class="editor-field">
#Html.DropDownListFor(item => Model.BrandName, Model.BrandSelectListItem)
#Html.ValidationMessageFor(item => Model.BrandName)
</div>
Notice how we're no longer using any indexing notation to access the model properties.
Now in your Edit.cshtml view, you'd be left with this:
#model IEnumerable<MvcMedicalStore.Models.MedicalProductViewModel>
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>MedicalProduct</legend>
#Html.EditorFor(m => m)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Although I gave a brief explanation at the start, I should really explain what this is actually doing. Your original HTML would have produced output like the following:
<input name="ID" type="text" value="1" />
<input name="Name" type="text" value="Name 1" />
<input name="ID" type="text" value="2" />
<input name="Name" type="text" value="Name 2" />
As you can see, multiple input fields share the same name. That's why the model binder is tripping up, because your action is telling it to bind to a collection and the binder needs to be able to distinguish between each element in the collection. EditorTemplates are smart enough to figure out when you're working with a collection and will apply indices to your input fields automatically. What the code above will do is generate output like this instead:
<input name="[0].ID" type="text" value="1" />
<input name="[0].Name" type="text" value="Name 1" />
<input name="[1].ID" type="text" value="2" />
<input name="[1].Name" type="text" value="Name 2" />
As you can see, the fields now have an index associated with them. That gives the model binder all the information it needs to be able to add all of the items to the collection. Now that's out of the way, we can get back to fixing your product saving code.
What Gert said is still right about the way you're trying to save productList. You need to be setting the EntityState.Modified flag on each individual item in that collection:
if (ModelState.IsValid)
{
foreach (var product in productList)
_db.Entry(product).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index");
}
See if that works.

Model not being passed to rendered view

I am having an issue getting data in my model on my MakePayment.cshmtl view.
The AccountScreen.cshtml is calling the MakePayment.cshtml view:
#model SuburbanCustPortal.SuburbanService.CustomerData
#{
ViewBag.Title = "Account Screen";
}
<h2>AccountScreen</h2>
<div class="leftdiv">
<fieldset>
<legend>customer info</legend>
#Html.Partial("CustomerInfoPartialView", Model)
</fieldset>
<fieldset>
<legend>delivery address</legend>
#Html.Partial("DeliveryAddressPartialView", Model)
</fieldset>
<fieldset>
<legend>delivery info</legend>
#Html.Partial("DeliveryInfoPartialView", Model)
</fieldset>
</div>
<div class="rightdiv">
<fieldset>
<legend>balance</legend>
<div>
#Html.Partial("BalancePartialView", Model)
</div>
</fieldset>
<fieldset>
<legend>payment</legend>
<div>
#Html.Partial("MakePayment", Model)
</div>
</fieldset>
<fieldset>
<legend>billing info</legend>
<div>
#Html.Partial("BillingInfoPartialView", Model)
</div>
</fieldset>
</div>
My MakePayment.cshtml view:
#model SuburbanCustPortal.SuburbanService.CustomerData
#using (Html.BeginForm("MakePayment2", "Customer", FormMethod.Post))
{
<div style="text-align:center;">
<input class="makePaymentInput" type="submit" value="Make a Payment" />
</div>
}
My CustomerController:
public ActionResult AccountScreen(LogOnModel model)
{
return ShowCustomer(model.AccountNumber);
}
public ActionResult MakePayment(CustomerData model)
{
return View("MakePayment", model);
}
[HttpPost]
public ActionResult MakePayment2(CustomerData model)
{
//CustomerData model = new CustomerData();
var newmodel = new PaymentModel.SendToGateway();
newmodel.AccountBalance = model.TotalBalance;
newmodel.Amount = model.TotalBalance;
return RedirectToAction("PrePayment", "Payment", newmodel);
}
The public ActionResult MakePayment(CustomerData model) is never being reached.
My problem: The [HttpPost] public ActionResult MakePayment2(CustomerData model) is being reached but the model has nulls in it.
I know the data initial model from the AccountScreen is being populated since the other views that are being rendered is showing data.
Anyone see what I am doing wrong?
The problem is there's nothing inside your form except a submit button. You need to make sure input fields are there (either text boxes, select lists, or hidden fields), as those are what post data back to the controller.
You could try using EditorForModel inside your partial view:
#using (Html.BeginForm("MakePayment2", "Customer", FormMethod.Post))
{
#Html.EditorForModel()
<div style="text-align:center;">
<input class="makePaymentInput" type="submit" value="Make a Payment" />
</div>
}
Edit based on comments
Razor doesn't include an Html.HiddenForModel() method, for whatever reason. Possible workarounds:
List out each property of the model using Html.HiddenFor(model => model.Property)
Annotate the model properties with \[HiddenInput\]
Use EditorForModel() but wrap it in <div style="display: none;"></div> (NOTE that a malicious user can still modify the properties as if they were visible.)
Use only Html.HiddenFor(model => model.id) and fetch the model in the controller.
Use the serialization method in the MVC Futures assembly
Related quesion here:
Is there some way to use #Html.HiddenFor for complete model?
The problem is, you are creating a form containing nothing else than a submit button.
When you submit it, it posts nothing back to the server, thus your function receives an empty model.
#using (Html.BeginForm("MakePayment2", "Customer", FormMethod.Post))
{
<div style="text-align:center;">
<input class="makePaymentInput" type="submit" value="Make a Payment" />
</div>
}
This translates as :
<form method="POST" action="{url}">
<div style="text-align:center;">
<input class="makePaymentInput" type="submit" value="Make a Payment" />
</div>
</form>
More details :
Since in the logic you then redirect to a new page to collect payment information, you don't want to give the user the opportunity to mess with your model, thus you should query your customer data from your Context instead of trusting what is submitted in the POST.
Thus all you really need to add if this :
#using (Html.BeginForm("MakePayment2", "Customer", FormMethod.Post))
{
#Html.HiddenFor(model => model.{ID Field})
<div style="text-align:center;">
<input class="makePaymentInput" type="submit" value="Make a Payment" />
</div>
}
This way, you will be able to get your model back in the server side code.
Basically, your form submits nothing as there are no input fields inside the form scope. Try to wrap all your html in AccountScreen.cshtml within #using (Html.BeginForm( statement (and throw it out from MakePayment.cshtml).

Categories