Попытка использовать AutoMapper для модели с дочерними коллекциями, получая нулевую ошибку в Asp.Net MVC 3
Я полностью новичок в AutoMapper, и у меня есть вид, который выглядит следующим образом:
@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>
Вот метод GET Edit:
public ActionResult Edit(int id)
{
Consultant consultant = _repository.GetConsultant(id);
ConsultantViewModel vm = Mapper.Map<Consultant, ConsultantViewModel>(consultant);
return View(vm);
}
И метод POST Edit:
[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");
}
Теперь, когда AutoMapper создает ViewModel, он, кажется, работает нормально (используя его простейшую форму, без распознавателя или чего-либо еще, просто сопоставляя Consultant с ConsultantViewModel), включая дочерние коллекции и все. Также есть свойство UserName. Теперь в представлении у меня нет поля для UserName, потому что оно всегда автоматически заполняется текущим пользователем (User.Identity.Name). Но когда я возвращаю vm, свойство UserName имеет значение null, предположительно потому, что в представлении для него не было поля.
Я подозреваю, что некоторые коллекции вызовут ту же ошибку, даже если я добавлю туда скрытое поле для UserName, потому что Консультант необязательно заполнять языки и т. Д. Так что, хотя ViewModel имеет конкретный список для каждого из эти дочерние коллекции, входящие в представление (с числом 0), вместо этого возвращаются с нулевым значением.
Как мне это решить? Я не хочу, чтобы пользователь заполнял значения во всех дочерних коллекциях. Я имею в виду, что я всегда мог создать объект Language с пустой строкой для свойства Name, но это означало бы ненужный дополнительный код, и все, что мне действительно нужно, - это вернуть дочерние коллекции (и UserName) обратно так, как они вошли в Представление - с заполненным именем пользователя и экземплярами дочерних коллекций, но со счетчиком 0, если пользователь не добавил ни одного элемента.
ОБНОВИТЬ:
Я не знаю, я думаю, что почему-то неправильно понимаю AutoMapper... Я обнаружил, что на самом деле дочерние коллекции на самом деле не были проблемой в отношении отображения, которое отлично работало, отображая его обратно в объект Консультанта. Однако... Мне также нужно было вернуть идентификатор обратно в объект Консультанта, потому что у ViewModel его не было. Но даже в этом случае, когда я сохраняю данные в хранилище, они не сохраняются. Здесь я думаю, что неправильно понял AutoMapper - я думал, что это каким-то образом заполнит объект Консультанта значениями из ViewModel, но я предполагаю, что это заставит переменную консультанта ссылаться на другой объект в выражении Map()? Потому что ничего из этого не сохранилось...
Вот модифицированный метод POST (который не работает):
[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");
}
Что я делаю неправильно? Как мне вернуть заполненные значения в ViewModel обратно в объект Консультанта и сохранить его в базе данных???
ОБНОВЛЕНИЕ 2:
Хорошо, в замешательстве... Начнем сначала: создание карты в Application_Start:
Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore());
Mapper.CreateMap<Consultant, ConsultantViewModel>();
Методы редактирования:
// 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");
}
Это тоже не работает, но, по крайней мере, пытается обновить модель сущностей, потому что теперь я получаю исключение:
EntityCollection не удалось инициализировать, поскольку менеджер отношений для объекта, которому принадлежит EntityCollection, уже присоединен к ObjectContext. Метод InitializeRelatedCollection следует вызывать только для инициализации новой коллекции EntityCollection во время десериализации графа объектов.
И пример кода ошибки YSOD:
Line 698: if ((value != null))
Line 699: {
Line 700: ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Program>("ConsultantsModel.ConsultantProgram", "Program", value);
Line 701: }
Line 702: }
Это почти та же ошибка, что и при попытке использовать объект-сущность непосредственно в качестве модели, вместо того, чтобы AutoMapper создавал ViewModel. Так что я делаю не так? Это сводит меня с ума...
ОБНОВЛЕНИЕ 3:
Ну, бесконечная история... Я нашел некоторую информацию об использовании UseDestinationValue для метода CreateMap в AutoMapper. Так что я попробовал это, и, ну, на самом деле это дало мне немного больше Но... теперь я получаю новое исключение для SaveChanges() (в модели EF). Теперь исключение: "Операция завершилась неудачно: отношение не может быть изменено, поскольку одно или несколько свойств внешнего ключа не могут иметь значение NULL". Похоже, это исключение, которое также возникает при попытке удалить дочерние объекты в отношении "один ко многим", если у вас не установлено каскадное удаление, но это не то, что я пытаюсь сделать здесь...
Вот обновленные методы CreateMap:
Mapper.CreateMap<ConsultantViewModel, Consultant>().ForMember("Id", opts => opts.Ignore()).ForMember(
x => x.Programs, opts => opts.UseDestinationValue());
Mapper.CreateMap<Consultant, ConsultantViewModel>();
Есть идеи?
1 ответ
Пока не получили никаких ответов, и я действительно нашел способ заставить это работать. Все еще не очень хорошо, потому что код довольно многословный... Так что, если у кого-то есть идеи получше, принесите их!
Я изменил его так, что теперь у меня есть объект DTO для каждого из типов в дочерних коллекциях (вероятно, должен был иметь это для начала при использовании AutoMapper). Так, например, теперь у меня есть тип ProgramDTO для сопоставления с программой.
Я попытался выполнить сопоставление просто с помощью объекта "Консультант", надеясь, что вложенные коллекции будут работать самостоятельно, но только снова получил ошибку "EntityCollection уже инициализирован". Итак, догадываясь, я попробовал этот метод вместо:
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;
}
Разница в том, что я заполняю простые свойства Консультанта с помощью UpdateModel(), а затем перебираю коллекцию ProgramDTO и сопоставляю каждую Программу.
Ну, это сработало, хотя мне не очень нравится код... После того, как я это сделал, меня тоже поразило, что мне также нужно иметь возможность удалять любые элементы, которые пользователь "пометил для удаления", так сказать в представлении. Я использую представление, где вы можете добавлять и удалять текстовые поля и т. Д. С помощью jQuery, следуя руководству Стивена Сандерсона. Но это сделало код еще более сложным... Во всяком случае, это было лучшее, что я мог придумать, поэтому, пожалуйста, еще раз предложите любые другие идеи, если вы можете улучшить это! Мне особенно понравилось бы решение, в котором мне не нужно было вручную циклически проходить по коллекциям в POST, но AutoMapper обрабатывал сами вложенные коллекции, без ошибок, упомянутых выше!