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>
Related
I am building ASP.NET MVC project, All other posts about this topic did not help me. i have 2 models, Client and City.
public class Client
{
[Key]
public int Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public City City { get; set; }
}
public class City
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
And when i want to create a client a have an exception There is no ViewData item of type 'IEnumerable' that has the key 'City'.
This is my get and post method
private readonly ApplicationDbContext _context;
private List<City> _cities;
public ClientsController(ApplicationDbContext context)
{
_context = context;
}
// GET: Clients/Create
public IActionResult Create()
{
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View();
}
// POST: Clients/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Surname,Name,Patronymic,Telephone,City,Adress,SeriaNumberPassport,IdentificalCode")]
Client client)
{
if (ModelState.IsValid)
{
_context.Add(client);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(client);
}
And View code:
<div class="form-group" mb-3>
<label asp-for="City" class="control-label"></label>
#Html.DropDownListFor(model => model.City, ViewBag.Cities as SelectList, new { #class = "form-select" })
</div>
The data is displayed correctly, but I cannot create a client.
Think that the ModelState.IsValid is false, hence it returns to Create View rather than redirect to Index view (the flow for successful inserting Client).
While for the failure inserting case, you didn't provide the ViewBag.Cities value before returning to Create View (Check the Create method with [HttpPost]).
Talk about why the ModelState.IsValid was false, there is conflict in the type that you are passing CityId which is int type to City property with City type.
Updated:
Recommend creating and using the ViewModel class instead of Model. The main reason is to we can design the class in which the properties are only required for view, create/edit purposes.
Exposing the Model class is unsafe as the users will know how is your database entity looks like.
But the trouble with using ViewModel will be you need to map the property value from ViewModel to Model manually or with reflection. Of course, there are open-source libraries that can automate the mapping such as AutoMapper.
These are the steps you need to do for the fix:
Model
Add for CityId foreign key property.
public class Client
{
[Key]
public int Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public int CityId { get; set; } // Add this foreign key property
public City City { get; set; }
}
Note: If you use Entity Framework Code First approach, you need to create migration and update the database via command for this change.
ViewModel
Create ViewModel class with the properties require for Create Client.
public class CreateClientModel
{
public string Surname { get; set; }
public string Name { get; set; }
public int CityId { get; set; }
// Other properties that needed
}
View
2.1. Change the #model to CreateClientModel.
2.2. Use model.CityId instead of model.City. The CityId property is used to pass the selected city's Id.
#model CreateClientModel
<div class="form-group" mb-3>
<label asp-for="CityId" class="control-label"></label>
#Html.DropDownListFor(model => model.CityId, ViewBag.Cities as SelectList, new { #class = "form-select" })
</div>
Controller
3.1. Replace City with CityId in Bind attribute. (To add properties based on CreateClientModel).
3.2. Use CreateClientModel as request body.
3.3. Initialize the ViewBag.Cities value before returning to Create view for the ModelState.IsValid is false case.
public IActionResult Create()
{
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View(new CreateClientModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Surname,Name,CityId")]
ClientClientModel client)
{
if (ModelState.IsValid)
{
// Perform mapping from CreateClientModel to
_context.Add(new Client
{
Surname = client.Surname,
Name = client.Name,
CityId = client.CityId
// Other properties
});
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// Initialize ViewBag.Cities value
if (_context.City != null) _cities = _context.City.ToList();
ViewBag.Cities = new SelectList(_cities, "Id", "Name");
return View(client);
}
Suggested have a read and follow this tutorial which is similar to your scenario:
Tutorial: Update related data - ASP.NET MVC with EF Core
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.
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" })
I want to get dropdown list with values from table. I'm have model:
namespace MobileService.Models
{
public class Mobile
{
public int Id { get; set; }
public string Model { get; set;}
Public string EMEI { get; set ;}
public int MasterId { get; set; }
public List<Master> MasterList { get; set; }
}
public class Master
{
public int Id { get; set; }
public string Name { get; set; }
}
}
In controller I creat action for this model:
public ActionResult Create()
{
return View();
}
In view:
#model MobileService.Models.Mobile
#using (Html.BeginForm()) {
#Html.EditorFor(model => model.Model)
#Html.EditorFor(model => model.EMEI )
// And here i want to display a dropdown list with all available masters
<button type="submit" class="btn btn-primary">Creat</button>
}
But it always tell me that Model.MasterList is null . But why? In model i told to get list from another model (public List MasterList { get; set; }). Why is it null?
This code is in VB. In the beginning even i looked for a lot of time this is the simple solution i found.
Convert the model into list of selectListItems
Dim lstEmpTypes As New List(Of SelectListItem)
lstTicketWrTypes.Add(New SelectListItem With {.Text = row("Something"), .Value = row("Something")})
'Then load it into a ViewBag
ViewBag.EmpTypes = lstEmpTypes
'And in you View
#Html.DropDownList("selectWrType", New SelectList(ViewBag.EmpTypes , "Value", "Text"), New With {.class = "span3"})
When you query for Mobile you need to Include("MasterList"), or turn lazy loading on on your context (not recommended)
The answer for me was to add this to controller:
public ActionResult Create()
{
ViewBag.MasterId = new SelectList(db.Masters, "Id", "Name");
return View();
}
Then in view to show droplist:
#Html.DropDownList("MasterId", null, htmlAttributes: new { #class = "form-control" })
Thnx ZikO who let me now that list didn't loaded automatical.
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.