holdenmcgrohen
holdenmcgrohen

Reputation: 1063

ASP.NET MVC - Proper usage of View Model and Command pattern

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.

Option 1

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)

Option 2

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)

Option 3

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?

Upvotes: 7

Views: 2679

Answers (3)

Johnathan Barclay
Johnathan Barclay

Reputation: 20354

Here's my take on the issue.

The reason we introduce all these layers (entity, view model, command etc.), which essentially represent the same concept within different domains, is to enforce separation of concerns.

However, as each layer is introduced, we increase the complexity and margin for error, due to increased mapping between objects and distributed validation.

In my opinion it is absolutely correct that entities and view models are implemented separately; a domain entity should represent the business logic, and shouldn't be polluted with UI specific features. Similarly, a view model shouldn't contain more than is necessary than to satisfy a specific view.

There is no reason, on the other hand, that commands should introduce a new layer to your architecture. A command simply needs to provide data, doesn't need to rely on a specific implementation, and therefore can be defined as an interface:

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

Whilst a view model isn't a command, or vice-versa, a view model could act as a command:

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)
}

This way, the validation takes place only in the view model, which I think is the correct place for that to happen, as feedback can be given to the user immediately.

The command should simply transfer data, and the domain entity it mutates should validate itself anyway; a third layer of validation is unnecessary.

Your controller would then look as follows:

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 });
}

And the handler:

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

    // Other command handling methods...
}

Upvotes: 0

holdenmcgrohen
holdenmcgrohen

Reputation: 1063

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

Upvotes: 3

Slicksim
Slicksim

Reputation: 7172

Personally,

If I had to pass my model back to the view using a viewModel, I'd use option 4, inherit my view model from my command.

That way I get all the properties for my command, and I can set new properties that are just needed for the view, say dropdown list options etc.

Let inheritance do the work for you.

Also, you don't need to copy properties, in your post, don't send back a ViewModel, send back the command.

public ActionResult Product(PreOrderProductCommand command)

Don't forget, Mvc doesn't care what model is in your view, it only maps keys on the formcollection to properties in the model in the argument list. So even though you send a ProductViewModel out, you can still get a PreOrderProductCommand in.

HTH

Upvotes: 1

Related Questions