Tags: asp.net, c#, mvc, nhibernate, razor, model binding, view models, domain models, nhibernate.validator, dataannotations, validation |
Categories: ASP.Net, C#, MVC, NHibernate, Razor
Posted by
Scott on
12/19/2011 10:53 AM |
Comments (1)
We keep discussing what use a view model has and where (if at all) we should be using them. Eventually we came to some conculsions, not all of which are obvious.
Firstly a view model is important to keeping your views clean; it should provide all of the data required by a view - this avoids calling entity managers from within a view to get (for arguements sake) the list of orders for a customer. Putting this kind of code in your views is bad; it puts business logic into a file that should be exclusively UI logic, it puts executable code into a file that is not (usually) compiled, it puts code beyond the reach of (most) refactoring tools and, it creates a very confusing document.
Secondly a view model provides a buffer between our domain models and our web application - yes, our web application does have visibility of the domain models and an api via the entity manager to CRUD the entities but, should an entity be forced to implement a parameterless constructor so that binding can take place? Or (as in our case) should our protected internal parameterless .ctor (written for the exclusive use of NHibernate) now become public and therefore bypass some of the business logic that is implemented within the parameterised .ctors?
Clearly the answer is no but we can create a view model within our application that mirrors the domain model and provides the required parameterless .ctor, binding can take place, validation can happen and the only cost is calling the parameterised .ctor on the domain model, passing in the values from the view model.
Let's explain this with an example. Firstly, our domain class Client, note that the default parameterless .ctor is protected internal so it will not be available within our MVC application and therefore not available to MVC model binding. The public .ctor implements our business logic that all clients must have a forename and surname (this could be further enhanced within the .ctor by adding validation errors if either forename or surname are null or empty).
namespace Playground.Models
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate.Validator.Constraints;
public class Client : BaseEntity
{
private IList<ClientEmailAddress> emailAddresses = new List<ClientEmailAddress>();
private IList<ClientTelephoneNumber> telephoneNumbers = new List<ClientTelephoneNumber>();
// public .ctor requires forename and surname
public Client(string forename, string surname)
{
this.Forename = forename;
this.Surname = surname;
}
// internal .ctor for NHibernate. Never expose this publically.
protected internal Client() { }
// NotNullNotEmpty attribute from NHibernate.Validation.Constraints
[NotNullNotEmpty()]
public virtual string Forename { get; set; }
// NotNullNotEmpty attribute from NHibernate.Validation.Constraints
[NotNullNotEmpty()]
public virtual string Surname { get; set; }
public virtual IList<ClientEmailAddress> EmailAddresses
{
get { return this.emailAddresses; }
private set { this.emailAddresses = value; }
}
public virtual IList<ClientTelephoneNumber> TelephoneNumbers
{
get { return this.telephoneNumbers; }
private set { this.telephoneNumbers = value; }
}
}
}So, what do we do within our MVC application that wants to perform CRUD operations on our Client type? Well, we could use the type directly within our MVC controller:
namespace Playground.Web.Controllers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Playground.BusinessLogic;
using Playground.Models;
public class ClientController : Controller
{
[HttpGet]
public ActionResult Add()
{
return View();
}
[HttpPost]
public ActionResult Add(string forname, string surname)
{
if (!ModelState.IsValid)
{
return View();
}
Client client = new Client(forename, surname);
client = this.entityManager.Save(client);
if (client.ValidationErrors.Any() || client.OperationErrors.Any())
{
foreach (var validationError in client.ValidationErrors)
{
ModelState.AddModelError(validationError.Property, validationError.Error);
}
foreach (var operationError in client.OperationErrors)
{
ModelState.AddModelError(operationError.Operation, operationError.Error);
}
return View();
}
return RedirectToAction("Index");
}
}
}The issue with this approach is that checking the ModelState.IsValid property really doesn't do anything and we're all the way into the Save method before we get any validation of our object. And when we do validate we're pulling business (or possibly database) errors and their not-so-user-friendly messages into our UI. The typical solution is to use the domain model for the parameter on the post method of the controller (and as the model in the view) which automagically wires up form values with properties on the model. This type of model binding requires a default parameterless .ctor which now doesn't exist because it would break the business rules on the Client type.
So what we really want is to use types throughout our MVC application, use binding within our controllers and, not break our business rules. If we create a model within our MVC application - a view model - we can solve our issue and get early validation with user friendly messages into the bargin. Note the namespace and the implicit default parameterless .ctor.
namespace Playground.Web.ViewModels
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
public class Client
{
// Required is from the System.ComponentModel.DataAnnotations namespace.
[Required(ErrorMessage="This is a user friendly validation message")]
public virtual string Forename { get; set; }
[Required()]
public virtual string Surname { get; set; }
}
}Now we can update our controller to use our view model:
namespace Playground.Web.Controllers
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Playground.BusinessLogic;
using Playground.Models;
public class ClientController : Controller
{
[HttpGet]
public ActionResult Add()
{
return View(new Playground.Web.ViewModels.Client());
}
[HttpPost]
public ActionResult Add(Playground.Web.ViewModels.Client postedClient)
{
if (!ModelState.IsValid)
{
return View(postedClient);
}
Client client = new Client(postedClient.Forename, postedClient.Surname);
client = this.entityManager.Save(client);
if (client.ValidationErrors.Any() || client.OperationErrors.Any())
{
foreach (var validationError in client.ValidationErrors)
{
ModelState.AddModelError(validationError.Property, validationError.Error);
}
foreach (var operationError in client.OperationErrors)
{
ModelState.AddModelError(operationError.Operation, operationError.Error);
}
return View(postedClient);
}
return RedirectToAction("Index");
}
}
}Now when we check ModelState.IsValid we're checking the attributes for the view model before we try to save the domain object.We also get the advantage of using model binding with the view like this:
@model Playground.Web.ViewModels.Client
@{
ViewBag.Title = "Add";
}
Add
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
}
@Html.ActionLink("Back to List", "Index")
And we have the advantage where we can add properties to the view model that aren't required on the domain model; the user input on a registration form might require the password to be confirmed (an attempt at mitigating typos resulting in the user setting their password incorrectly) so a ConfirmPassword property can be added to the view model together with a validator but the domain model need not be sullied with this non-persisted, UI only property.
528ce65e-be58-4276-a549-c6f899d7054f|0|.0