I've been writing ASP.NET MVC applications for some time and I found them to be a good place for using the command pattern: we represent every user request as a command - a set of input params - then this command is processed (processing includes validation and other domain logic) and the result is sent back to the user.
Another thing I've been using in my applications is view models. I found them to be a more convenient way of passing data to the view than using domain objects as models or filling ViewData/ViewBag.
These 2 concepts work great for separating data that is shown to the user from user input and its handling, but they don't quite agree with each other in ASP.NET MVC.
Let's say I want to use commands and view models when developing a simple web store where users look through products and can order a product by providing their name and email address:
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. */ }
}
When looking through tutorials and SO I've seen people suggest several ways of doing this.
Controller:
[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);
}
View:
@using (Html.BeginForm())
{
@Html.Hidden("ProductId", Model.Id)
@Html.TextBox("Name")
@Html.TextBox("Email")
<input type="submit" value="Place order" />
}
Pros: view model and command are separated from each other, the HttpPost
method looks clean
Cons: I can't use convenient HTML helpers like @Html.TextBoxFor(model => model.Email)
, I can't use client validation (see my other question)
We copy Id
, Name
and Email
together with their validation attributes from command
to viewModel
.
Controller:
[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)
// ...
}
View:
@Html.TextBoxFor(m => m.Email)
...
Pros: all of option 1 cons go away
Cons: copying of properties seems inconvenient (what if I have 50 of them?), validation of Name
and Email
in view model (it should be done in command
where the rest of the domain logic resides), model as a POST parameter (see below)
We make command
a property of viewModel
.
Controller:
[HttpPost]
public ActionResult Product(ProductViewModel viewModel)
{
var command = viewModel.Command;
if (ModelState.IsValid)
// ...
}
View:
@Html.TextBoxFor(m => m.Command.Email)
...
Pros: all of option 1 cons go away
Cons: view model should only contain data that is displayed to the user (and command
is not displayed), model as POST parameter (see below)
--
What I don't like about options 2 and 3 is that we use a view model as a POST method parameter. This method is meant for handling user input (only 2 fields + 1 hidden in this case) and the model contains 50 more properties that I'll never use in this method and that will always be empty. Not to mention the necessity to create an empty constructor for the view model just to handle this POST request and the unnecessary memory consumption when creating large view model objects for every POST request.
My question is (that's like the longest question ever, I know): is there a secret Option 4 for properly using commands and view models that has all of the pros and none of the cons of the other ones? Or am I being paranoid and these cons are not that important and can be ignored?
ViewModel in the MVC design pattern is very similar to a "model". The major difference between "Model" and "ViewModel" is that we use a ViewModel only in rendering views. We put all our ViewModel classes in a "ViewModels" named folder, we create this folder. Understand it with an example.
MVC is a design pattern used to decouple user-interface (view), data (model), and application logic (controller). This pattern helps to achieve separation of concerns. Using the MVC pattern for websites, requests are routed to a Controller that is responsible for working with the Model to perform actions and/or retrieve data.
ASP.NET Core MVC follows the tried and tested MVC Design pattern at its core and everything revolves around it. MVC is a powerful and elegant means of separating concerns within an application. MVC Architecture splits the application into three separate and distinct layers. The Model layer, View Layer and the Controller Layer.
If we want to display more than one model on view in asp.net mvc, we need to create a new viewmodel. The following image shows a visual representation of the view model in asp.net mvc.
Seems like the only other decent way go is to use a partial view for rendering the form and use OrderProductCommand
as the view model.
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" />
}
...
This way there is no need to create a data map between view models and business objects, and the controller code can be left clean as it was in in Option 1:
[HttpGet]
public ActionResult Product(int id)
{
return View(new ProductViewModel(id));
}
[HttpPost]
public ActionResult Product(OrderProductCommand command)
{
// process command...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With