ViewModels and one-to-many relationships with Entity Framework in MVC?
I have an application for storing information about consultants in a database. The model is an Entity Framework model, and the database tables are Consultant with one-to-many relationships to a number of other tables (WorkExperiences, Programs, CompetenceAreas, etc). Now, when I want to create a new Consultant object in a View, I would开发者_JAVA百科 really just want to pass a Consultant object as the model to the View. But for one, it has been suggested to me (Collection of complex child objects in Asp.Net MVC 3 application?) that I shouldn't do this, but use ViewModels instead. Secondly, and maybe this is the reason, I get an error saying "The EntityCollection has already been initialized" when I try to post the Consultant object if using it as a model in the View, and the cause of the error seems to be the collections of objects such as WorkExperiences.
So my first question is why I'm getting this error.
But more importantly, if I should instead use a ViewModel, how would I do that properly? Because I have in fact tried something, and got it working. But...the code is awful. Can anyone please tell me what I should be doing instead to get this working more cleanly?
Let me show you what I have (that again works, but is a nightmare codewise):
The GET Create method:
public ActionResult Create()
{
Consultant consultant = new Consultant();
ConsultantViewModel vm = GetViewModel(consultant);
return View(vm);
}
Helper method to create the "ViewModel" (if this is in fact what a ViewModel is supposed to be like):
private ConsultantViewModel GetViewModel(Consultant consultant)
{
ConsultantViewModel vm = new ConsultantViewModel();
vm.FirstName = consultant.FirstName;
vm.LastName = consultant.LastName;
vm.UserName = consultant.UserName;
vm.Description = consultant.Description;
vm.Programs = consultant.Programs.ToList();
vm.Languages = consultant.Languages.ToList();
vm.Educations = consultant.Educations.ToList();
vm.CompetenceAreas = consultant.CompetenceAreas.ToList();
vm.WorkExperiences = consultant.WorkExperiences.ToList();
return vm;
}
The POST Create method:
[HttpPost]
[ValidateInput(false)] //To allow HTML in description box
public ActionResult Create(ConsultantViewModel vm, FormCollection collection)
{
try
{
Consultant consultant = CreateConsultant(vm);
_repository.AddConsultant(consultant);
_repository.Save();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
Helper method to create a Consultant object (this one is particularly awful, where I have to check that collections are not null, in case the user decides not to add anything in those lists...):
private Consultant CreateConsultant(ConsultantViewModel vm)
{
Consultant consultant = new Consultant();
consultant.Description = vm.Description;
consultant.FirstName = vm.FirstName;
consultant.LastName = vm.LastName;
consultant.UserName = vm.UserName;
if (vm.Programs != null)
foreach (var program in vm.Programs)
consultant.Programs.Add(program);
if (vm.Languages != null)
foreach (var language in vm.Languages)
consultant.Languages.Add(language);
if (vm.Educations != null)
foreach (var education in vm.Educations)
consultant.Educations.Add(education);
if (vm.WorkExperiences != null)
foreach (var workExperience in vm.WorkExperiences)
consultant.WorkExperiences.Add(workExperience);
if (vm.CompetenceAreas != null)
foreach (var competenceArea in vm.CompetenceAreas)
consultant.CompetenceAreas.Add(competenceArea);
return consultant;
}
So, again it works, but is nowhere near as clean as if I could have used a Consultant object directly (if not for that "EntityCollection is already initialized" error"...). So how should I do it instead?
First of all, you shouldn't use your entity object as the viewmodel because (and I can think of at least two reasons right now, but there are more):
You don't want to expose sensitive data, such as 'Id' or 'Password'. Imagine your consultant has an
Id
and an evil user opens the edit consultant page and posts back a differentId
. As a result, the evil user will succeed in updating differentConsultant
.Currently whatever you show in the View corresponds to what your
Consultant
object looks like. But in case you want to add extra info that is not part of theConsultant
object (as simple as a checkbox field). In that case, you have to rewrite quite a bit of code, create the ViewModel, map it, etc. While if you follow the ViewModel pattern from the start, you can just make this simple change whenever you need it.
Regarding your code - you can try to use AutoMapper
with Nested Mappings for this type of conversion. Even if you don't, your code can be made a bit cleaner by using projections.
private ConsultantViewModel GetViewModel(Consultant consultant)
{
return new ConsultantViewModel
{
FirstName = consultant.FirstName,
LastName = consultant.LastName,
...
vm.Programs = consultant.Programs.ToList(),
...
};
}
private Consultant CreateConsultant(ConsultantViewModel vm)
{
var consultant = new Consultant
{
Description = vm.Description,
FirstName = vm.FirstName,
...
};
if (vm.Programs != null)
{
vm.Programs.ForEach(consultant.Programs.Add);
}
...
return consultant;
}
精彩评论