Как сопоставить модель просмотра с моделью домена в действии POST?

87

Каждая статья в Интернете об использовании ViewModels и Automapper дает рекомендации по отображению направления «Контроллер -> Просмотр». Вы берете модель предметной области вместе со всеми списками выбора в одну специализированную модель представления и передаете ее представлению. Это ясно и нормально.
У представления есть форма, и в конце концов мы находимся в действии POST. Здесь все связующие модели выходят на сцену вместе с [очевидно] другой моделью представления, которая [очевидно] связана с исходной моделью представления, по крайней мере, в части соглашений об именах ради привязки и проверки.

Как вы сопоставите это с вашей моделью домена?

Пусть это будет действие вставки, мы могли бы использовать тот же Automapper. Но что, если это было действие обновления? Нам нужно получить объект домена из репозитория, обновить его свойства в соответствии со значениями в ViewModel и сохранить в репозитории.

ПРИЛОЖЕНИЕ 1 (9 февраля 2010 г.): Иногда недостаточно присвоить свойства модели. Должны быть предприняты некоторые действия против модели предметной области в соответствии со значениями модели представления. Т.е. некоторые методы должны вызываться в модели предметной области. Вероятно, должен быть своего рода уровень службы приложений, который стоит между контроллером и доменом для обработки моделей представления ...


Как организовать этот код и где его разместить для достижения следующих целей?

  • держать контроллеры тонкими
  • уважать практику SoC
  • следовать принципам доменно-ориентированного дизайна
  • быть СУХИМ
  • продолжение следует ...
Антоний Сердюков
источник

Ответы:

37

Я использую интерфейс IBuilder и реализую его с помощью ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (реализация) RebuildViewModel просто вызываетBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

кстати, я не пишу ViewModel, я пишу Input, потому что он намного короче, но это не очень важно,
надеюсь, это поможет

Обновление: я использую этот подход сейчас в демонстрационном приложении ProDinner ASP.net MVC , теперь он называется IMapper, также есть PDF-файл, в котором этот подход подробно объясняется

Ому
источник
Мне нравится такой подход. Одна вещь, которую я не понимаю, - это реализация IBuilder, особенно в свете многоуровневого приложения. Например, моя ViewModel имеет 3 списка выбора. Как реализация построителя извлекает значения списка выбора из репозитория?
Мэтт Мюррелл
@Matt Murrell, посмотрите на prodinner.codeplex.com Я делаю это там, и я называю это IMapper вместо IBuilder
Ому
6
Мне нравится такой подход, я реализовал его здесь: gist.github.com/2379583
Пол Стовелл,
На мой взгляд, это не соответствует подходу модели предметной области. Похоже, какой-то подход CRUD для неясных требований. Разве мы не должны использовать фабрики (DDD) и связанные с ними методы в модели предметной области для передачи разумных действий? Таким образом, нам лучше загрузить сущность из БД и обновить ее по мере необходимости, верно? Так что, похоже, это не совсем правильно.
Артём
7

Такие инструменты, как AutoMapper, можно использовать для обновления существующего объекта данными из исходного объекта. Действие контроллера для обновления может выглядеть так:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Помимо того, что видно во фрагменте выше:

  • Данные POST для просмотра модели + проверка выполняется в ModelBinder (может быть расширена с помощью пользовательских привязок)
  • Обработка ошибок (т.е. перехват исключений доступа к данным репозиторием) может выполняться фильтром [HandleError]

Действие контроллера довольно тонкое, и проблемы разделены: проблемы сопоставления решаются в конфигурации AutoMapper, проверка выполняется с помощью ModelBinder, а доступ к данным - с помощью репозитория.

PanJanek
источник
6
Я не уверен, что здесь Automapper полезен, поскольку он не может отменить выравнивание. В конце концов, модель предметной области - это не простой DTO, как модель представления, поэтому может быть недостаточно присвоить ей какие-то свойства. Возможно, какие-то действия следует выполнить с моделью предметной области в соответствии с содержимым модели представления. Впрочем, +1 за обмен неплохой подход.
Антоний Сердюков
@Anton ValueInjecter может отменить сглаживание;)
Ому
при таком подходе вы не делаете контроллер тонким, вы нарушаете SoC и DRY ... как сказал Ому, у вас должен быть отдельный слой, который заботится о материалах отображения.
Rookian
5

Я хотел бы сказать, что вы повторно используете термин ViewModel для обоих направлений взаимодействия с клиентом. Если вы прочитали достаточно кода ASP.NET MVC в реальной жизни, вы, вероятно, заметили различие между ViewModel и EditModel. Я думаю, это важно.

ViewModel представляет всю информацию, необходимую для визуализации представления. Это могут быть данные, которые отображаются в статических неинтерактивных местах, а также данные, предназначенные исключительно для проверки, чтобы решить, что именно отображать. Действие GET контроллера обычно отвечает за упаковку ViewModel для своего View.

EditModel (или, возможно, ActionModel) представляет данные, необходимые для выполнения действия, которое пользователь хотел сделать для этого POST. Итак, EditModel действительно пытается описать действие. Это, вероятно, исключит некоторые данные из ViewModel, и, хотя они связаны, я думаю, важно понимать, что они действительно разные.

Одна идея

Тем не менее, вы можете очень легко получить конфигурацию AutoMapper для перехода от Model -> ViewModel и другую для перехода от EditModel -> Model. Тогда для различных действий контроллера просто необходимо использовать AutoMapper. Черт возьми, EditModel может иметь на себе функции для проверки его свойств на соответствие модели и применения этих значений к самой модели. Он больше ничего не делает, и у вас есть ModelBinders в MVC для сопоставления запроса с EditModel.

Другая идея

Кроме того, что-то, о чем я недавно размышлял, такого рода отрабатывает идею ActionModel, заключается в том, что то, что клиент отправляет вам в ответ, на самом деле является описанием нескольких действий, выполненных пользователем, а не просто одним большим шаром данных. Это, безусловно, потребует некоторого Javascript на стороне клиента для управления, но я думаю, что идея интригует.

По сути, когда пользователь выполняет действия на экране, который вы ему представили, Javascript начнет создавать список объектов действий. Например, возможно, пользователь находится на экране информации о сотруднике. Они обновляют фамилию и добавляют новый адрес, потому что сотрудник недавно был женат. Под обложками это создает объекты a ChangeEmployeeNameи an AddEmployeeMailingAddressв списке. Пользователь нажимает «Сохранить», чтобы зафиксировать изменения, и вы отправляете список из двух объектов, каждый из которых содержит только информацию, необходимую для выполнения каждого действия.

Вам понадобится более интеллектуальный ModelBinder, чем тот, который используется по умолчанию, но хороший сериализатор JSON должен иметь возможность позаботиться о сопоставлении объектов действий на стороне клиента с объектами на стороне сервера. На стороне сервера (если вы находитесь в двухуровневой среде) легко могут быть методы, завершающие действие в модели, с которой они работают. Таким образом, действие контроллера заканчивается просто получением идентификатора экземпляра модели для извлечения и списка действий, которые необходимо выполнить над ним. Или в действиях есть идентификатор, чтобы они были разделены.

Так что, возможно, что-то подобное будет реализовано на стороне сервера:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

Это действительно делает действие обратной отправки довольно универсальным, поскольку вы полагаетесь на свой ModelBinder, чтобы получить правильный экземпляр IUserAction и ваш экземпляр IUserAction, чтобы либо выполнить правильную логику, либо (что более вероятно) вызвать модель с информацией.

Если бы вы были в трехуровневой среде, IUserAction можно было бы просто сделать простыми DTO, которые будут сниматься через границу и выполняться аналогичным методом на уровне приложения. В зависимости от того, как вы делаете этот слой, он может быть очень легко разделен и по-прежнему оставаться в транзакции (что приходит на ум, так это запрос / ответ Агаты и использование преимуществ карты идентичности DI и NHibernate).

В любом случае, я уверен, что это не идеальная идея, для управления потребуется некоторый JS на стороне клиента, и я еще не смог создать проект, чтобы увидеть, как он разворачивается, но в сообщении пытались подумать о том, как добраться туда и обратно, так что я подумал, что выскажу свои мысли. Я надеюсь, что это поможет, и я хотел бы услышать о других способах управления взаимодействиями.

Шон Копенгейвер
источник
Интересно. Что касается различия между ViewModel и EditModel ... вы предполагаете, что для функции редактирования вы должны использовать ViewModel для создания формы, а затем привязать к EditModel, когда пользователь ее разместил? Если да, то как бы вы справились с ситуациями, когда вам нужно будет повторно опубликовать форму из-за ошибок проверки (например, когда ViewModel содержит элементы для заполнения раскрывающегося списка) - не могли бы вы также просто включить раскрывающиеся элементы в EditModel? В таком случае, в чем будет разница между ними?
UpTheCreek
Я предполагаю, что вас беспокоит то, что если я использую EditModel и возникает ошибка, мне придется перестроить мою ViewModel, что может оказаться очень дорогостоящим. Я бы сказал, просто перестройте ViewModel и убедитесь, что в нем есть место для размещения сообщений с уведомлениями пользователей (возможно, как положительных, так и отрицательных, таких как ошибки проверки). Если окажется, что это проблема производительности, вы всегда можете кэшировать ViewModel до тех пор, пока не закончится следующий запрос этого сеанса (вероятно, это будет публикация EditModel).
Шон Копенгейвер 08
0

Вам не нужно сопоставлять модель просмотра с доменом, потому что ваша модель просмотра может быть создана больше, чем модель домена. Модели просмотра оптимизированы для экрана (ui) и отличаются от модели предметной области.

http://lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models/

огуж4н
источник