Attempting to use AutoMapper for a model with child collections, getting a null error in Asp.Net MVC 3

I am 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 that AutoMapper creates the ViewModel, it seems to be working fine (using its simplest form, without permission or anything else, just matching the Consultant to the ViewModel consultant), including child collections and all. In addition, there is a UserName property. Now in the view, I do not have a field for UserName, because it is always automatically populated by the current user (User.Identity.Name). But when I go back to vm, the UserName property is null, apparently because the view did not have a field for it.

I suspect that some of the collections will cause the same error, even if I put a hidden field there for UserName, because the Consultant does not have to fill in the languages, etc. Therefore, although the ViewModel has an instance list for each of these child collections included in the view (with the number 0), they are returned with a null value.

How can i solve this? I do not want the user to enter values ​​populated by all child collections. 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 return the child collections (and UserName) to where they went View - with the filled in user name and with the created child collections, but with the number 0 if the user has not added any elements.

UPDATE:

I don’t know, I think that I misunderstand AutoMapper ... I found that in fact child collections were not really a problem in relation to mappings, which worked fine with displaying it back to the "Consultant" object, However ... I it was also necessary to return the identifier to the "Consultant" object, because the ViewModel was not there. But even so, when I save to the store, it is not saved. This is where I think I misunderstand AutoMapper - I thought it would somehow populate the Consultant object with the values ​​from the ViewModel, but I think it makes the consultant variable refer to another object in the Map () statement? Because none of this has survived ...

Here's the POST method changed (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 to get filled values ​​in ViewModel back to the Consultant object and save it in the database ???

UPDATE 2:

Well, too confusing ... Starting with the bit: here's the map creation in Application_Start:

  Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore()); Mapper.CreateMap<Consultant, ConsultantViewModel>(); 

Change 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 does not work either, but it seems to at least try to update the entity model, because now I get an exception:

EntityCollection cannot be initialized because the relationship manager for the object to which EntityCollection belongs is already bound to an ObjectContext. The InitializeRelatedCollection method should only be called to initialize a new EntityCollection during deserialization of the object graph.

And an example YSOD error code:

 Line 698: if ((value != null)) Line 699: { Line 700: ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Program>("ConsultantsModel.ConsultantProgram", "Program", value); Line 701: } Line 702: } 

This is almost the same error I received when trying to use the entity object directly as a model, instead of AutoMapper creating a ViewModel. So what am I doing wrong? It drives me crazy...

UPDATE 3:

Well, an endless story ... I found information about using UseDestinationValue in the CreateMap method in AutoMapper. So I tried to do it, and it really put me in order. But ... now I get a new exception in SaveChanges () (in the EF model). An exception is the following: "The operation failed: the relation cannot be changed because one or more properties of the foreign key cannot be reset." This is similar to the exception that also occurs when trying to delete child objects in a one-to-many relationship if you don’t have a set of cascading deletes, but that’s not what I am trying to do here ...

The CreateMap methods are updated here:

 Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore()).ForMember( x => x.Programs, opts => opts.UseDestinationValue()); Mapper.CreateMap<Consultant, ConsultantViewModel>(); 

Any ideas?

+6
collections null asp.net-mvc-3 automapper
source share
1 answer

There are no answers yet, and I really found a way to make it work. Still not feeling well, because the code is pretty detailed ... So, if anyone has any better ideas, please bring them!

I changed it, so now I have a DTO object for each of the types of child collections (this probably should have started using AutoMapper). So, for example, I now have a ProgramDTO type to map to a program.

I tried to make a comparison simply with the "Consultant" object, hoping that the nested collections would work on their own, but again received the error "EntityCollection is already initialized." So I decided to use this method:

  private Consultant CreateConsultant(ConsultantViewModel vm, Consultant consultant) //Parameter Consultant needed because an object may already exist from Edit method. { Mapper.Map(vm, consultant); //To do this I had to add an Ignore in the mapping configuration: //Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember(x => x.Programs, opts => opts.Ignore()); //Delete items "marked for deletion" by removing with jQuery in the View: var programs = consultant.Programs.Except(consultant.Programs.Join(vm.Programs, p => p.Id, d => d.Id, (p, d) => p)).ToList(); Delete(programs); foreach (var programDto in vm.Programs) { Program program = consultant.Programs.SingleOrDefault(x => x.Id == programDto.Id); if (program == null) { program = new Program(); consultant.Programs.Add(program); } program = Mapper.Map(programDto, program); } _repository.Save(); return consultant; } 

The difference is that I fill out the simple properties of the UpdateModel () Consultant, and then iterate over the ProgramDTO collection and map each Program.

Well, it worked, although I don’t really like the code ... It also hit me after I did this, that I also need to delete all the elements that the user “marked for deletion”, so to speak, in the view. I use a view where you can add and remove text fields, etc. JQuery, following the direction of Stephen Sanderson. But that made the code even more complex ... Anyway, it was the best I could come up with, so suggest any other ideas again if you can improve this! I especially liked the solution, in which I did not have to manually sort through the collections in POST, but AutoMapper processes the nested collections themselves without the errors mentioned above!

+4
source share

All Articles