ASP.NET MVC-视图模型和命令模式的正确使用

本文关键字:模式 命令 MVC- NET 视图 模型 ASP | 更新日期: 2023-09-27 18:19:49

我编写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. */ }
}

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

选项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.TextBoxFor(model => model.Email)这样方便的HTML助手,我不能使用客户端验证(请参阅我的另一个问题)

选项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的所有缺点都会消失

Cons:复制属性似乎不方便(如果我有50个属性怎么办?),在视图模型中验证NameEmail(应该在域逻辑的其余部分所在的command中进行),将模型作为POST参数(见下文)

选项3

我们使CCD_ 11成为CCD_ 12的一个性质。

控制器:

[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可以正确使用命令和视图模型,它具有其他命令和视图的所有优点而没有缺点?或者我是偏执狂,而这些缺点并没有那么重要,可以忽略?

ASP.NET MVC-视图模型和命令模式的正确使用

似乎唯一其他合适的方法是使用部分视图来呈现表单,并使用OrderProductCommand作为视图模型。

产品.cshtml:

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

Product_OrderForm.cs.html:

@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...
}

就个人而言,

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

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

让继承为你做事吧。

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

public ActionResult Product(PreOrderProductCommand command)

别忘了,Mvc并不关心视图中的模型,它只将表单集合上的键映射到参数列表中模型中的属性。因此,即使您发送了一个ProductViewModel,您仍然可以在.中获得一个PreOrderProductCommand

HTH

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

我们之所以引入所有这些层(实体、视图模型、命令等),本质上代表不同领域中的相同概念,是为了强制分离关注点。

然而,随着每一层的引入,由于对象之间的映射和分布式验证的增加,我们增加了复杂性和误差幅度。

在我看来,实体和视图模型分别实现是绝对正确的;域实体应该表示业务逻辑,并且不应该被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...
}