I'm completely new to AutoMapper, and I have a View that looks like this:
@using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Consultant</legend>
<div class="editor-label">
@Html.LabelFor(model => model.FirstName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.FirstName)
@Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
@Html.TextAreaFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
<div class="editor-label">
Program du behärskar:
</div>
<div>
<table id="programEditorRows">
<tr>
<th>
Program
</th>
<th>
Nivå
</th>
</tr>
@foreach (var item in Model.Programs)
{
Html.RenderPartial("ProgramEditorRow", item);
}
</table>
<a href="#" id="addProgram">Lägg till</a>
</div>
<div class="editor-label">
Språk du behärskar:
</div>
<div>
<table id="languageEditorRows">
<tr>
<th>
Språk
</th>
<th>
Nivå
</th>
</tr>
@foreach (var item in Model.Languages)
{
Html.RenderPartial("LanguageEditorRow", item);
}
</table>
<a href="#" id="addLanguage">Lägg till</a>
</div>
<div>
<table id="educationEditorRows">
<tr>
<th>
Utbildning
</th>
<th>
Nivå
</th>
</tr>
@foreach (var item in Model.Educations)
{
Html.RenderPartial("EducationEditorRow", item);
}
</table>
<a href="#" id="addEducation">Lägg till</a>
</div>
<div>
<table id="workExperienceEditorRows">
<tr>
<th>
Arbetserfarenhet
</th>
<th>
Startdatum
</th>
<th>
Slutdatum
</th>
</tr>
@foreach (var item in Model.WorkExperiences)
{
Html.RenderPartial("WorkExperienceEditorRow", item);
}
</table>
<a href="#" id="addWorkExperience">Lägg till</a>
</div>
<div>
<table id="competenceAreaEditorRows">
<tr>
<th>
Kompetensområde
</th>
<th>
Nivå
</th>
</tr>
@foreach (var item in Model.CompetenceAreas)
{
Html.RenderPartial("CompetenceAreaEditorRow", item);
}
</table>
<a href="#" id="addCompetenceArea">Lägg till</a>
</div>
<div>
<input id="fileInput" name="FileInput" type="file" />
</div>
<p>
<input type="submit" value="Spara" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
Here's the GET Edit method:
public ActionResult Edit(int id)
{
Consultant consultant = _repository.GetConsultant(id);
ConsultantViewModel vm = Mapper.Map<Consultant, ConsultantViewModel>(consultant);
return View(vm);
}
And the POST Edit method:
[HttpPost]
[ValidateInput(false)] //To allow HTML in description box
public ActionResult Edit(int id, ConsultantViewModel vm, FormCollection collection)
{
Consultant consultant = Mapper.Map<ConsultantViewModel, Consultant>(vm);
_repository.Save();
return RedirectToAction("Index");
}
Now, when AutoMapper creates the ViewModel it seems to be working fine (using its simplest form, no resolver or anything, just mapping Consultant to ConsultantViewModel), including the child collections and all. Also, the UserName property is there. Now, in the View I don't have a field for UserName, because it is always automatically filled in by the current User (User.Identity.Name). But when I get the vm back, the UserName property is null, presumably because there was no field for it in the View.
I suspect some of the collections will cause the same error even if I put a hidden field in there for UserName, because it is optional for the Consultant to fill in Languages etc... So even though the ViewModel has an instantiated list for each of these child collections going in to the View (with a count of 0), they come back with a null value instead.
How do I solve this? I don't want to force having values filled in all the child collections by the user. I mean, I could always create a Language object with an empty string for the Name property, but that would mean unnecessary extra code, and all I really want is to get the child collections (and UserName) back the way they went in to the View - with UserName filled in, and with the child collections being instantiated, but with a count of 0 if the user hasn't added any items.
UPDATE:
I don't know, I think I'm misunderstanding AutoMapper somehow... I found that in fact the child collections were'nt really a problem with regard to the mapping, that worked fine mapping it back into the Consultant object. However...I also needed to put the id back in the Consultant object, because the ViewModel didn't have any. But even so, when I save to repository, it doesn't get saved. This is where I think I misunderstand AutoMapper - I had thought it would somehow populate the Consultant object with values from the ViewModel, but I guess it causes the consultant variable to refer to a different object in the Map() statement? Because none of it is persisted...
Here's the modified POST method (which doesn't work):
[HttpPost]
[ValidateInput(false)] //To allow HTML in description box
public ActionResult Edit(int id, ConsultantViewModel vm, FormCollection collection)
{
vm.UserName = User.Identity.Name;
Consultant consultant = _repository.GetConsultant(id);
consultant = Mapper.Map<ConsultantViewModel, Consultant>(vm);
consultant.Id = id;
_repository.Save();
return RedirectToAction("Index");
}
What am I doing wrong? How do I get the populated values in the ViewModel back into the Consultant object and persist it to the database???
UPDATE 2:
Ok, thorougly confused... Starting over a bit: Here's the map creation in Application_Start:
Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore());
Mapper.CreateMap<Consultant, ConsultantViewModel>();
Edit methods:
// GET: /Consultant/Edit/5
public ActionResult Edit(int id)
{
Consultant consultant = _repository.GetConsultant(id);
ConsultantViewModel vm = Mapper.Map<Consultant, ConsultantViewModel>(consultant);
return View(vm);
}
//
// POST: /Consultant/Edit/5
[HttpPost]
[ValidateInput(false)] //To allow HTML in description box
public ActionResult Edit(int id, ConsultantViewModel vm, FormCollection collection)
{
vm.UserName = User.Identity.Name;
Consultant consultant = _repository.GetConsultant(id);
consultant = Mapper.Map<ConsultantViewModel, Consultant>(vm, consultant);
_repository.Save();
return RedirectToAction("Index");
}
This doesn't work either, but apparently at least tries to update the entity model, because now I get an exception:
The EntityCollection could not be initialized because the relationship manager for the object to which the EntityCollection belongs is already attached to an ObjectContext. The InitializeRelatedCollection method should only be called to initialize a new EntityCollection during deserialization of an object graph.
And YSOD error code sample:
Line 698: if ((value != null))
Line 699: {
Line 700: ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Program>("ConsultantsModel.ConsultantProgram", "Program", value);
Line 701: }
Line 702: }
This is pretty much the same error I got when trying to use the entity object directly as model, rather than having AutoMapper create a ViewModel. So what am I doing wrong? This is driving me crazy...
UPDATE 3:
Well, neverending story... I found some info on using UseDestinationValue on the CreateMap method in AutoMapper. So I tried that, and well, that actually got me a bit further. But...now I get a new exception on SaveChanges() (in the EF model). The exception now is: "The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable." This appears to be an exception that also occurs when trying to delete child objects in a one-to-many relationship if you don't have cascade delete set, but that's not what I'm trying to do here...
Here's the updated CreateMap methods:
Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore()).ForMember(
x => x.Programs, opts => opts.UseDestinationValue());
Mapper.CreateMap<Consultant, ConsultantViewModel>();
Any ideas?