【问题标题】:ASP.NET MVC - Proper usage of View Model and Command patternASP.NET MVC - 正确使用视图模型和命令模式
【发布时间】:2016-01-30 14:25:46
【问题描述】:

我已经编写 ASP.NET MVC 应用程序有一段时间了,我发现它们是使用命令模式的好地方:我们将每个用户请求表示为一个命令 - 一组输入参数 - 然后这个命令是处理(处理包括验证和其他域逻辑)并将结果发送回用户。

我在我的应用程序中使用的另一件事是视图模型。我发现它们是向视图传递数据的一种比使用域对象作为模型或填充 ViewData/ViewBag 更方便的方法。

这两个概念非常适合将显示给用户的数据与用户输入及其处理分开,但它们在 ASP.NET MVC 中并不完全一致。

假设我想在开发一个简单的网上商店时使用命令和查看模型,用户可以在其中浏览产品并可以通过提供他们的姓名和电子邮件地址来订购产品:

class ProductViewModel 
{
    public ProductViewModel(int id) { /* init */ }
    public int Id { get; set; }
    public string Name { get; set; }
    // a LOT of other properties (let's say 50)
}

class OrderProductCommand
{
    public int ProductId { get; set; }

    [Required(ErrorMessage = "Name not specified")]
    public string Name { get; set; }

    [Required(ErrorMessage ="E-Mail not specified")]
    public string Email { get; set; }

    public CommandResult Process() { /* validate, save to DB, send email, etc. */ }
}

在浏览教程时,我看到人们提出了几种方法。

选项 1

控制器:

[HttpGet]
public ActionResult Product(int id)
{
    return View(new ProductViewModel(id));
}

[HttpPost]
public ActionResult Product(OrderProductCommand command)
{
    if (ModelState.IsValid)
    {
        var result = command.Process();
        if(result.Success)
            return View("ThankYou");
        else
            result.CopyErrorsToModelState(ModelState);
    }
    return Product(command.Id);
}

查看:

@using (Html.BeginForm())
{
    @Html.Hidden("ProductId", Model.Id)
    @Html.TextBox("Name")
    @Html.TextBox("Email")
    <input type="submit" value="Place order" />
}

优点:视图模型和命令相互分离,HttpPost 方法看起来很干净

缺点:我不能使用方便的 HTML 帮助程序,例如 @Html.TextBoxFor(model =&gt; model.Email),我不能使用客户端验证(请参阅 my other question

选项 2

我们将IdNameEmail 及其验证属性从command 复制到viewModel

控制器:

[HttpPost]    
public ActionResult Product(ProductViewModel viewModel)
{
        var command = new OrderProductCommand();
        command.Id = viewModel.Id;
        command.Name = viewModel.Name;
        command.Email = viewModel.Email;        
        if (ModelState.IsValid)
        // ...
}

查看:

@Html.TextBoxFor(m => m.Email)
...

优点:选项 1 的所有缺点都消失了

缺点:复制属性似乎不方便(如果我有 50 个呢?),在视图模型中验证 NameEmail(应该在 command 中完成)其余的域逻辑驻留),模型作为 POST 参数(见下文)

选项 3

我们将command 设为viewModel 的属性。

控制器:

[HttpPost]
public ActionResult Product(ProductViewModel viewModel)
{
        var command = viewModel.Command;
        if (ModelState.IsValid)
        // ...
}

查看:

@Html.TextBoxFor(m => m.Command.Email)
...

优点:选项 1 的所有缺点都消失了

缺点:视图模型应该只包含向用户显示的数据(而不显示command),模型作为POST参数(见下文)

--

我不喜欢选项 2 和 3 的地方是我们使用视图模型作为 POST 方法参数。此方法用于处理用户输入(在这种情况下只有 2 个字段 + 1 个隐藏),并且模型包含另外 50 个属性,我将永远不会在此方法中使用这些属性,这些属性将始终为空。更不用说需要为视图模型创建一个空的构造函数来处理这个 POST 请求,以及在为每个 POST 请求创建大型视图模型对象时不必要的内存消耗。

我的问题是(这是有史以来最长的问题,我知道):是否有一个秘密 选项 4 用于正确使用具有所有优点和没有其他的缺点?还是我是偏执狂,这些缺点并不重要,可以忽略?

【问题讨论】:

  • 你不能在局部视图中使用选项 1 吗?如果您的命令表单是与页面其余部分隔离的块,您可以在部分视图中提取命令表单,该视图将使用 OrderProductCommand 作为模型。你可以这样称呼它:@{Html.RenderPartial("OrderProductCommand", new OrderProductCommand());}。这允许您使用 html 帮助器,如您所说:@Html.TextBoxFor(model => model.Email).
  • @PaulDS 这可能真的有效。唯一我不太喜欢的是必须为每个表单创建一个单独的视图,但我想这将是轻而易举的事情。
  • 我不认为为每个表单分离视图是一件坏事。如果表单显示主视图模型之外的其他内容,则它应该在其他视图中分开。这会减小视图的大小(可能会很大,尤其是当您有 50 个要显示的属性时),这对项目的可维护性更好。
  • @holdenmcgrohen 我很好奇 - 两年后的今天你是怎么做到的?另外,您如何使用嵌套视图模型/命令来做到这一点?

标签: c# asp.net-mvc design-patterns command-pattern


【解决方案1】:

似乎唯一其他不错的方法是使用局部视图来呈现表单并使用OrderProductCommand作为视图模型。

Product.cshtml:

@model ProductViewModel
...
@Html.Partial("Product_OrderForm", new OrderProductCommand { ProductId = Model.Id })
...

Product_OrderForm.cshtml:

@model OrderProductCommand
...
@using (Html.BeginForm("Product", "Home"))
{
    @Html.HiddenFor(cmd => cmd.ProductId)
    @Html.TextBoxFor(cmd => cmd.Name)
    @Html.TextBoxFor(cmd => cmd.Email)
    <input type="submit" value="Place order" />
}
...

这样就不需要在视图模型和业务对象之间创建数据映射,并且控制器代码可以像在选项1中一样保持干净:

[HttpGet]
public ActionResult Product(int id)
{
    return View(new ProductViewModel(id));
}

[HttpPost]
public ActionResult Product(OrderProductCommand command)
{
    // process command...
}

【讨论】:

    【解决方案2】:

    个人,

    如果我必须使用 viewModel 将我的模型传递回视图,我会使用选项 4,从我的命令继承我的视图模型。

    这样我可以获得命令的所有属性,并且可以设置视图所需的新属性,例如下拉列表选项等。

    让继承为您完成工作。

    另外,您不需要复制属性,在您的帖子中,不要发回 ViewModel,发回命令。

    public ActionResult Product(PreOrderProductCommand command)
    

    别忘了,Mvc 并不关心你视图中的模型是什么,它只会将 formcollection 上的键映射到参数列表中模型中的属性。因此,即使您将 ProductViewModel 发送出去,您仍然可以获得 PreOrderProductCommand 。

    HTH

    【讨论】:

    • 如果您有多个可用于同一视图模型的命令(在 C# 中不提供多重继承,并且您可能有多个包含相同属性的命令),则此解决方案不起作用。
    • 从概念、OO 的角度来看,这也不好。通过这样做,您说的是 ViewModel Is A 命令。没有。
    • 鉴于每个视图模型有多个命令,那么我会选择选项 3。视图模型不一定只包含要显示给用户的数据,它更像是一个模型来构建查看从。
    【解决方案3】:

    这是我对这个问题的看法。

    我们引入所有这些层(实体、视图模型、命令等)的原因是为了强制分离关注点。

    但是,随着每一层的引入,由于对象之间的映射和分布式验证的增加,我们增加了复杂性和容错率。

    在我看来,实体和视图模型分开实现是绝对正确的;域实体应该代表业务逻辑,并且不应该被 UI 特定功能污染。同样,视图模型所包含的内容不应超过满足特定视图所需的内容。

    另一方面,命令没有理由为您的架构引入一个新层。命令只需要提供数据,不需要依赖具体的实现,因此可以定义为接口:

    interface IOrderProductCommand
    {
        int ProductId { get; }
        string Name { get; }
        string Email { get; }
    }
    

    虽然视图模型不是命令,反之亦然,但视图模型可以充当命令:

    class ProductViewModel : IOrderProductCommand
    {
        public int ProductId { get; set; }
    
        [Required(ErrorMessage = "Name not specified")]
        public string Name { get; set; }
    
        [Required(ErrorMessage ="E-Mail not specified")]
        public string Email { get; set; }
    
        public ProductViewModel(int id) { /* init */ }
    
        // a LOT of other properties (let's say 50)
    }
    

    这样,验证只发生在视图模型中,我认为这是正确的地方,因为可以立即向用户提供反馈。

    该命令应该简单地传输数据,并且它变异的域实体无论如何都应该验证自己;不需要第三层验证。

    您的控制器将如下所示:

    readonly CommandHandler _handler;
    
    public YourController(CommandHandler handler)
    {
        _handler = handler;
    }
    
    [HttpGet]
    public ActionResult Product(int id)
    {
        return View(new ProductViewModel(id));
    }
    
    [HttpPost]
    public ActionResult Product(ProductViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }
    
        _handler.HandleProductCommand(model);
    
        return RedirectToAction(nameof(Product), new { id = model.ProductId });
    }
    

    还有处理程序:

    class CommandHandler
    {
        void HandleProductCommand(IOrderProductCommand command)
        {
            // Update domain...
        }
    
        // Other command handling methods...
    }
    

    【讨论】:

      猜你喜欢
      • 2010-11-24
      • 1970-01-01
      • 1970-01-01
      • 2011-10-24
      • 1970-01-01
      • 2013-05-09
      • 1970-01-01
      • 2012-07-10
      • 2010-11-18
      相关资源
      最近更新 更多