Попытка использовать AutoMapper для модели с дочерними коллекциями, получая нулевую ошибку в Asp.Net MVC 3 - PullRequest
6 голосов
/ 26 февраля 2011

Я совершенно новичок в 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, кажется, что он работает нормально (используя простейшую форму, без распознавателя или чего-либо еще, просто сопоставляя Консультанта с ConsultantViewModel), включая дочерние коллекции и все. Также есть свойство UserName. Теперь в представлении у меня нет поля для UserName, потому что оно всегда автоматически заполняется текущим пользователем (User.Identity.Name). Но когда я возвращаю vm, свойство UserName имеет значение null, предположительно потому, что в представлении для него не было поля.

Я подозреваю, что некоторые коллекции вызовут ту же ошибку, даже если я добавлю туда скрытое поле для UserName, потому что Консультант необязательно заполнять языки и т. Д. Так что, хотя ViewModel имеет созданный экземпляр списка для каждой из этих дочерних коллекций, поступающих в представление (со счетчиком 0), вместо этого они возвращаются с нулевым значением.

Как мне решить это? Я не хочу, чтобы пользователь заполнял значения во всех дочерних коллекциях. Я имею в виду, что я всегда мог создать объект Language с пустой строкой для свойства Name, но это означало бы ненужный дополнительный код, и все, что я действительно хочу, это вернуть дочерние коллекции (и UserName) обратно так, как они вошли в Представление - с заполненным именем пользователя и экземплярами дочерних коллекций, но со счетчиком 0, если пользователь не добавил ни одного элемента.

UPDATE:

Не знаю, мне кажется, что я как-то неправильно понимаю 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 Ответ

4 голосов
/ 27 февраля 2011

Еще не получил никаких ответов, и я действительно нашел способ заставить его работать. Все еще не очень хорошо, потому что код довольно многословный ... Так что, если у кого-то есть идеи получше, принесите их!

Я изменил его так, что теперь у меня есть объект 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 и сопоставляю каждую Программу.

Ну, это сработало, хотя мне не очень нравится код ... После того, как я это сделал, меня поразило, что мне также нужно иметь возможность удалять любые элементы, которые пользователь "пометил для удаления" так сказать в View. Я использую представление, где вы можете добавлять и удалять текстовые поля и т.д. с помощью jQuery, следуя инструкции Стивена Сандерсона. Но это сделало код еще более сложным ... Во всяком случае, это было лучшее, что я мог придумать, поэтому, пожалуйста, еще раз предложите любые другие идеи, если вы можете улучшить это! Мне особенно понравилось бы решение, в котором мне не нужно было вручную циклически проходить по коллекциям в POST, но AutoMapper обрабатывал бы сами вложенные коллекции без ошибок, упомянутых выше!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...