I have two autogenerated database models (Product and ProductDetails) which I merged into a ViewModel so I can edit all data at once.
What confuses me is the part where I am supposed to iterate through ICollection of Product_ProductCategoryAttributes (within ProductDetail model) inside a view to allow .NET automagically bind properties to the ViewModel. I have tried using for as well as foreach loop but without any success as controls are being created with wrong names (needed for auto binding).
Product model
public partial class Product
{
public Product()
{
this.ProductDetail = new HashSet<ProductDetail>();
}
public int idProduct { get; set; }
public int idProductCategory { get; set; }
public string EAN { get; set; }
public string UID { get; set; }
public bool Active { get; set; }
public virtual ProductCategory ProductCategory { get; set; }
public virtual ICollection<ProductDetail> ProductDetail { get; set; }
}
ProductDetail model
public partial class ProductDetail
{
public ProductDetail()
{
this.Product_ProductCategoryAttribute = new HashSet<Product_ProductCategoryAttribute>();
}
public int idProductDetail { get; set; }
public int idProductCategory { get; set; }
public int idMeta { get; set; }
public int idProduct { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual Meta Meta { get; set; }
public virtual Product Product { get; set; }
public virtual ICollection<Product_ProductCategoryAttribute> Product_ProductCategoryAttribute { get; set; }
public virtual ProductCategory ProductCategory { get; set; }
}
ProductViewModel - One product can have many ProductDetails
public class ProductViewModel
{
public Product Product { get; set; }
public List<ProductDetail> ProductDetails { get; set; }
}
View (some code is intentionally omitted)
#for (int i = 0; i < Model.ProductDetails.Count(); i++)
{
#Html.TextAreaFor(model => model.ProductDetails[i].Description, new { #class = "form-control", #rows = "3" })
#for (int j = 0; j < Model.ProductDetails[i].Product_ProductCategoryAttribute.Count(); j++)
{
#Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).idProductCategoryAttribute)
#Html.TextBoxFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).Value, new { #class = "form-control" })
}
}
All controls outside the second for loop are being named properly eg. ProductDetails[0].Description, however controls generated within the second for loop get their name by the property value which in this case are Value and idProductCategoryAttribute. If I'm not wrong one solution would be converting ICollection to IList, but having model autogenerated I don't think it would be the best option.
You can't use ElementAt() within the lambda within the HTML helpers. The name that will be generated will just be the name of the field without indexes which allows the posted values to be populated.
You should use the indexes to traverse all the way through your view model so that the names that are generated actually match up.
So this:
#Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).idProductCategoryAttribute)
Should be this, or similar:
#Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute[j].idProductCategoryAttribute)
As for changing your model from ICollection to IList, this will be fine as IList inherits from ICollection. But as you say it is auto generated, it would probably be ok if you were using code first entity framework or something like that.
The real solution is to map your incoming model (the view model) to the auto generated ICollection<> lists and back again, depending on whether you're posting or getting.
In the example below, we are taking the posted values and mapping them to the auto generated Product object and manipulating it.
///
/// ProductViewModel incoming model contains IList<> fields, and could be used as the view model for your page
///
[HttpPost]
public ActionResult Index(ProductViewModel requestModel)
{
// Create instance of the auto generated model (with ICollections)
var product = new Product();
// Map your incoming model to your auto generated model
foreach (var productDetailViewModel in requestModel)
{
product.ProductDetail.Add(new ProductDetail()
{
Product_ProductCategoryAttribute = productDetailViewModel.Product_ProductCategoryAttribute;
// Map other fields here
}
}
// Do something with your product
this.MyService.SaveProducts(product);
// Posted values will be retained and passed to view
// Or map the values back to your valid view model with `List<>` fields
// Or pass back the requestModel back to the view
return View();
}
ProductViewModel.cs
public class ProductViewModel
{
// This shouldn't be here, only fields that you need from Product should be here and mapped within your controller action
//public Product Product { get; set; }
// This should be a view model, used for the view only and not used as a database model too!
public List<ProductDetailViewModel> ProductDetails { get; set; }
}
If your model is ICollection<T> (and can't be changed to IList<T> or use in a for loop), then you need to use a custom EditorTemplate for typeof T
In /Views/Shared/EditorTemplates/Product_ProductCategoryAttribute.cshtml
#model yourAssembly.Product_ProductCategoryAttribute
#Html.HiddenFor(m => m.idProductCategoryAttribute)
#Html.TextBoxFor(m => m.Value, new { #class = "form-control" })
In /Views/Shared/EditorTemplates/ProductDetail.cshtml
#model yourAssembly.ProductDetail
#Html.TextAreaFor(m => m.Description, new { #class = "form-control", #rows = "3" })
#Html.EditorFor(m => m.Product_ProductCategoryAttribute)
In the main view
#model yourAssembly.ProductViewModel
#using (Html.BeginForm())
{
...
#Html.EditorFor(m => m.ProductDetails)
...
The EditorFor() method will recognize a collection (IEnumerable<T>) and will render each item in the collection using the corresponding EditorTemplate including adding the indexers in the controls name attributes so that the collection an be bound when you post.
The other advantage of a custom EditorTemplate for complex types is that they can be reused in other views. You can also create multiple EditorTemplate's for a type by locating them in the view folder associated with a controller, for example /Views/YourControllerName/EditorTemplates/ProductDetail.cshtml
Side note. In any case, you should be using view models for each type that includes only those properties you want to edit/display in the view.
Related
I'am trying to view information from two tables in a view with the view model, but it does not work.
This gives two tables with information.
public class HeatImage
{
[Key]
public int ImageId { get; set; }
public string name { get; set; }
}
public class HeatingArea
{
[Key]
public int ID { get; set; }
public string Area { get; set; }
}
My viewmodel
public class HeatingAreaViewModel
{
public IEnumerable<HeatImage> heatingImage { get; set; }
public IEnumerable<HeatingArea> heatingArea { get; set; }
}
My controller
public ActionResult Index()
{
HeatingAreaViewModel mymodel = new HeatingAreaViewModel();
mymodel.heatingArea = db.HeatingAreas.ToList();
mymodel.heatingImage = db.HeatImages.ToList();
return View(mymodel);
}
And my view
#model IEnumerable<Project.Models.HeatingAreaViewModel>
#foreach (var item in Model)
{
#item.heatingArea
}
Error
The model item passed into the dictionary is of type
'Project.Models.HeatingAreaViewModel', but this dictionary requires a
model item of type
'System.Collections.Generic.IEnumerable`1[Project.Models.HeatingAreaViewModel]'.
The error message tells it: the View expects IEnumerable of HeatingAreaViewModel, while your controller action sends to the view only HeatingAreaViewModel.
So either correct the controller to send a list of HeatingAreaViewModel, or correct the view to expect only a single instance of HeatingAreaViewModel.
You are sending through appels and expecting oranges, that is why you getting the error.
In your controller you are sending through a single (one) instance of HeatingAreaViewModel. In your view you are making provision to except a list of HeatingAreaViewModel instances (more than 1).
Reading through your replies you want to use both HeatImage and HeatingArea in each loop iteration. You will need to change your view model to accommodate this. For example, create a view model that can accommodate both:
public class HeatViewModel
{
public HeatImage HeatImage { get; set; }
public HeatingArea HeatingArea { get; set; }
}
You will pass this HeatViewModel as a list to your view.
public ActionResult Index()
{
// This is just dummy code
HeatingViewModel model1 = new HeatingAreaViewModel();
// Populate the properties
model1.HeatImage = new HeatImage();
// Populate the properties
model1.HeatingArea = new HeatingArea();
HeatingViewModel model2 = new HeatingAreaViewModel();
// Populate the properties
model2.HeatImage = new HeatImage();
// Populate the properties
model2.HeatingArea = new HeatingArea();
// Now pass this list to the view
List<HeatingViewModel> models = new List<HeatingViewModel>();
return View(models);
}
In your view your code would look something like this:
#model List<Project.Models.HeatingViewModel>
#foreach (var item in Model)
{
<p>item.HeatImage.Name</p>
<p>item.HeatingArea.ID (I have used ID because I don't know what your Area looks like)</p>
}
This way you have both objects in a single loop. You will just have to go and figure out how you are going to populate them, this is where the bulk of the work will be done.
I also noticed that you you start your properties in the lower case, best practices start them with caps. For example:
public HeatImage heatImage { get; set; }
...should be...
public HeatImage HeatImage { get; set; }
I hope this helps :)
Your view is expecting an IEnumerable of HeatingAreaViewModel and it seems as though your only passing a single instance.
Change this
#model IEnumerable<Project.Models.HeatingAreaViewModel>
to this
#model Project.Models.HeatingAreaViewModel
Then you'll be able to loop through the heatingArea and heatingImage properties like this
#foreach (var item in model.heatingArea)
{
//what ever you need to do here
}
If your problem is, how to iterate in one loop through the two lists of HeatingImage and HeatingArea, in your view, you have to redo your viewModel:
public class HeatingAreaViewModel
{
public HeatImage heatingImage { get; set; }
public HeatingArea heatingArea { get; set; }
}
and then the controller action:
public ActionResult Index()
{
heatingAreas = db.HeatingAreas.ToList();
heatingImages = db.HeatImages.ToList();
List<HeatingAreaViewModel> myModel = heatingAreas.Zip(heatingImages, (a, b) => new HeatingAreaViewModel {HeatingArea = a, HeatImage = b})
return View(mymodel);
}
Then the View will work, as it is.
However, I strongly advice against doing it this way. You would have to ensure, that corresponding elements in the two lists are in reality corresponding to each other. Which is very prone to errors. If there is an underlying logic that ties these two lists together (some relationship on database tables), I would use that one to join the lists/tables together, instead.
Simply your Model in the view doesn't matches what you sent from controller action, so I think you need to change your view to be like this:
#model HeatingAreaViewModel
#foreach (var item in Model.heatingImage)
{
#item.heatingArea
}
How can I bind a multi select dropdown list to a list property of a manually added intermediate table?
Classes
public class Department
{
public virtual int Id { get; set; }
public virtual IList<Lam> Lams { get; set; }
}
public class Person
{
public virtual int Id { get; set; }
public virtual IList<Lam> Lams { get; set; }
}
public class Lam
{
public virtual int Id { get; set; }
public virtual Department Department { get; set; }
public virtual Person Person { get; set; }
}
ViewModel
public class DepartmentCreateEditViewModel
{
public Department Department { get; set; }
public IList<Person> Persons { get; set; }
}
ActionResult
public ActionResult Create()
{
// Get all Persons
var persons = repositoryPerson.GetAll();
// Create ViewModel
var viewModel = new DepartmentCreateEditViewModel() { Department = new Department(), Persons = persons };
// Display View
return View(viewModel);
}
Create View
I tried to add a ListBox like this.
#Html.ListBoxFor(model => model.Department.Lams, new SelectList(Model.Persons, "Id", "Name"), new { #class = "form-controll" })
To save the object I want to get back a Department object
[HttpPost]
public ActionResult Create(Department department)
The binding from the dropdown (with persons) to the IList is not working. How am I supposed to do this? Is this even possible?
[Edit] Code after Erik's suggestion
Create ActionResult
[HttpPost]
public ActionResult Create(DepartmentCreateEditViewModel viewModelPostBack)
View
#Html.ListBoxFor(model => model.Department.Lams, new MultiSelectList(Model.Persons, "Id", "Name"), new { #class = "form-controll" })
What I get back:
viewModelPostBack
{EmpLaceMgmt.ViewModels.DepartmentCreateEditViewModel}
Department: {EmpLaceMgmt.Models.Department}
Persons: null
viewModelPostBack.Department
{EmpLaceMgmt.Models.Department}
Id: 0
Lams: Count = 0
The generated HTML looks like this
<select class="form-controll" id="Department_Lams" multiple="multiple" name="Department.Lams">
<option value="1">Example Person</option>
</select>
You have three problems. First, you are trying to bind to an IList<T>, but that won't work because the model binder won't know what kind of concrete object it should create to satisfy that... There are many objects that support IList<T>, so which one?
Secondly, You need to use a MultiSelectList and not a SelectList in your helper.
Third, you are posting back a different model type than you are using to create your pages. And that type has a very different structure. In the structure that you create your page with, your data is created with the naming of Department.Lams (because Department is a property of your ViewModel) but in your Post action takes a Department model which, the binder would be looking for an object simply called Lams, not Department.Lams.
So, convert your models to use a concrete type, such as List<Lam>, then post back to your ViewModel rather than Department, and extract the department out of the ViewModel, and finally change your helper to this:
#Html.ListBoxFor(model => model.Department.Lams,
new MultiSelectList(Model.Persons, "Id", "Name"), new { #class = "form-controll" })
Which I'd expect to be implemented in .NET MVC, but trying to figure out how to actually do it. Currently on my ViewModel, I have (for example):
public class GroupPolicyViewModel
{
public int PolicyId { get; set; }
public int HistoryId{ get; set; }
public SelectList ProductList { get; set; } // tried this
public List<Product> ProductList1 { get; set; } // tried this
}
Whenever I try and auto-generate my View from this ViewModel, the ProductList gets ignored. Is there any way to auto-generate a DropDownList at all from the ViewModel?
With model
public class GroupPolicyViewModel
{
public int PolicyId { get; set; }
public int HistoryId{ get; set; }
public int SelectedProductId{ get; set; }
public List<Product> ProductList { get; set; }
}
You can create DropDownList
#Html.DropDownListFor(m => m.SelectedProductId,
new SelectList(Model.ProductList, "ProductId", "ProductName"))
Or if you have SelectList of products in your model
#Html.DropDownListFor(m => m.SelectedProductId, Model.ProductSelectList)
If you want some generated code, you need to use scaffolding option with providing data context class. Here is nice tutorial MVC Music Store
You can (from VS2010) when creating a new Controller and using Entity Framework. Specify in the wizard to include Entity Framework and Read/Write ops and the wizard will create both the controller and the views.
It'll generate code like this [there is more] in the controller:
public ActionResult Create()
{
ViewBag.CostCentre_ID = new SelectList(db.CostCentres, "ID", "Name");
ViewBag.Location_ID = new SelectList(db.Locations, "ID", "Name");
ViewBag.User_ID = new SelectList(db.UCMUsers, "User_ID", "EmployeeNo");
return View();
}
and this in the view:
<div class="editor-field">
#Html.DropDownList("User_ID", String.Empty)
#Html.ValidationMessageFor(model => model.User_ID)
</div>
I'm new to MVC and I use MVC4 and I'm new to C#. I want to retrieve data from two tables: tblProduct and tblCategory in one View. In that View I want to get from tblCategory the column "Name" and from tblProduct all the columns.
I've defined my tables in code first in class tables.cs:
public class tblCategory
{
//Primary Key
[Key]
[ScaffoldColumn(false)]
public int CategoryId { get; set; }
[MaxLength(160)]
public string Name { get; set; }
etc...
}
public class tblProduct {
//Primary Key
[Key]
[ScaffoldColumn(false)]
public int ProductId { get; set; }
//Foreign Key
public int CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual tblCategory tblCategorys { get; set; }
[MaxLength(500)]
public string MainImageFileName { get; set; }
[MaxLength(160)]
public string Name { get; set; }
ect...
}
My model class, bar.cs:
Namespace xx.Models
public class bar {
public tblProduct product { get; set; }
public tblCategory category { get; set; }
}
How do I define the Index class in the Controller right? So that I can send data from model bar into the View.
public ActionResult Index(){
//How to define this?
}
And how should build up the View?
#model xx.Models.bar
But I want to use a Foreach loop in my View for all the columns from tblProduct.
And one column from tblCategory.
Can somebody help me with this? Thanks!
Your Index action should build an instance of your bar class
public ActionResult Index(){
var b = new bar();
b.product = some loading code;
b.category= some loading code;
return View(b);
}
your Index.cshtml need to expect an instance of that same model
#model ProjectName.xx.Models.bar
<div class="editor-label">
#Html.LabelFor(model => model.category.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.category.Name)
#Html.ValidationMessageFor(model => model.category.Name)
</div>
#* just duplicate this once for each property you want from product *#
<div class="editor-label">
#Html.LabelFor(model => model.product.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.product.Name)
#Html.ValidationMessageFor(model => model.product.Name)
</div>
i wouldn't use a for loop on the properties in product, because then you need reflection, and i doubt that page will function very fast.
I think both of this classes should be model cause clearly they are representation of tables in database. However class Bar i think it's not. I think your structure should be organized like this: product(model class), category(model class) = bar(model collection class). So instead of adding both classes as properties of class Bar you should define dictionary that will contain those class but tie those class with common interface i.e. IViewModel.
public interface IViewModel{
string name {get;set;}
}
public class TblProduct: IVievModel{
/// your code
}
public class TblCategory: IVievModel{
/// your code
}
public class Bar{
private Dictionary<string, IViewModel> viewModels = new Dictionary<string,IViewModel>();
}
Now in your ActionResult method you will just add those two classes to dictionary and return this class Bar to your view.
Hi I'm struggling to find the correct approach on SO for what I am currently doing, so I thought I would ask.
Here is my simplified code:
The entities are nested types based on using them with EF CodeFirst and the ViewModel is being mapped with AutoMapper.
When posting the form the ModelState is not valid due to the dropdownlist being mapped to model.CourseId and displaying my Course data.. i.e. CourseId = 2, CourseList = Null, but also having the [Required] attribute, really only CourseId is required but I also needed a relevant error message.
I then thought that in my Create GET & POST actions the view should probably just have the CourseId but I still need to display it as a dropdown and populate it and I was unsure as how to do that correctly.
I may also not be understanding how this should be used correctly and if I even need CourseName, i.e. since the Course already exists in the database I just want a foreign key to it, which will still let me show the selected course.
I'm also planning to break out all this mapping and data setting in my controller actions into a separate service layer but at the moment its a small prototype.
// Entities
public class Recipe {
public int Id { get; set; }
public string Name { get; set; }
public Course Course { get; set; }
}
public class Course {
public int Id { get; set; }
public string Name { get; set; }
}
// View Model
public class RecipeCreateViewModel {
// Recipe properties
public int Id { get; set; }
public string Name { get; set; }
// Course properties, as primitives via AutoMapper
public int CourseId { get; set; }
public string CourseName { get; set; }
// For a drop down list of courses
[Required(ErrorMessage = "Please select a Course.")]
public SelectList CourseList { get; set; }
}
// Part of my View
#model EatRateShare.WebUI.ViewModels.RecipeCreateViewModel
...
<div class="editor-label">
Course
</div>
<div class="editor-field">
#* The first param for DropDownListFor will make sure the relevant property is selected *#
#Html.DropDownListFor(model => model.CourseId, Model.CourseList, "Choose...")
#Html.ValidationMessageFor(model => model.CourseId)
</div>
...
// Controller actions
public ActionResult Create() {
// map the Recipe to its View Model
var recipeCreateViewModel = Mapper.Map<Recipe, RecipeCreateViewModel>(new Recipe());
recipeCreateViewModel.CourseList = new SelectList(courseRepository.All, "Id", "Name");
return View(recipeCreateViewModel);
}
[HttpPost]
public ActionResult Create(RecipeCreateViewModel recipe) {
if (ModelState.IsValid) {
var recipeEntity = Mapper.Map<RecipeCreateViewModel, Recipe>(recipe);
recipeRepository.InsertOrUpdate(recipeEntity);
recipeRepository.Save();
return RedirectToAction("Index");
} else {
recipe.CourseList = new SelectList(courseRepository.All, "Id", "Name");
return View(recipe);
}
}
I fixed my particular problem just by doing the below.
[Required(ErrorMessage = "Please select a Course.")]
public int CourseId { get; set; }
// public string CourseName { get; set; }
public SelectList CourseList { get; set; }
The view will use the DropDownListFor helper to map the drop down to my CourseId and that's all I really needed.
On to another problem now with AutoMapper and why it is not mapping back to the Recipe entity in the POST Create action.
I probably first need to find a way to store the relevant Course name in the "CourseName" property.