vendredi 30 octobre 2015

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?

Aucun commentaire:

Enregistrer un commentaire