我已经编写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. */ }
}
在浏览教程时,我发现有人提出了几种方法。
控制器:
[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)
我们将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参数(见下文)
我们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 用于正确使用命令和查看模型拥有所有的优点,没有其他的缺点?或者我是偏执狂,这些缺点并不重要,可以忽略不计?
答案 0 :(得分:3)
似乎唯一正确的方法是使用局部视图渲染表单并使用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...
}
答案 1 :(得分:1)
就个人而言,
如果我必须使用viewModel将模型传递回视图,我使用选项4,从我的命令继承我的视图模型。
这样我获得了命令的所有属性,并且我可以设置视图所需的新属性,比如下拉列表选项等。
让继承为你工作。
此外,您不需要复制属性,在您的帖子中,不要发送回ViewModel,发回命令。
public ActionResult Product(PreOrderProductCommand command)
别忘了,Mvc并不关心视图中的模型,它只将formcollection上的键映射到参数列表中模型中的属性。因此,即使您发送了ProductViewModel,您仍然可以获得PreOrderProductCommand。
HTH
答案 2 :(得分:0)
这是我的问题。
之所以引入所有这些层(实体,视图模型,命令等),其本质上代表了不同领域中的相同概念,是为了加强关注点分离。
但是,由于引入了每一层,由于对象之间的映射增加和分布式验证,我们增加了错误的复杂性和容错性。
我认为,实体和视图模型分别实现是绝对正确的;域实体应该代表业务逻辑,并且不应受到UI特定功能的污染。同样,视图模型不应包含满足特定视图所需的内容。
另一方面,没有理由命令应该为您的体系结构引入新的层。命令只需要提供数据,而不必依赖特定的实现,因此可以定义为接口:
interface IOrderProductCommand
{
int ProductId { get; }
string Name { get; }
string Email { get; }
}
虽然视图模型不是命令,反之亦然,但视图模型可以 act 作为命令:
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...
}