FluentValidation Improperly Validating Model from DropDown - c#

I have the following two models (stripped to relevant parts):
Models\Department.cs:
public class DepartmentValidator : AbstractValidator<Department> {
public DepartmentValidator() {
RuleFor(d => d.Name)
.NotEmpty().WithMessage("You must specify a name.")
.Length(0, 256).WithMessage("The name cannot exceed 256 characters in length.");
}
}
[Validator(typeof(DepartmentValidator))]
public class Department {
public int Id { get; set; }
[Column(TypeName = "nvarchar")]
[MaxLength(256)]
public string Name { get; set; }
}
Models\FacultyMember.cs:
public class FacultyValidator : AbstractValidator<FacultyMember> {
public FacultyValidator() {
RuleFor(f => f.Name)
.NotEmpty().WithMessage("You must specify a name.")
.Length(0, 64).WithMessage("The name cannot exceed 64 characters in length.");
}
}
[Validator(typeof(FacultyValidator))]
public class FacultyMember {
public int Id { get; set; }
[Column(TypeName = "nvarchar")]
[MaxLength(64)]
public string Name { get; set; }
public virtual ICollection<Department> Departments { get; set; }
public FacultyMember() {
Departments = new HashSet<Department>();
}
}
I have the following controller code:
Controllers\FacultyController.cs:
// GET: Faculty/Create
public ActionResult Create() {
// Get Departments.
var departmentList = db.Departments.ToList().Select(department => new SelectListItem {
Value = department.Id.ToString(),
Text = department.Name
}).ToList();
ViewBag.DepartmentList = departmentList;
var facultyMember = new FacultyMember();
facultyMember.Departments.Add(new Department()); // Create a single dropdown for a department to start out.
return View(facultyMember);
}
// POST: Faculty/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,Departments")] FacultyMember facultyMember) {
// Get Departments.
var departmentList = db.Departments.ToList().Select(department => new SelectListItem {
Value = department.Id.ToString(),
Text = department.Name
}).ToList();
ViewBag.DepartmentList = departmentList;
if (!ModelState.IsValid) { // Problem here...
return View(facultyMember);
}
db.Faculty.Add(facultyMember);
db.SaveChanges();
return RedirectToAction("Index");
}
Views\Faculty\Create.cshtml:
...
<div class="form-group">
#Html.LabelFor(model => model.Departments, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Departments, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Departments, "", new { #class = "text-danger" })
</div>
</div>
...
Views\Shared\EditorTemplates\Department.cshtml:
#model MyProject.Models.Department
#Html.DropDownListFor(model => model.Id, ViewBag.DepartmentList as IEnumerable<SelectListItem>, "Select...", new { #class = "form-control" })
So, when I navigate to the create faculty page, everything displays properly; the 'Departments' field has a dropdown list with the departments in my database. However, upon submitting the form, my model state is invalid (see comment in code above). Upon further inspection, it seems that FluentValidation is spitting out an error because my "Name" field is null. That's exactly what it should do when I'm creating/editing departments, but for this dropdown in faculty members, it shouldn't be validating the entire department, should it? The only thing the dropdown is sending back is the Id, as I've specified.
The only thing that this dropdown sends is the Id of the department, which is properly received. So, what do I need to do to make this work? My goal is to have a dynamic set of dropdown lists, each populated with existing departments in the database. Similar to this example.
Please let me know if anything else needs explaining.

The solution, as explained by Stephen Muecke, was to create a view model to represent all data I wanted to pass to the form and back.
ViewModel\FacultyMemberViewModel.cs:
public class FacultyMemberViewModelValidator : AbstractValidator<FacultyMemberViewModel> {
public FacultyMemberViewModelValidator() {
RuleFor(f => f.Name)
.NotEmpty().WithMessage("You must specify a name.")
.Length(0, 64).WithMessage("The name cannot exceed 64 characters in length.");
RuleFor(s => s.SelectedDepartments)
.NotEmpty().WithMessage("You must specify at least one department.")
}
}
[Validator(typeof(FacultyMemberViewModelValidator))]
public class FacultyMemberViewModel {
public int Id { get; set; }
public string Name { get; set; }
public int[] SelectedDepartments { get; set; }
[DisplayName("Departments")]
public IEnumerable<SelectListItem> DepartmentList { get; set; }
}
Views\Faculty\Create.cshtml:
...
<div class="form-group">
#Html.LabelFor(model => model.DepartmentList, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.ListBoxFor(model => model.SelectedDepartments, Model.DepartmentList, new { #class = "form-control" }) #Html.ValidationMessageFor(model => model.SelectedDepartments, "", new { #class = "text-danger" })
</div>
</div>
...
Controllers\FacultyController.cs:
// GET: Faculty/Create
public ActionResult Create() {
var facultyMemberViewModel = new FacultyMemberViewModel {
DepartmentList = GetDepartmentList()
};
return View(facultyMemberViewModel);
}
// POST: Faculty/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,SelectedDepartments,DepartmentList")] FacultyMemberViewModel facultyMemberViewModel) {
if (!ModelState.IsValid) {
// Re-set the Department list.
if (facultyMemberViewModel.DepartmentList == null) {
facultyMemberViewModel.DepartmentList = GetDepartmentList();
}
return View(facultyMemberViewModel);
}
var facultyMember = new FacultyMember {
Id = facultyMemberViewModel.Id,
Name = facultyMemberViewModel.Name,
};
foreach (var departmentId in facultyMemberViewModel.SelectedDepartments) {
// I'm assuming this is safe to do (aka the records exist in the database)...
facultyMember.Departments.Add(db.Departments.Find(departmentId));
}
db.Faculty.Add(facultyMember);
db.SaveChanges();
return RedirectToAction("Index");
}

Related

Create Dropdown field form from List with ASP.NET MVC

During the next time, I could create some posts because I'm learning C# and ASP.NET MVC. I'm coming from Pythonic world, so some things are not clear for me.
I would like to generate a List of strings, then I would like to display this list in my form as a DropDownList.
This is my model:
public class Joueur
{
public int ID { get; set; }
[Required, Display(Name = "Nom"), StringLength(30)]
public string Lastname { get; set; }
[Required, Display(Name = "Prénom"), StringLength(30)]
public string Firstname { get; set; }
[Required, StringLength(15)]
public string Poste { get; set; }
public string Image { get; set; }
}
This is my controller according to Create Method:
// GET: Joueurs/Create
public ActionResult Create()
{
List<Strings> posteList = new List<SelectListItem>{ "Gardien", "Défenseur", "Milieu", "Attaquant" };
ViewBag.PosteList = posteList;
return View();
}
And this is my view:
<div class="col-md-10">
#*ViewBag.PosteList is holding all the postes values*#
#Html.DropDownListFor(model => model.Poste, ViewBag.PosteList as SelectList, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Poste, "", new { #class = "text-danger" })
</div>
But I get this issue:
#Html.DropDownListFor(model => model.Poste, ViewBag.PosteList as SelectList, new { #class = "form-control" })
There is no ViewData element of type « IEnumerable » with the key « Poste ».
How I could do that ?
With Django, it's pretty easy, in my model I create a dictionary and I pass this dict in the property, but with C# ASP.NET? I don't find a way to do that.
I assume your view will display a form to represent the Joueur object that you want the user to fill out, and your ViewBag.PosteList will have the values that the user can select from for the Joueur.Poste property. In order to accomplish this, you should create a new/empty Joueur object in your Create controller method and pass it to the view like so:
public ActionResult Create()
{
var model = new Joueur();
List<Strings> posteList = new List<SelectListItem>{ "Gardien", "Défenseur", "Milieu", "Attaquant" };
ViewBag.PosteList = posteList;
return View(model);
}
Then the rest of your original code should work.
I found a solution, hopefully it's a good way:
In my model I created an Enum:
public class Joueur
{
public int ID { get; set; }
[Required, Display(Name = "Nom"), StringLength(30)]
public string Lastname { get; set; }
[Required, Display(Name = "Prénom"), StringLength(30)]
public string Firstname { get; set; }
[Required, StringLength(15)]
public Position Poste { get; set; }
public string Image { get; set; }
}
public enum Position
{
Gardien,
Défenseur,
Milieu,
Attaquant
}
And in my view I added:
<div class="form-group">
#Html.LabelFor(model => model.Poste, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(m => m.Poste, new SelectList(Enum.GetValues(typeof(FCSL.Models.Joueur.Position))), "Sélectionner le poste", new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Poste, "", new { #class = "text-danger" })
</div>
</div>
And I applied migration commands. It seems to work now.
#Html.DropDownList("Poste", new SelectList( ViewBag.PosteList, "id", "Poste"))
and in controller
public ActionResult Create()
{
List<Strings> posteList = new List<SelectListItem>{ "Gardien", "Défenseur", "Milieu", "Attaquant" };
ViewBag.PosteList = posteList;
return View(ViewBag.PosteList); // return viewbag
}

ASP.NET MVC 5 & Entity Framework many to many relationship

I have a many-to-many relationship between User and Task model. I want to assign tasks to users when I am creating a new task. Because of that I created UserViewModel and CheckBoxView model. But now I can just assign tasks to users when I am editing a user table. I know I have to change UserController's Create action but I do not know how.
Here is my code:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public virtual ICollection<UserToTask> UserToTasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<UserToTask> UserToTasks { get; set; }
}
public class UserToTask
{
public int Id { get; set; }
public int UserId { get; set; }
public int TaskId { get; set; }
public virtual User User { get; set; }
public virtual Task Task { get; set; }
}
public class CheckBoxViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool Checked { get; set; }
}
public class UserViewModel
{
public int UserId { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public List<CheckBoxViewModel> Tasks { get; set; }
}
User Controller:
public class UsersController : Controller
{
private MyModel db = new MyModel();
// GET: Users
public ActionResult Index()
{
return View(db.Users.ToList());
}
// GET: Users/Create
public ActionResult Create()
{
return View();
}
// POST: Users/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,Name,Surname")] User user)
{
if (ModelState.IsValid)
{
db.Users.Add(user);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(user);
}
// GET: Users/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
User user = db.Users.Find(id);
if (user == null)
{
return HttpNotFound();
}
var result = from a in db.Tasks
select new
{
a.Id,
a.Title,
Checked = (from ab in db.UserToTasks
where (ab.UserId == id) & (ab.TaskId == a.Id)
select ab).Any()
};
var MyViewModel = new UserViewModel();
MyViewModel.UserId = id.Value;
MyViewModel.Name = user.Name;
MyViewModel.Surname = user.Surname;
var MyCheckBoxList = new List<CheckBoxViewModel>();
foreach (var item in result)
{
MyCheckBoxList.Add(new CheckBoxViewModel{Id = item.Id, Name = item.Title, Checked = item.Checked});
}
MyViewModel.Tasks = MyCheckBoxList;
return View(MyViewModel);
}
// POST: Users/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(UserViewModel user)
{
if (ModelState.IsValid)
{
var MyUser = db.Users.Find(user.UserId);
MyUser.Name = user.Name;
MyUser.Surname = user.Surname;
foreach (var item in db.UserToTasks)
{
if (item.UserId == user.UserId)
{
db.Entry(item).State = EntityState.Deleted;
}
}
foreach (var item in user.Tasks)
{
if (item.Checked)
{
db.UserToTasks.Add(new UserToTask() {UserId = user.UserId, TaskId = item.Id});
}
}
db.SaveChanges();
return RedirectToAction("Index");
}
return View(user);
}
}
Edit view:
#model ItalianCoach.Models.UserViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>User</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.UserId)
<div class="form-group">
#Html.LabelFor(model => model.Name, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Name, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Surname, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Surname, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Surname, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Tasks, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#for (int i = 0; i < Model.Tasks.Count(); i++)
{
#Html.EditorFor(m => m.Tasks[i].Checked)
#Html.DisplayFor(m => m.Tasks[i].Name)<br/>
#Html.HiddenFor(m => m.Tasks[i].Name)
#Html.HiddenFor(m => m.Tasks[i].Id)
}
</div>
</div>
</div>
}
You do not need the UserToTask class. The relationships between them should be with the object classes themselves.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public int TaskId { get; set; }
public virtual Task Task { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public string Title { get; set; }
}
You add in the TaskId as foreign key in the User class. Each User will have a collection of Tasks (which may or may not be empty - so be careful with your constructors).
To find all the Users for a particular Task - you use a database query on Users -> where TaskId == Task.Id.
You'll need to sort out the xaml, controllers, etc, to fit the new model.

c# MVC - Populate DropDownList from class property

I'm working with EF and MVC, new stuff to me.
I have the following class:
public class Client
{
[Key]
public int ClientId { get; set; }
public Category Category { get; set; }
}
and I want to show in the View related to the "Create" ActionResult, a selectable DropDownList with all the categories to choose one.
I have a ViewModel also:
public class CategoryViewModel
{
public int SelectedCategoryId { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
}
which I use in the controller:
private IEnumerable<SelectListItem> GetCategories()
{
var db = new MyDBContext();
var categories = db.Categories
.Select(x =>
new Category
{
CategoryId = x.CategoryId,
Name = x.Name
});
return new SelectList(categories, "Value", "Text");
}
...and in the Create():
public ActionResult Create()
{
var model = new CategoryViewModel();
model.Categories = GetCategories();
return View(model);
}
I dont know how to populate the dropdown in the view:
<div class="form-group">
#Html.LabelFor(model => model.Category, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(WHAT GOES HERE????)
</div>
</div>
Thanks for rescuing me! (RE5 quote)
There are different overloads but this is an option:
#Html.DropDownListFor(x => x.SelectedCategoryId, Model.Categories)

Populate a DropDownList with Users

I want to make my model to associate with a User from the Identity system in ASP.NET MVC 5, instead of an Employee. My problem is that I can't seem to figure out the magic behind #Html.DropDownList... I'm pulling my hair out. Here is my ViewModel:
public class TicketViewModel
{
[Display(Name="ID#")]
public int TicketId { get; set; }
public int EmployeeId { get; set; }
public int ShopId { get; set; }
public int UserId { get; set; }
public int ApplicationUserId { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public EmployeeViewModel Employee { get; set; }
public ShopViewModel Shop { get; set; }
public TotsUser User { get; set; }
}
and the TicketController method:
// GET: /Ticket/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Ticket ticket = db.Tickets.Find(id);
if (ticket == null)
{
return HttpNotFound();
}
TicketViewModel ticketVM = Mapper.Map(ticket, new TicketViewModel());
ViewBag.EmployeeId = new SelectList(db.Employees, "EmployeeId", "Name", ticketVM.EmployeeId);
ViewBag.ShopId = new SelectList(db.Shops, "ShopId", "Name", ticketVM.ShopId);
ViewBag.UserListing = new SelectList(db.Users, "Id", "UserName", ticketVM.UserId);
return View(ticketVM);
}
The view (the part in question)
<div class="form-group">
#Html.LabelFor(model => model.EmployeeId, "EmployeeId", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("EmployeeId", String.Empty)
#Html.ValidationMessageFor(model => model.EmployeeId)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.UserId, "UserId", new { #class = "control-label col-md-2" })
<div class="col-md-10">
#* #Html.DropDownList("UserId", String.Empty) *#
#Html.DropDownList("UserId", #ViewBag.UserListing, null);
#Html.ValidationMessageFor(model => model.UserId)
</div>
</div>
I can't even explain how it even works with EmployeeId. This is the way that the scaffold generated it for my original model. It populates EmployeeId just fine. Can someone please explain the dark magic going on with the #Html.DropDownList helper and explain why I can't get the DropDownList to populate with anything?
you can try to change
ViewBag.UserListing = new SelectList(db.Users, "Id", "UserName", ticketVM.UserId);
to
ViewBag.UserId = new SelectList(db.Users, "Id", "UserName", ticketVM.UserId);
or change
#Html.DropDownList("UserId", #ViewBag.UserListing, null);
to
#Html.DropDownList("UserId", (SelectList)#ViewBag.UserListing);
This is incorrect no matter which method you use
#Html.DropDownList("UserId", #ViewBag.UserListing, null);
You have to tell the helper what type of object #ViewBag.UserListing is. In your case it would be (SelectList)#ViewBag.UserListing

Convention for adding items to a collection property from a select list in the create portion of CRUD?

My main entity is the Recipe which contains a collection of Ingredient items as follows:
public class Recipe {
[Key]
public virtual int RecipeId { get; set; }
public string RecipeName { get; set; }
...
public virtual ApplicationUser LastModifiedBy { get; set; }
public virtual IList<Ingredient> Ingredients { get; set; }
}
public class Ingredient {
public virtual int IngredientId { get; set; }
[Display(Name = "Name")]
public string IngredientName { get; set; }
....
public virtual IList<Recipe> Recipes { get; set; }
}
Which is fine. Then my controller and view for creating a new Recipe are as follows:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "stuff to include")] Recipe recipe)
{
IList<int> ingredientIds = (ModelState.Values.ElementAt(1).Value.AttemptedValue).Split(',').Select(int.Parse).ToList(); //[1,2,3,4,5]
foreach (int id in ingredientIds) {
Ingredient ing = db.Ingredients.Where(i => i.IngredientId == id).FirstOrDefault() as Ingredient;
recipe.Ingredients.Add(ing);
}
db.Recipes.Add(recipe);
db.SaveChanges();
return RedirectToAction("Index");
ViewBag.Ingredients = new MultiSelectList(db.Ingredients,
"IngredientId", "IngredientName", string.Empty);
ViewBag.CreatedById = new SelectList(db.Users, "Id", "Email", recipe.CreatedById);
return View(recipe);
}
And the view:
#for (Int16 i = 0; i < 5; i++) {
<div class="form-group">
#Html.LabelFor(model => model.Ingredients, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("Ingredients", null, htmlAttributes: new { #class = "form-control" })
</div>
</div>
}
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="button" value="Add Ingredients" class="btn btn-default" />
</div>
</div>
So this sets ModelState.Values.ElementAt(1).Value.AttemptedValue = "1,3,5,4,5" where this is a list of id numbers. I know I can come in before the if (ModelState.IsValid) and iterate through the above and place it into recipe.Ingredients which is fine except...
It feels just so un ASP.NET MVC like, as if there's no way they could have thought of so much and not thought of this scenario? Am I missing something here? The ingredients list will be too long to make a multi select list any use.
You are creating arbitrary dropdownlists that all have the same id (invalid html) and name attribute that has no relationship to your model and wont bind on post back. You first need to create view models that represent what you want to display.
public class RecipeVM
{
[Required]
public string Name { get; set; }
[Display(Name = Ingredient)]
[Required]
public List<int?> SelectedIngredients { get; set; }
public SelectList IngredientList { get; set; }
}
Then in the controller
public ActionResult Create()
{
RecipeVM model = new RecipeVM();
// add 5 'null' ingredients for binding
model.SelectedIngredients = new List<int?>() { null, null, null, null, null };
ConfigureViewModel(model);
return View(model);
}
[HttpPost]
public ActionResult Create(RecipeVM model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
// Initialize new instance of your data model
// Map properties from view model to data model
// Add values for user, create date etc
// Save and redirect
}
private void ConfigureViewModel(RecipeVM model)
{
model.IngredientList = new SelectList(db.Ingredients, "IngredientId", "IngredientName");
}
View
#model RecipeVM
#using (Html.BeginForm())
{
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
#Html.ValidationMessageFor(m => m.Name)
for (int i = 0; i < Model.SelectedIngredients.Count; i++)
{
#Html.LabelFor(m => m.SelectedIngredients[i])
#Html.DropDownListFor(m => m.SelectedIngredients[i], Model.IngredientList, "-Please select-")
}
}
Note this is based on your current implementation of creating 5 dropdowns to select 5 ingredients. In reality you will want to dynamically add ingredients (start with none). The answers here and here give you a few options to consider.

Categories