ASP.NET 4.5, MVC 5, EF6 code first
I'm a newbie and probably asking something long-known but I couldn't find solution on the web, probably because I don't know correct terminology to formulate this question.
To simplify things, let's say I have two model classes Teacher and Kid; One kid can be assigned only to one teacher, but one teacher can have many kids. As I'm using code first, my database is constructed from these model classes:
public class Kid
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual Teacher { get; set; }
}
public class Teacher
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual ICollection<Kid> Kids { get; set; }
}
Now, I need to have a view for adding new kid with: Textbox for Kid's name; Dropdown with list of Teachers
So, I'm creating a data transfer object, specifically for that view:
public class AddNewKidViewDTO
{
public IEnumerable<SelectListItem> Teachers { get; set; }
public int SelectedTeacherId { get; set; }
public Kid Kid { get; set; }
}
I also have a method for populating IEnumerable Teachers:
public AddNewKidViewDTO LoadTeachersForDropDownList()
{
... //get the list of Teachers
AddNewKidViewDTO addNewKidViewDTO = new AddNewKidViewDTO();
List<SelectListItem> selectListItems = new List<SelectListItem>();
foreach (teacher in Teachers)
{
selectListItems.Add (new SelectListItem
{
Text = teacher.Name.ToString(),
Value = teacher.Id.ToString()
});
}
addNewKidViewDTO.Teachers = selectListItems;
return addNewKidViewDTO;
}
and in the view AddNewKid.cshtml
<form>
@Html.LabelFor(model => model.Kid.Name)
@Html.TextBoxFor(model => model.Kid.Name, new {id ="Name"}
<br/>
@Html.LabelFor(model => model.Kid.Teacher)
@Html.DropDownListFor(model => model.SelectedTeacherId, Model.Teachers)
</form>
Form gets submitted and in the controller I get my populated AddNewKidViewDTO model:
[HttpPost]
public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
{
if (ModelState.IsValid)
{
//here is where the problem comes
}
}
ModelState.IsValid in my case will always return false. Because when it starts validating AddNewKidViewDTO.Kid, Teacher is compulsory field but in my addNewKidViewDTO model it's null. I have the necessary teacher Id contained in addNewKidViewDTO.SelectedTeacherId only.
My question is, what is an elegant way to validate my model before passing to my inner business logic methods?
Any help is appreciated.
There are multiple possible solutions:
Changing your AddNewKidViewDTO
and decorating it with the DataAnnotaions
for validation:
public class AddNewKidViewDTO
{
public IEnumerable<SelectListItem> Teachers { get; set; }
[Range(1, 2147483647)] //Int32 max value but you may change it
public int SelectedTeacherId { get; set; }
[Required]
public string KidName { get; set; }
}
Then you can create Kid object manually in case that your model valid.
UPDATE (to address your comment)
If you use this approach your action will look like this:
[HttpPost]
public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
{
if (ModelState.IsValid)
{
using (var dbContext = new yourContext())
{
var teacher = dbContext.Teachers.FirstOrDefault(t=>t.id == addNewKidViewDTO.SelectedTeacherId );
if(teacher == default(Teacher))
{
//return an error message or add validation error to model state
}
//It is also common pattern to create a factory for models
//if you have some logic involved, but in this case I simply
//want to demonstrate the approach
var kid = new Kid
{
Name = addNewKidViewDTO.KidName,
Teacher = teacher
};
dbContext.SaveChanges();
}
}
}
AddNewKidViewDTO
that will initialize Teacher
property in Kid object so once you actually use Model.IsValid
the property will be initialized.