I have two models and I want to insert a row in the database with a foreign key relationship populated in the DropDownList. The Item model's data insert without problems but ManufacturerID does not get inserted (it inserts null). I could not find why.
Update: Uploaded the project to: http://mvcapplication2.codeplex.com/
<div class="editor-label">
#Html.LabelFor(model => model.ManufacturerID,"Manufacturer")
</div>
<div class="editor-field">
#Html.DropDownList("ManufacturerID",string.Empty)
#Html.ValidationMessageFor(model => model.ManufacturerID)
</div>
public class Item
{
public int ItemID { get; set; }
public string Serial { get; set; }
public string ItemName { get; set; }
public int? ManufacturerID { get; set; }
public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
public int ManufacturerID { get; set; }
public string ManufacturerName { get; set; }
public List<Item> Items { get; set; }
}
public ActionResult Create()
{
ViewBag.ManufacturerID = new SelectList(db.Manufacturers, "ManufacturerID", "ManufacturerName");
return View();
}
[HttpPost]
public ActionResult Create(Item ıtem)
{
if (ModelState.IsValid)
{
db.Items.Add(ıtem);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(ıtem);
}
I would prefer to NOT use the domain model in the view. I would create some view models specific to the view. Also, to transfer data from action method (ex : dropdown list), i would use a strongly typed approach, instead of the dynamic viewbag/ viewdata approach.
Create a view model class
public class CreateItemVM
{
public string SerialNumber { set;get;}
public int SelectedManufactureID { set;get;}
public List<SelectListItem> Manufacturers { set;get;}
public CreateItemVM()
{
Manufacturers =new List<SelectListItem>();
}
}
Now in your GET action method, create an object of our viewmodel, initialize the relevant values and send to the view.
public ActionResult Create()
{
var vm=new CreateItemVM();
vm.Manufacturers=GetManufacturerList();
return View(vm);
}
private List<SelectListItem> GetManufacturerList()
{
List<SelectListItem> manuList=new List<SelectListItem>();
manuList=(from p in db.Manufacturers
select new SelectListItem {
Value=p.ID.ToString(),
Text=p.Name}
).ToList();
return manuList;
}
Now in our view, which is strongly typed to our Viewmodel,
#model CreateItemVM
#using(Html.Beginform())
{
#Html.DropDownListfor(x=>x.SelectedManufactureID ,
Model.Manufacturers,"select")
<input type="submit" />
}
And finally in our POST action method, we will read values from our posted viewmodel and assign it as the property values of our domain object and save it. the selected manufacturers ID value will be in the SelectedManufactureID property of our viewmodel.
[HttpPost]
public ActionResult Create(CreateItemVM model)
{
if(ModelState.IsValid)
{
Item domainObject=new Item();
domainObject.ManufacturerID =model.SelectedManufactureID ;
//set other relevant properties also
db.Items.Add(ıtem);
db.SaveChanges();
return RedirectToAction("Index");
}
// reload the dropdown before returning to the view
model.Manufacturers=GetManufacturerList();
return View(model);
}
Try to make the relationship more explicit, making properties virtual and adding an attribute:
public class Item
{
...
[ForeignKey("Manufacturer")]
public int? ManufacturerID { get; set; }
public virtual Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
...
public virtual List<Item> Items { get; set; }
}
Edit:
And you can use a more tied way of building the drop down:
#Html.DropDownListfor(x=>x.SelectedManufactureID ,
ViewBag.ManufacturerID as SelectList,"Choose one")
Edit 2:
A better approach is to make a specific model for the view (called ViewModel) to represent data and build the view like #Shyju said.
Found the problem. The Viewbag I was sending to the view should be named as ManufacturerID, I was sending Manufacturers and somehow it was not matching although it populated the dropdown.
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
As a developer asp.net web site I've created project. And Scaffold it for controller creation with DataBase First approach.
There are two EF data model objects: Personal and PersonalDetails.
using System;
using System.Collections.Generic;
public partial class personalDetails
{
public int personID { get; set; }
public Nullable<bool> rehber { get; set; }
//many others, removed for easy to understand
}
public partial class personal
{
public personal()
{
this.passportDetails = new HashSet<passportDetails>();
}
public string name { get; set; }
public string surname { get; set; }
public int ID { get; set; }
public Nullable<short> politicalPartyID { get; set; }
public Nullable<short> familyStatusID { get; set; }
public virtual familyStatus familyStatus { get; set; }
public virtual politicalParties politicalParties { get; set; }
}
For a business logic it's necessary to create viewModel, because on view there are some fields from personal and some fields from personalDetils object. And I added viewModel class.
public class SocialViewModels
{
public personalDetails personalDetails { get; private set; }
public personal personal { get; private set; }
public SelectList marriageStatus { get; private set; }
public SelectList partyList { get; private set; }
public SocialViewModels()
{
}
public SocialViewModels(personalDetails _personalDetails, personal _personal, IEnumerable _marriageStatus, IEnumerable _partyList)
{
personalDetails = _personalDetails;
personal=_personal;
marriageStatus = new SelectList(_marriageStatus, "ID", "familyStatusName", personal.familyStatusID);
partyList = new SelectList(_partyList, "ID", "partyName", personal.politicalPartyID);
}
}
This ViewModel works well for get Details operation. Controller and View below:
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
personalDetails personalDetails = db.personalDetails.Find(id);
SocialViewModels svm = new SocialViewModels(personalDetails, personalDetails.personal, db.familyStatus, db.politicalParties);
if (personalDetails == null)
{
return HttpNotFound();
}
return View(svm);
}
Details view:
#model kadrws.ViewModels.SocialViewModels
#Html.DisplayFor(model => model.personal.familyStatus.familyStatusName)
#Html.DisplayFor(model => model.personal.politicalParties.partyName)
#Html.DisplayFor(model => model.personalDetails.rehber)
<p>
#Html.ActionLink("Redaktə et", "Edit", new { id = Model.personal.ID })
</p>
But problem with Edit post method:
// GET: personalDetails/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
personalDetails personalDetails = db.personalDetails.Find(id);
SocialViewModels svm = new SocialViewModels(personalDetails, personalDetails.personal, db.familyStatus, db.politicalParties);
if (personalDetails == null)
{
return HttpNotFound();
}
return View(svm);
}
// POST: personalDetails/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "personID,rehber, partyID, familyStatusID")] SocialViewModels scm )
{
if (ModelState.IsValid)
{
//do
}
ViewBag.personID = new SelectList(db.personal, "ID", "name", scm.personal.ID);
return View(scm);
}
Edit view :
#model kadrws.ViewModels.SocialViewModels
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.personalDetails.personID)
#Html.DropDownList("personal_familyStatusID", Model.marriageStatus)
#Html.DropDownList("personal_politicalPartyID", Model.partyList)
#Html.EditorFor(model => model.personalDetails.rehber)
<input type="submit" value="Save" class="btn btn-default" />
}
Problem is that, SocialViewModels is null.
I also search similar behaviors in Google. Dozen this like problems. Please, help to understand where is problem in this code...
Firstly, properties marriageStatus and partyList will be null because they are typeof SelectList (and you don't generate forms controls for each property of each SelectListItem in the collection - and nor should you). You do however generate <select> elements with names personal_familyStatusID and personal_politicalPartyID but your model does not include properties with those names.
Secondly, properties personal and personalDetails are null because you have include a [Bind] attribute which specifically excludes then (it would need to be Bind(Include = "personal, personalDetails")].
However your misunderstanding what a view model is and how to use it. It is not just a class that holds multiple instances of data models, it should contain only those properties you display/edit in the view. Based on the 4 controls you have included in your form, your view model should be
public class SocialViewModels
{
public int ID { get; set; }
public Nullable<bool> Rehber { get; set; }
public int MarriageStatus { get; set; }
public int Party { get; set; }
public SelectList MarriageStatusList { get; set; }
public SelectList partyList { get; set; }
}
Side note: Do not use private set on your SelectList properties. If you needed to return the view because ModelState was invalid, then you could never repopulate them
Then in the GET method, initialize a new instance of the view model, set its properties from the data models and return it to the view
public ActionResult Details(int? id)
{
....
personalDetails personalDetails = db.personalDetails.Find(id);
....
SocialViewModels model = new SocialViewModels()
{
ID = personalDetails.personID,
Rehber = personalDetails.rehber,
MarriageStatus = personalDetails.personal.familyStatusID,
....
MarriageStatusList = new SelectList(db.familyStatus, "ID", "familyStatusName"),
....
};
return View(model);
}
Side note: Your naming is extremely confusing (one minute its familystatus, then next its marriagestatus) so not sure if I have interpreted it correctly.
Then in the view
#model kadrws.ViewModels.SocialViewModels
#using (Html.BeginForm())
{
..
#Html.HiddenFor(m => m.ID)
#Html.DropDownListFor(m => m.MarriageStatus, Model.MarriageStatusList)
....
#Html.EditorFor(m=> m.Rehber)
<input type="submit" value="Save" class="btn btn-default" />
}
And in the POST method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SocialViewModels model)
{
if (!ModelState.IsValid)
{
// repopulate your select lists
return View(model);
}
// Get your data models, map the view model properties it
// Save and redirect
}
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 have a list of items from a dropdown list created from a custom property in the "ASP.NET Identity", "aspnetusers" table named "FullName". My goal is to select the name from the dropdown list and have the fullname and Id saved in my "Application" table creating a one-to-many relationship. The dropdown list is populating with the names, but isn't saving them into my application table.
Model:
public class HomeController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
public class Application
{
public int ApplicationId { get; set; }
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
*****Asp.net identity application user class that I have a custom full name property in*****
public virtual ApplicationUser ApplicationUser { get; set; }
}
Controller:
public ActionResult Index()
{
ViewBag.LoanOfficer = new SelectList(db.Users,"Id","FullName");
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index([Bind(Include = "Id,FullName,FirstName,LastName")] Application application)
{
if (ModelState.IsValid)
{
ViewBag.LoanOfficer = new SelectList(db.Users, "Id", "FullName", application.ApplicationUser);
db.Applications.Add(application);
db.SaveChanges();
return RedirectToAction("Thanks");
}
return View(application);
}
View:
#Html.DropDownList("LoanOfficer", "Select Loan Officer")
Your model does not have a property named LoanOfficer so it cant be bound. If you inspect Request.Form you will see the ID value of the selected User so you would then need to manually add that to Application before saving it. But you have a lot of issues with your code including the way you are using #Html.DropDownList(), the pointless use of [Bind(Include="...")] (your listing all 3 properties but they are all bound by default anyway, and you have included "FullName" which is not even a property of the model), and your recreating the SelectList if the model is valid (which is pointless since you immediately redirect, but not creating it if you return the view - which means nothing will be shown in the dropdownlist)
Create a view model to represent what you want to display and edit
public class ApplicationVM
{
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
[Required]
[DisplayName("Loan officer")]
public int? LoanOfficer { get; set; }
public SelectList LoanOfficerList { get; set; }
}
Controller
public ActionResult Create()
{
ApplicationVM model = new ApplicationVM();
model.LoanOfficerList = new SelectList(db.Users, "Id", "FullName");
return View();
}
[HttpPost]
public ActionResult Create(ApplicationVM model)
{
if(!ModelState.IsValid)
{
model.LoanOfficerList = new SelectList(db.Users, "Id", "FullName");
return View(model);
}
// Initialize new Application, set properties from the view model, save and redirect
}
View
#model ApplicationVM
#using(Html.BeginForm())
{
....
// controls for FirstName and LastName properties
#Html.LabelFor(m => m.LoanOfficer)
#Html.DropDownListFor(m => m.LoanOfficer, Model.LoanOfficerList, "--Please select--")
#Html.ValidationMessageFor(m => m.LoanOfficer)
<input type="submit" value="Create" />
}
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.