如何在POST操作中将视图模型映射回域模型?


87

互联网上有关使用ViewModels和Automapper的每篇文章都提供了“ Controller-> View”方向映射的指南。您将域模型与所有选择列表一起放入一个专用的ViewModel中,并将其传递给视图。很好。
该视图具有一个表单,最终我们处于POST操作中。在这里,所有的Model Binders和另一个[显然]的View Model [显然]至少在命名约定的一部分方面与原始ViewModel有关,这显然是为了绑定和验证。

您如何将其映射到您的域模型?

让它成为一个插入动作,我们可以使用相同的Automapper。但是,如果这是更新操作,该怎么办?我们必须从存储库中检索域实体,根据ViewModel中的值更新其属性,然后将其保存到存储库中。

附录1(2010年2月9日):有时,仅分配Model的属性是不够的。应该根据视图模型的值对域模型采取一些措施。即,应在域模型上调用某些方法。可能应该在控制器和域之间存在一种应用程序服务层,以便处理视图模型...


如何组织此代码以及将其放置在何处以实现以下目标?

  • 保持控制器薄
  • 荣誉SoC实践
  • 遵循领域驱动设计原则
  • 干燥
  • 未完待续 ...

Answers:


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 cuz,它要短得多,但这并不是很重要,
希望对您有所帮助

更新: 我现在在ProDinner ASP.net MVC演示应用程序中使用这种方法,现在称为IMapper,还提供了一个pdf文件,其中对该方法进行了详细说明


我喜欢这种方法。我尚不清楚的一件事是IBuilder的实现,尤其是考虑到分层应用程序的情况。例如,我的ViewModel有3个SelectLists。构建器实现如何从存储库中检索选择列表值?
马特·

@Matt Murrell看看prodinner.codeplex.com,我在那儿做,我在那儿叫IMapper而不是IBuilder
Omu

6
我喜欢这种方法,在这里实现了一个示例:gist.github.com/2379583
Paul Stovell 2012年

在我看来,它不符合域模型方法。看起来有些CRUD方法可以解决不明确的要求。我们是否应该在域模型中使用工厂(DDD)和相关方法来传达一些合理的行动?这样,我们最好从数据库加载一个实体并根据需要对其进行更新,对吗?因此,看起来它并不完全正确。
Artyom

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完成,数据访问由存储库完成。


6
我不确定Automapper在这里是否有用,因为它无法反向展平。毕竟,域模型不是像视图模型那样的简单DTO,因此可能不足以为其分配一些属性。可能应根据视图模型的内容针对域模型执行某些操作。但是,+ 1用于共享相当不错的方法。
Anthony Serdyukov

@Anton ValueInjecter可以反向展平;)
Omu

使用这种方法,您不会使控制器变薄,就违反了SoC和DRY…正如Omu提到的,您应该有一个单独的层来照顾映射内容。
Rookian 2010年

5

我想说的是,您在客户端交互的两个方向上都重复使用了ViewModel一词。如果您狂野地阅读了足够的ASP.NET MVC代码,则可能已经看到了ViewModel和EditModel之间的区别。我认为这很重要。

ViewModel代表呈现视图所需的所有信息。这可能包括在静态非交互式位置渲染的数据,也可能纯粹用于执行检查以确定要渲染的内容的数据。Controller GET操作通常负责为其视图打包ViewModel。

EditModel(或者可能是ActionModel)表示执行用户要对该POST进行的操作所需的数据。因此,EditModel确实试图描述一个动作。这可能会从ViewModel中排除一些数据,尽管相关,但我认为重要的是要意识到它们确实有所不同。

一个想法

也就是说,您可以很容易地从模型-> ViewModel拥有一个AutoMapper配置,而从EditModel->模型得到一个不同的配置。然后,不同的Controller操作仅需要使用AutoMapper。地狱中的EditModel可以具有一个函数,以针对模型验证其属性,并将这些值应用于Model本身。它什么也没做,而且您在MVC中拥有ModelBinder,可以将请求映射到EditModel。

另一个想法

除了最近我一直在考虑的某种动作模型之外,ActionModel的想法还在于,客户端发回给您的内容实际上是用户执行的几个动作的描述,而不仅仅是一个大数据。当然,这需要在客户端使用一些Javascript进行管理,但是我认为这个想法很有趣。

本质上,当用户在您呈现给他们的屏幕上执行动作时,Javascript将开始创建动作对象列表。一个示例可能是用户在员工信息屏幕上。他们更新姓氏并添加新地址,因为该雇员最近结婚了。在封面下产生一个ChangeEmployeeName和一个AddEmployeeMailingAddress对象到列表。用户单击“保存”以提交更改,然后提交两个对象的列表,每个对象仅包含执行每个操作所需的信息。

您将需要一个更智能的ModelBinder,然后使用默认的但更好的JSON序列化程序才能处理客户端操作对象到服务器端对象的映射。服务器端的方法(如果您处于2层环境中)可以轻松地使用完成对所使用模型的操作的方法。因此,Controller动作最终只是获取要为Model实例提取的ID和要在其上执行的动作的列表。或者,动作中包含ID,以使它们非常独立。

因此,也许这样的事情在服务器端实现了:

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实例本身可以执行正确的逻辑,或者(更可能)使用信息调用Model。

如果您在3层环境中,则可以将IUserAction设置为简单的DTO,以便跨越边界拍摄并在应用程序层上以类似的方法执行。根据您执行该层的方式,可以很容易地将其拆分并保留在事务中(想到的是Agatha的请求/响应,并利用了DI和NHibernate的身份映射)。

无论如何,我确定这不是一个完美的主意,它需要客户端上的一些JS来管理,而且我还无法做一个项目来了解它的进展情况,但是这篇文章试图考虑如何到达那里然后再回来,所以我想我会发表自己的想法。我希望它能有所帮助,并且我很想听听其他方式来管理交互。


有趣。关于ViewModel和EditModel之间的区别...您是否认为对于编辑功能,您将使用ViewModel创建表单,然后在用户发布表单时将其绑定到EditModel?如果是这样,您将如何处理由于验证错误而需要重新发布表单的情况(例如,当ViewModel包含要填充下拉菜单的元素时)-您是否还要在EditModel中包括下拉菜单元素?在这种情况下,两者之间有什么区别?
2010年

我猜您担心的是,如果我使用EditModel并出现错误,那么我必须重建我的ViewModel,这可能会非常昂贵。我想说的只是重建ViewModel并确保它有放置用户通知消息的地方(可能是肯定的也可能是否定的,例如验证错误)。如果结果是性能问题,则可以始终缓存ViewModel,直到该会话的下一个请求结束(可能是EditModel的帖子)为止。
肖恩·哥本哈根

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.