I have a problem which I don't understand and there doesn't seem to be an easy way to debug the problem. I'm sure it's simple.
#model StartStop.ServiceResources.UserSettings
my MVC3 view is bound a specific model;
public class Setting
{
public Int64 SettingID { get; set; }
public Int64 UserID { get; set; }
public int PreferenceType { get; set; }
public string PreferenceName { get; set; }
public bool PreferenceBool { get; set; }
public int PreferenceInt { get; set; }
public string PreferenceString { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime ModifiedOn { get; set; }
}
public class UserSettings
{
public Int64 UserID { get; set; }
public List<Setting> Settings { get; set; }
}
the view lists out the check boxes which represent the list;
#using (Html.BeginForm("ManageAccount","Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
foreach (var item in Model.Settings.ToList())
{
<tr>
<td>#item.PreferenceName
</td>
<td>
#if (item.PreferenceType == 2)
{
#Html.CheckBoxFor(modelItem => item.PreferenceBool)
}
</td>
</tr>
}
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
All good, I load the data into the view it renders the view and picks up the correct settings. However, when I do a post at the bottom, the view model returns a null! I'm not sure why...
[HttpPost]
[Authorize]
public ActionResult ManageAccount(StartStop.ServiceResources.UserSettings model)
{
if (ModelState.IsValid)
{
foreach (StartStop.ServiceResources.Setting oSetting in model.Settings)
{
StartStop.Helpers.UserPreferences.SaveUserSetting(oSetting);
}
}
return View(model);
}
Can anyone help?
The problem is on the following line in your view:
#Html.CheckBoxFor(modelItem => item.PreferenceBool)
I see people writing the following lambda expression modelItem => item.SomeProperty in their views very often and asking why the model binder doesn't correctly bind collection properties on their view models.
This won't generate proper name for the checkbox so that the default model binder is able to recreate the Settings collection. I would recommend you reading the following blog post to better understand the correct format that the model binder expects.
Try like this:
#model StartStop.ServiceResources.UserSettings
#using (Html.BeginForm("ManageAccount", "Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
for (var i = 0; i < Model.Settings.Count; i++)
{
<tr>
<td>#Model.Settings[i].PreferenceName</td>
<td>
#if (Model.Settings[i].PreferenceType == 2)
{
#Html.CheckBoxFor(x => x.Settings[i].PreferenceBool)
}
</td>
</tr>
}
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
This being said, I would recommend you using editor templates, like so:
#using (Html.BeginForm("ManageAccount","Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
#Html.EditorFor(x => x.Settings)
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
and then define a custom editor template which will automatcially be rendered for each element of the Settings collection (~/Views/Shared/EditorTemplates/Setting.cshtml):
#model StartStop.ServiceResources.Setting
<tr>
<td>#Model.PreferenceName</td>
<td>
#if (Model.PreferenceType == 2)
{
#Html.CheckBoxFor(x => x.PreferenceBool)
}
</td>
</tr>
Also the only input field that I can see in this form is the checkbox which is bound to the PreferenceBool property on your model. So inside your POST controller action you will get the Settings list property initialized but don't expect to find any values for the other properties in this Setting class unless of course you include input fields for them in the form (and more precisely in the editor template that I have shown).
Related
Starting up with ASP.NET Core 6 MVC and in my case I have one view which lists few properties of one object and then some other for their children in a editable table (user can edit the values)
View model has the properties and an IEnumerable of the children:
public class MyObjectViewModel
{
public String Id { get; set; }
public String Descr { get; set; }
public IEnumerable<ChildrenObject> Children { get; set; }
public MyObjectViewModel()
{
Children = Enumerable.Empty<ChildrenObject>();
}
public class ChildrenObject
{
public String? Id { get; set; }
public String? Name { get; set; }
}
}
All under the same form:
#using (Html.BeginForm("Save", "Controller", FormMethod.Post))
{
<input type="submit" value="SAVE"/>
<br />
#Html.LabelFor(model => model.Id)
#Html.TextBoxFor(model => model.Id, new { #readonly = "readonly" })
<br />
#Html.LabelFor(model => model.Descr)
#Html.EditorFor(model => model.Descr)
<br />
<table>
<tbody>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
#foreach (var child in Model.Children)
{
<tr>
<td>#child.Id</td>
<td><input class="ef-select" type="text" value="#child.Name"></td>
</tr>
}
</tbody>
</table>
}
So when the button is pressed the data is dumped back to the model to perform the SAVE action in the controller.
All ok for the simple fields (I can get the data in the controller as the view model), but not sure how to accomplish it with the table / children property...
Any simple way without needing to use JS to serialise or pick up the data from the table?
Thanks
If you want to pass id and name of ChildrenObject in table,you can try to add hidden inputs for the Id,and set name attribute for name inputs:
#{var count = 0; }
#foreach (var child in Model.Children)
{
<tr>
<td>
#child.Id
<input value="#child.Id" name="Children[#count].Id" />
</td>
<td><input class="ef-select" type="text" value="#child.Name" name="Children[#count].Name"></td>
</tr>
count++;
}
I'm trying to make a simple shopping cart app in Asp.Net Core 2.0 MVC. I'm not doing any Ajax-ing. I have three models:
public class Product
{
public int Id { get; set; }
public string Title { get; set; }
public string Info { get; set; }
public decimal Price { get; set; }
}
public class Cart
{
public int Id { get; set; }
public int CartItemId { get; set; }
public int CustomerId { get; set; } // not in use yet
}
public class CartItem
{
public int Id { get; set; }
public int ProductId { get; set; }
public int NumEach { get; set; } // not in use yet
}
From either one of the two views below, I want to update Cart and CartItem, and then get redirected back to the view where I clicked the Add to cart-button:
1) Index-view:
#model IEnumerable<simpleShop.Models.Product>
<table class="table">
<thead>
<tr>
<th>#Html.DisplayNameFor(model => model.Title)</th>
<th>#Html.DisplayNameFor(model => model.Info)</th>
<th>#Html.DisplayNameFor(model => model.Price)</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model)
{
<tr>
<td>
<a asp-action="Details" asp-route-id="#item.Id">
#Html.DisplayFor(modelItem => item.Title)
</a>
</td>
<td>#Html.DisplayFor(modelItem => item.Info)</td>
<td>#Html.DisplayFor(modelItem => item.Price)</td>
<td>
<form asp-action="AddToCart">
<button type="submit" value="#item.Id">Add to cart</button>
</form>
</td>
</tr>
}
</tbody>
</table>
2) Details-view:
#model simpleShop.Models.Product
#{
ViewData["Title"] = Html.DisplayFor(model => model.Title);
}
<h2>#Html.DisplayFor(model => model.Title)</h2>
<h4>#Html.DisplayFor(model => model.Info)</h4>
<h1>#Html.DisplayFor(model => model.Price)</h1>
<form asp-action="AddToCart">
<input type="hidden" asp-for="Id" />
<p>
<input type="submit" value="Add to cart" />
</p>
</form>
<div>
<a asp-action="Index">Return to list</a>
</div>
Below is my faulty AddToCart-method in the home controller, which at the moment certainly isn't doing anything to save data to the Cart or CartItem tables. How can I get it to?
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddToCart([Bind("Id")] Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
if (product.Id > 0) // added to cart via the details-view
{
return RedirectToAction("Details", "Home", product.Id);
}
else // added to cart via the index-view
{
return RedirectToAction(nameof(Index));
}
}
return View(product);
}
for one thing Bind() requires way more than just "id", remember what ever you wanted to bind has to include all properties in question of the object you wanted to pass to new action.
reference comments in line -->>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddToCart([Bind("Id")] Product product)
{
if (ModelState.IsValid)
{
_context.Add(product); //**BAD this will probably try to
// add product to the product table again.**
// With error to follow about already exists
// exception at the SaveChangesAsync call.
await _context.SaveChangesAsync();
if (product.Id > 0)
{
return RedirectToAction("Details", "Home", product.Id);
}
else // added to cart via the index-view
{
return RedirectToAction(nameof(Index));
}
}
return View(product);
}
My suggestion goes along these lines where you will have ProductId and Qty of the Product (your NumEach)
[HttpPost]
[ValidateAntiForgeryToken]
public asyync Task<IActionResult> AddToCart([Bind("ProductId", "NumEach", "CartId")] CartItem model, string ReturnUrl){
if(ModelState.IsValid){
_context.CartItem.Add(model);
await _context.SaveChangesAsync();
//model.Id greater than 0 indicates a save occurred.
if(model.Id > 0)
return RedirectToAction(ReturnUrl); // forced return for further shopping?
else
return RedirectToAction("Index");
}
//this assumes bad model state with no error unless model is
//annotated accordingly.
return View(model);
}
problem with Cart though is right now you can only store 1 item...
public class Cart{
public Cart(){}
public int Id {get;set;} //cart's integer id
public List<CartItem> CartItems {get;set;} //collection of cartitems
//FK
public int CustomerId {get;set;} //customer's id
//NAV
public Customer CustomerId {get;set;} //customer nav
...
}
public class CartItem{
public CartItem(){}
public int Id {get;set;} //cart item id
public int ProductId {get;set;} //productid
public Product Product {get;set;} //nav property
public int NumEach {get;set;} //quantity of each product
//FK
public int CartId {get;set;} //Foreign Key
//NAV
public Cart Cart {get;set;} //nav property
}
One other thing your index view wouldn't just have the model be a collection of Products it would probably be a viewmdodel that had a collection of products in it but it would also contain the cartid that would have been generated with the visitor. That CartId would follow that visitor until the transaction was completed and you then have an OrderId or it dies once the session is closed.
#model simpleShop.Models.IndexViewModel
<table class="table">
<thead>
<tr>
<th>#Html.DisplayNameFor(model => model.Title)</th>
<th>#Html.DisplayNameFor(model => model.Info)</th>
<th>#Html.DisplayNameFor(model => model.Price)</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Products)
{
<tr>
<td>
<a asp-action="Details" asp-route-id="#item.Id">
#Html.DisplayFor(modelItem => item.Title)
</a>
</td>
<td>#Html.DisplayFor(modelItem => item.Info)</td>
<td>#Html.DisplayFor(modelItem => item.Price)</td>
<td>
<form asp-action="AddToCart">
<input type="Hidden" asp-for="#Model.CartId" />
<button type="submit" value="#item.Id">Add to cart</button>
</form>
</td>
</tr>
}
</tbody>
</table>
public class IndexViewModel
{
public IndexViewModel(){}
public int CartId{get;set;}
public List<Product> Products{get;set;}
}
I think you get the idea from there.
MVC4 How to get the data in partial view when parent view click submit
I create a edit page for user update the data, the parent view show some of book details and partial view is a loop show the status of each book. Also, the parent view have a submit button for update both of partial view and parent view, but when I click the submit only parent view data can update and partial view still not update. The code below:
Model:
public class LibraryInventory
{
public decimal LibraryID { get; set; }
public string Title { get; set; }
public List<LibraryItem> Entities { get; set; }
}
public class LibraryItem
{ public decimal StatusID { get; set; }
public string Location { get; set; }
public decimal BorrowedBy { get; set; }
}
Controller :
public ActionResult EditRecord(string ID)
{
DataTable dt = (DataTable)Session["EditGridData"];
LibraryInventory record = new LibraryInventory(dt.Rows[0]);
dt = LibraryEditBLL.GetEditItems(ID);
if (results.ToList().Count > 0)
{
record.Entities = LibraryItem.ConvertToLibraryEntity(dt).ToList();
}
return View(record);
}
[HttpPost]
public ActionResult EditRecord(string FormButton, LibraryInventory model)
{
switch (FormButton)
{
case "Submit":
if (ModelState.IsValid)
{
LibraryEditBLL.UpdateInventoryLibrary(model);
}
return View("EditRecord",record);
default:
return RedirectToAction("Edit"); //other page not need check
}
View :
#model XXX.Models.LibraryModels.LibraryInventory
#using (Html.BeginForm("EditRecord", "Library", FormMethod.Post, new { enctype = "multipart/form-data" })){
#Html.HiddenFor(m =>m.LibraryID)
<table>
<tr>
<td>#Html.TextBoxFor(m => m.Title)</td>
#for (var i = 0; i < #Model.Entities.Count; i++)
{
#Html.Partial("EditItem", #Model.Entities[i])
}
</tr>
</table>
}
#model XXX.Models.LibraryModels.LibraryItem
<tr>
<td> #Html.DropDownListFor(m => m.StatusID) </td>
<td> #Html.DropDownListFor(m => m.Location)</td>
<td> #Html.DropDownListFor(m => m.BorrowedBy) </td>
</tr>
Try this code for your view. And no need of partialview for this simple scenario.
#model XXX.Models.LibraryModels.LibraryInventory
#using (Html.BeginForm("EditRecord", "Library", FormMethod.Post, new { enctype = "multipart/form-data" })){
#Html.HiddenFor(m =>m.LibraryID)
<table>
<tr>
<td>#Html.TextBoxFor(m => m.Title)</td>
</tr>
#for (var i = 0; i < Model.Entities.Count; i++)
{
<tr>
<td> #Html.DropDownListFor(m => Model.Entities[i].StatusID) </td>
<td> #Html.DropDownListFor(m => Model.Entities[i].Location)</td>
<td> #Html.DropDownListFor(m => Model.Entities[i].BorrowedBy) </td>
</tr>
}
</table>
}
I am having issues with a view model that constantly return null properties after a post. Below is my code (it could be a syntax issue or two calling a class or property the same name as i saw in other posts but i could not see any such issue in code):
VIEW MODEL:
public class ProductItem
{
public int ProductID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string LongDescription { get; set; }
public int SupplierID { get; set; }
public string Dimensions { get; set; }
public double Price { get; set; }
public bool On_Sale { get; set; }
public double DiscountedPrice { get; set; }
public string Thumbnail { get; set; }
public string LargeImage { get; set; }
public string LargeImage2 { get; set; }
public string LargeImage3 { get; set; }
public string CrossRef { get; set; }
public byte Available { get; set; }
public double Weight { get; set; }
public byte Important { get; set; }
public virtual ICollection<ProductCategory> ProductCategories { get; set; }
// this is required on the page to allow products to be marked for deletion
public bool IsForDelete { get; set; }
}
public class ProductListViewModel
{
public IEnumerable<ProductItem> ProductItems { get; set; }
public IEnumerable<Category> CategoryItems { get; set; }
}
CONTROLLER:
public ActionResult ProductList()
{
var productList = new ProductListViewModel();
productList.ProductItems = productRepository.GetProductsWithDeleteOption().ToList();
productList.CategoryItems = categoryRepository.GetCategories().ToList();
return View(productList);
}
[HttpPost]
public ActionResult ProductList(ProductListViewModel productViewModel, FormCollection formCollection, string submit)
{
if (ModelState.IsValid)
{
// Check for submit action
if (submit == "Change Sort")
{
if (formCollection["Sortby"] == "ProductID")
{
OrderBy(productViewModel, formCollection, "m.ProductID");
}
else if (formCollection["Sortby"] == "Code")
{
OrderBy(productViewModel, formCollection, "m.Code");
}
else if (formCollection["Sortby"] == "Name")
{
OrderBy(productViewModel, formCollection, "m.Name");
}
else if (formCollection["Sortby"] == "Price")
{
OrderBy(productViewModel, formCollection, "m.Price");
}
}
else if (submit == "Delete all selected")
{
}
else if (submit == "Update All")
{
}
else if (submit == "Restrict Display")
{
}
}
return View(productViewModel);
}
VIEW:
#model Admin.Models.ViewModels.ProductListViewModel
#{
ViewBag.Title = "View Products";
}
#using (Html.BeginForm())
{
<h2>Product List as at #DateTime.Now.ToString("dd/MM/yyyy")</h2>
<table>
<tr>
<td>Sort by:</td>
<td>
<select name="Sortby">
<option value="ProductID">ProductID</option>
<option value="Code">Code</option>
<option value="Name">Name</option>
<option value="Price">Price</option>
</select>
</td>
<td>
<input type="radio" name="sortDirection" checked="checked" value="Asc" /> Ascending
<input type="radio" name="sortDirection" value="Desc" /> Descending
</td>
<td>
<input type="submit" name="submit" value="Change Sort" />
</td>
</tr>
<tr>
<td>Display only : (category)</td>
<td>#Html.DropDownList("CategoryID", new SelectList(Model.CategoryItems, "CategoryID", "Name"), "All Categories")</td>
<td colspan="2"><input type="submit" name="submit" value="Restrict Display" /></td>
</tr>
<tr>
<td colspan="4"><br />Total Number of products: #Model.ProductItems.Count()</td>
</tr>
</table>
<table>
<tr>
<th>
Edit
</th>
<th>
Code
</th>
<th>
Name
</th>
<th>
Price
</th>
<th>
On_Sale
</th>
<th>
DiscountedPrice
</th>
<th>
Weight
</th>
<th>
Delete
</th>
<th></th>
</tr>
#for (var i = 0; i < Model.ProductItems.ToList().Count; i++)
{
<tr>
<td>
#Html.HiddenFor(m => m.ProductItems.ToList()[i].ProductID)
#Html.ActionLink(Model.ProductItems.ToList()[i].ProductID.ToString(), "ProductEdit", new { id = Model.ProductItems.ToList()[i].ProductID })
</td>
<td>
#Html.DisplayFor(m => m.ProductItems.ToList()[i].Code)
</td>
<td>
#Html.DisplayFor(m => m.ProductItems.ToList()[i].Name)
</td>
<td>
#Html.EditorFor(m => m.ProductItems.ToList()[i].Price)
</td>
<td>
#Html.CheckBoxFor(m => m.ProductItems.ToList()[i].On_Sale, new { id = "On_Sale_" + Model.ProductItems.ToList()[i].ProductID })
</td>
<td>
#Html.EditorFor(m => m.ProductItems.ToList()[i].DiscountedPrice)
</td>
<td>
#Html.EditorFor(m => m.ProductItems.ToList()[i].Weight)
</td>
<td>
#Html.CheckBoxFor(m => m.ProductItems.ToList()[i].IsForDelete, new { id = Model.ProductItems.ToList()[i].ProductID })
</td>
<td>
#Html.ActionLink("Edit", "ProductEdit", new { id = Model.ProductItems.ToList()[i].ProductID }) |
#Html.ActionLink("Details", "Details", new { id = Model.ProductItems.ToList()[i].ProductID }) |
#Html.ActionLink("Delete", "Delete", new { id = Model.ProductItems.ToList()[i].ProductID })
</td>
</tr>
}
</table>
<p>
<input name="submit" type="submit" value="Delete all selected" />
</p>
<p>
<input name="submit" type="submit" value="Update All" />
</p>
<p>
#Html.ActionLink("Add a new product", "ProductAdd")
</p>
}
In the post action, the productViewModel argument has the ProductItems and CategoryItems properties as null.
Ok, so there are two problems.
I don't understand why you want to post list of the CategoryItems You should only expect the selected category and not the list
The problem with ProductItems is the name generated for <input> tags. Currently, the name being generated is name="[0].Price" whereas it should have been name="ProductItems[0].Price"
I changed the following code
#Html.EditorFor(m => m.ProductItems.ToList()[i].Price)
to
#Html.EditorFor(m => m.ProductItems[i].Price)
and it worked.
Note: I changed IEnumerable<ProductItem> ProductItems to List<ProductItem> ProductItems in ProductListViewModel
Yes it will be null on post back. I have a similar table in one of my projects and this is what I would have done given my situation.
You could change your view model to look like this then you don't have to do so many converting to lists in your view:
public class ProductListViewModel
{
public List<ProductItem> ProductItems { get; set; }
public List<Category> CategoryItems { get; set; }
}
Now in you view it could look something like this (this is just part of it, then rest you can just go and add):
#for (int i = 0; i < Model.ProductItems.Count(); i++)
{
<tr>
<td>
#Html.DisplayFor(m => m.ProductItems[i].Name)
#Html.HiddenFor(m => m.ProductItems[i].Name)
</td>
</tr>
<tr>
<td>
#Html.CheckBoxFor(m => m.ProductItems[i].IsForDelete)
</td>
</tr>
}
Add the code and do some debugging to see how the values are returned on submit
I hope this helps.
I am trying to get my view to post a List back to the action however it keeps coming in as null.
So my Model has a List of WeightEntry objects.
Exercise Model
public class Exercise
{
public List<WeightEntry> Entries { get; set; }
public int ExerciseID { get; set; }
public int ExerciseName { get; set; }
}
WeightEntry Model
public class WeightEntry
{
public int ID { get; set; }
public int Weight { get; set; }
public int Repetition { get; set; }
}
My View contains the ExerciseName and a forloop of WeightEntry objects
#model Mymvc.ViewModels.Exercise
...
<span>#Model.ExerciseName</span>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<table class="left weight-record">
<tr>
<th>Reps</th>
<th>Weight</th>
</tr>
#foreach (var item in Model.Entries)
{
<tr>
<td>
#Html.EditorFor(x => item.Repetition)
</td>
<td>
#Html.EditorFor(x => item.Weight)
</td>
</tr>
}
</table>
<input type="submit" value="Save" />
}
The Controller Action (Post) Does nothing at the moment. I am just trying to get the binding working before I add the save code.
[HttpPost]
public ActionResult WeightEntry(Exercise exercise)
{
try
{
//Add code here to save and check isvalid
return View(exercise);
}
catch
{
return View(exercise);
}
}
I have seen a few little tricks with adding a numerator to the form elements' names used in MVC2 but I was wondering if MVC3 was any different? I was hoping it would all bind nicely with ID's being 0 or null but instead the whole List is null when I inspect it after the form posts. Any help is appreciated.
Thanks.
Replace the following loop:
#foreach (var item in Model.Entries)
{
<tr>
<td>
#Html.EditorFor(x => item.Repetition)
</td>
<td>
#Html.EditorFor(x => item.Weight)
</td>
</tr>
}
with:
#for (var i = 0; i < Model.Entries.Count; i++)
{
<tr>
<td>
#Html.EditorFor(x => x.Entries[i].Repetition)
</td>
<td>
#Html.EditorFor(x => x.Entries[i].Weight)
</td>
</tr>
}
or even better, use editor templates and replace the loop with:
#Html.EditorFor(x => x.Entries)
and then define a custom editor template that will automatically be rendered for each element of the Entries collection (~/Views/Shared/EditorTemplates/WeightEntry.cshtml):
#model WeightEntry
<tr>
<td>
#Html.EditorFor(x => x.Repetition)
</td>
<td>
#Html.EditorFor(x => x.Weight)
</td>
</tr>
The the generated input elements will have correct names and you will be able to successfully fetch them back in your POST action.