【发布时间】: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 => model.Email),我不能使用客户端验证(请参阅 my other question)
选项 2
我们将Id、Name 和Email 及其验证属性从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 个呢?),在视图模型中验证 Name 和 Email(应该在 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