mvc-demystified

A collection of articles designed to uncover ASP.NET MVC's secrets.

What are Areas? - ASP.NET MVC Demystified

ASP.NET MVC relies on certain conventions (both naming and placement) to properly route requests and return views and view models. One of the ways it allows us programmers to manage these conventions is with the use of Areas, which are modules within an MVC project that exist with their own controllers, views, and actions. Let's learn how we can add Areas to an existing project, and what their separation from the main project allows us to do.

What are Areas?

Areas are pieces of an MVC application that do not use the main controllers, actions, and views; rather, they mimic this structure within a specific subfolder in the project directory. In general, we use Areas to split out "semi-independent" sections of our applications, so that they can be maintained and upgraded separately from the rest of the app. Having an area has no functional impact (other than routing, which we will discuss in a bit).

In Visual Studio, we can scaffold an area by right-clicking on the project file and selecting Add -> Area.

A screenshot of an MVC project structure, showing the Areas folder and Billing area

As you might have guessed, Areas are especially useful in larger applications, where these semi-independent portions of the app might be greater in both scope and number.

Registering an Area

Notice that, in the earlier screenshot, the Billing area is a subfolder under the Areas folder. This structure is intentional; an Area by default must exist in the Areas folder and be named appropriately. This is in keeping with MVC's general Separation of Concerns ideology.

Under the Billing folder are folders for Controllers, Models, and Views, as well as another file called BillingAreaRegistration.cs. This file is what registers the new Billing area with the ASP.NET MVC framework. Here's what it looks like:

namespace MVCAreasDemo.Areas.Billing  
{
    public class BillingAreaRegistration : AreaRegistration 
    {
        public override string AreaName 
        {
            get 
            {
                return "Billing";
            }
        }

        public override void RegisterArea(AreaRegistrationContext context) 
        {
            context.MapRoute(
                "Billing_default",
                "Billing/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

The important part of this file is the RegisterArea() method. In that method, we set up the routes for the area, just like we do for the general application in the RouteConfig file. However, just having this registration isn't enough. We also need to make the following call in our Global.asax file:

protected void Application_Start()  
{
    AreaRegistration.RegisterAllAreas(); //Registers all Areas in the Areas folder
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

With both of those pieces, the Billing area becomes accessible in our MVC app.

Controllers in Areas

Notice that any route that directs to this new Billing area must begin with the "Billing" area name. This is so the convention-based routing system can identify which Area to route to when receiving a request like this.

Let's add a simple controller to the Billing area.

public class BillingMainController : Controller  
{
    // GET: Billing/BillingMain
    public ActionResult Index()
    {
        return View();
    }
}

Now that looks pretty darn similar to any other controller we might add to the main MVC app. Areas are only distinguished from the rest of an MVC application by their folder placement and routing; otherwise they behave the same way. But how can we create links to actions in Areas?

Linking to Area Actions

If we want to create links on a Razor view to a controller action in an Area, we need to use an additional parameter called Area:

@Html.ActionLink("BillingMain", "Index", new { Area = "Billing" })

If we click on that link, the follow page shows up:

A screenshot from Chrome, showing the route

Summary

Areas in ASP.NET MVC apps are used to give better structure to our large applications. They exist as modules independent of the main app, with their own controllers, folders, routes, and views. The only setup we need to do is to register the area with the framework, both in the Area's Registration file and in our Global.asax.

For more info, including a discussiong of what happens when two routes conflict with one another, check out Chapter 9 of Professional ASP.NET MVC. You can also take a look at the sample project I whipped up over on GitHub.

Happy Coding!

Using Partial Views - ASP.NET MVC Demystified

When I was writing applications in WebForms (which, thankfully, I no longer do) I often ended up used .ascx files, commonly known as User Controls. Once I moved to MVC, I had to get used to a similar (though not the same) concept: Partial Views.

In this tutorial, we'll walk through a use case for Partial Views and explain what they are and why we need to use them. We'll also walk through the two different render methods: @Html.Partial() and @Html.RenderPartial() and discuss how they are different. Let's get started!

What are Partial Views?

Partial views are .cshtml views that are meant to represent portions of a page that can be used on multiple other views. They are roughly analogous to WebForms' User Controls, but not exactly, mostly because (just like all other views) they don't have a code-behind file.

When you create a partial view, the reason for it to exist is normally that it will be used on many other "parent" views. Let's set up a scenario for this.

Entrees and Ingredients

Say I want to display a list of entrees that I can make for dinner, and along with these entrees I'd like to display the ingredients necessary to make them. In order to do this, I might create classes for Entree and Ingredient like so:

public class Entree  
{
    public int ID { get; set; }
    public string Name { get; set; }
    public List<Ingredient> Ingredients { get; set; }

    public Entree()
    {
        Ingredients = new List<Ingredient>();
    }
}

public class Ingredient  
{
    public string Name { get; set; }
    public decimal Amount { get; set; }
    public string Unit { get; set; }
}

I want to create two views for this: an Index view that will display all the entrees and a Details that will just display one. On each of these views will need to be a list of ingredients, with their units and amounts. Because that display is the same on each view, we can turn that display into a partial view. In our particular case, our partial view will need to have a model bound to it, since it will be displaying the ingredients; of course, it's entirely possible to not use a model on a partial view.

Our partial view (let's call it Ingredients.cshtml) will look like this:

@model List<PartialViewsDemo.Models.Ingredient>

@{
    string display = "";
    foreach (var ingredient in Model)
    {
        display += ingredient.Name + " (" + ingredient.Amount.ToString() + " " + ingredient.Unit + "), ";
    }
    display = display.Remove(display.LastIndexOf(","));
}

<span>@display</span>  

Because this is a partial view, MVC expects it to exist in the Views/Shared folder:

A snapshot of the Views/Shared folder, showing that the Ingredients.cshtml file exists there

Now that we've got our partial view defined, we can define the Entree controller and the Index and Details actions and views. First, here's the Entree controller:

public class EntreeController : Controller  
{
    [HttpGet]
    public ActionResult Index()
    {
        var entrees = EntreeManager.GetAll();
        return View(entrees);
    }

    [HttpGet]
    public ActionResult Details(int id)
    {
        try
        {
            var entree = EntreeManager.GetAll().First(x => x.ID == id);
            return View(entree);
        }
        catch(Exception ex)
        {
            TempData["ErrorMessage"] = "No Entree with ID " + id.ToString() + " exists.";
            return RedirectToAction("Index");
        }
    }
}

In order to use the partial view Ingredients on the Index.cshtml view, we make a call to @Html.Partial(), as shown in the view below:

@model List<PartialViewsDemo.Models.Entree>

@{
    ViewBag.Title = "Index";
}

<h2>All Entrees</h2>

@if(TempData["ErrorMessage"] != null)
{
    <span>@TempData["ErrorMessage"]</span>
}

<table>  
    <thead>
        <tr>
            <th>
                Name
            </th>
            <th>
                Ingredients
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach(var entree in Model)
        {
            <tr>
                <td>
                    @Html.ActionLink(entree.Name, "Details", new { id = entree.ID })
                </td>
                <td>
                    @Html.Partial("Ingredients", entree.Ingredients)
                </td>
            </tr>
        }
    </tbody>
</table>  

On the Details.cshtml view, we make a similar call:

@model PartialViewsDemo.Models.Entree

@{
    ViewBag.Title = "Details";
}

@Html.ActionLink("<< Back to Index", "Index")

<h2>@Model.Name</h2>

<p>Ingredients: @Html.Partial("Ingredients", Model.Ingredients)</p>  

With all of this in place, we can see our completed Index and Details pages.

The Entrees/Index page, showing all three Entrees and the partial call to Ingredients.cshtml

The Entrees/Details/2 page, showing the details for Entree 2 and the partial call to Ingredients.cshtml

An Alternate Rendering Method

In the previous examples we used a call to @Html.Partial() to render the partial views. There is also another call we can make, @Html.RenderPartial(), that will render the partial view in a different manner.

We would use it like this (here's a snippet from the Details view):

<p>Ingredients: @{ Html.RenderPartial("Ingredients", Model.Ingredients); }</p>  

Note that we had to use this call in server-side braces @{}. There's a couple of other differences between @Html.Partial() and @Html.RenderPartial():

  • @Html.Partial() returns a string (meaning you can store the output in a variable), whereas @Html.RenderPartial() returns void and writes directly to the Response.
  • Performance-wise @Html.RenderPartial() will perform better (per an answer on StackOverflow)

I almost always end up using [email protected]() just because I like the syntax better, but the decision is left to you.

Summary

Partial Views in ASP.NET MVC are meant to represent "pieces" of a page that can be rendered in multiple places on different views. They can have models bound to them, and can be rendered using @Html.Partial() or @Html.RenderPartial().

If you'd like to see this demo in a working MVC project, you can download the source from GitHub.

Happy Coding!

Attribute Routing vs Convention Routing - ASP.NET MVC Demystified

For most of ASP.NET MVC's lifetime, routing has been accomplished via Convention Routing, which allows developers to specify a format or group of formats which can be used to parse incoming URLs and determine the appropriate actions, controllers, and data to use for that request.

In MVC 5, though, Microsoft introduced another scheme called Attribute Routing. Attribute Routing allows us to define routes in close proximity to their actions, giving us greater flexibility.

Let's dive into Routing as a whole, and then show how to implement Convention Routing and Attribute Routing and why they actually work together nicely.

A signpost, written in Scottish Gaelic, showing pedestrian walking paths in the town of Stornoway, Scotland.

What is Routing?

ASP.NET Routing is the ability to have URLs represent abstract actions rather than concrete, physical files.

In "traditional" websites, every URL represents a physical file, whether it is an HTML or ASPX page, or a script file, or some other content. If I see a URL of www.example.com/articles/post.aspx?id=65, I'm going to assume that that URL represents a folder called articles at the root of the site, and within that folder a page called post.aspx.

In MVC, no such physical folders and pages exist, and so the MVC architecture allows us to map routes to controllers and actions which may represent many kinds of results. Routing is a layer of abstraction on top of regular URLs that allows programmers greater control over what those URLs mean and how they are formatted.

"Hackable" URLs

One of the things Routing allows us to do is to create "hackable" URLs; that is, URLs whose meaning is easily read, understood, and extended upon by human beings. We can use Routing to turn this messy URL:

www.example.com/article.aspx?id=69&title=my-favorite-podcasts

into a much cleaner one:

www.example.com/articles/69/my-favorite-podcasts

The concept of "hackable" URLs goes a bit further, too. If I was a user looking at the clean URL, I might append "/comments" on the end of it:

www.example.com/articles/69/my-favorite-podcasts/comments

"Hackable" URLs implies that this should display the comments for that article. If it does, then I (the user) have just discovered how I can view the comments for any given article, without needing to scroll through the page and hunt down the appropriate link.

So how do we actually implement Routing in MVC? It all starts with something called the Route Table.

The Route Table

The sign post outside Big Ben clocktower in London The Route Table is a collection of all possible routes that MVC can use to match submitted URLs. Items in the Route Table specify their format and where certain values are in the route structure. These values can then be mapped to controllers, actions, areas, etc. depending on their placement within the route.

Any URL that is submitted to the application will be compared to the routes in the Route Table, and the system will redirect to the first matching route found in that table. In versions of MVC up to version 5, we added routes to this table at a specific place, usually in RouteConfig. With the introduction of Attribute Routing, this method of adding routes has been retroactively termed Convention Routing.

Convention Routing

Convention Routing approaches the routing problem general-case-first; by default, you are given a route that will probably match most if not all of your routes, and are asked to define if there are any more specific routes you would also like to handle.

A call to set up convention-based routes might look like this:

public static void RegisterRoutes(RouteCollection routes)  
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Special",
        url: "Special/{id}",
        defaults: new { controller = "Home", action = "Special", id = UrlParameter.Optional }
    ); //Route: /Special/12

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    ); //Route: /Home/Index/12
}

Let's break down some of the pieces here:

  • routes is the Route Table of type RouteCollection, and stores all of the possible routes we can match for a given URL.
  • A call to IgnoreRoute allows us to tell ASP.NET to ignore any URL that matches that tokenized structure and process it normally.
  • A call to MapRoute allows us to add a route to the route table.

MapRoute includes a few parameters: a name of the route that must be unique to each route, a tokenized URL structure, and default values that will be applied if alternate values are not supplied by the route. The tokenized URL structure is then used to match supplied values in the URL.

Matching by Convention

Say we have this route:

routes.MapRoute(  
    name: "PersonDefault",
    url: "{controller}/{person}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Now imagine we have this controller:

public class HomeController : Controller  
{
    public ActionResult Index(string person) { ... }
    public ActionResult Documents(string person, int id) { ... }
}

If we submit a URL of /Home/Dude-Person/Documents/17, we will be directed to the Home controller's Documents action, and the person and id parameters will have values of "Dude-Person" and 17 respectively.

If we submit a URL of /Home/Dude-Person/Documents?id=17, we will again be directed to Home controller and Documents action with the same values as before, because MVC will look at query string values if no route values exist that match the expected parameters.

If we submit a URL of /Home/Dude-Person, we will be directed to the Index action (because that's what was specified in the defaults) with parameter person having the value "Dude-Person".

If we submit a URL of /Home we will be redirected to the Index action and person will be an empty string. If no matching value is found for a given parameter, the default value for that parameter's type is used.

One thing to keep in mind when designing your routes is that the order in which the routes are added to the table matters. The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route. Therefore, less common or more specialized routes should be added to the table first, while more general routes should be added later on.

For example, for the routes above, if we submit a URL of /Home/Documents we will be redirected to the Index action with parameter person having the value "Documents", which is probably not the desired behavior.

In short, Convention Routing approaches Routing from the general case; you generally add some routes that will match all or most of your URLs, then add more specific routes for more specialized cases. The other way to approach this problem is via Attribute Routing.

Attribute Routing

Attribute Routing (introduced in MVC 5) is the ability to add routes to the Route Table via attributes so that the route definitions are in close proximity to their corresponding actions. We will still populate the Route Table, but we will do it in a different manner.

A signpost showing the stops along the Ridgeway National Trail in England

Before we can start using Attribute Routing, though, we must first enable it.

Enable Attribute Routing

If you want to use Attribute Routing, you have to enable it by calling MapMvcAttributeRoutes on the RouteCollection for your app (usually this is done in RouteConfig):

public class RouteConfig  
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapMvcAttributeRoutes(); //Enables Attribute Routing
    }
}

Simple Example

A simple example of Attribute Routing might look like this:

public class HomeController : Controller  
{
    [Route("Users/Index")] //Route: /Users/Index
    public ActionResult Index() { ... }
}

What that [Route] attribute does is specify a route to be added to the Route Table which maps to this action. The parameters to [Route]'s constructor are where the real functionality happens.

Parameters

For example, what if we need a way to specify that we want to include parameter data in the routes? We can do so with the {} syntax:

[Route("Users/{id}")] //Route: /Users/12
public ActionResult Details(int id) { ... }  

Notice that the name in the curly braces matches the name of one of the inputs to the action. By doing this, that value of that parameter will appear in the route rather than in the query string.

We can also specify if a parameter is optional by using ?:

[Route("Users/{id}/{name?}")] //Route: /Users/12/Matthew-Jones or /Users/12
public ActionResult Details(int id, string name) { ... }  

If we need a given parameter to be of a certain type, we can specify a constraint:

[Route("Users/{id:int}")] //Route: /Users/12
public ActionResult Details(int id) { ... }

[Route("{id:alpha}/Documents")] //Route: /product/Documents
public ActionResult Documents(string id) { ... }  

There are quite a few different constraints we can use; Attribute Routing even includes support for regular expressions and string lengths. Check this article from MSDN for full details.

Route Prefixes

We can specify a RoutePrefix that applies to every action in a controller:

[RoutePrefix("Users")]
public class HomeController : Controller  
{
    [Route("{id}")] //Route: /Users/12
    public ActionResult Details(int id) { ... }
}

If we need to have an action that overrides the Route Prefix, we can do so using the absolute-path prefix ~/:

[RoutePrefix("Users")]
public class HomeController : Controller  
{    
    [HttpGet]
    [Route("~/special")] //Route: /special
    public ActionResult Special() { ... }
}

Default Routes

Specifying the default route for the application also uses the absolute-path prefix ~/. We can also specify a default route for a given route prefix by passing an empty string:

[RoutePrefix("Users")]
public class HomeController : Controller  
{
    [Route("~/")] //Specifies that this is the default action for the entire application. Route: /
    [Route("")] //Specifies that this is the default action for this route prefix. Route: /Users
    public ActionResult Index() { ... }
}     

We can also specify default routes another way: by capturing them as inputs.

[RoutePrefix("Users")] 
[Route("{action=index}")] //Specifies the Index action as default for this route prefix
public class HomeController : Controller  
{
    public ActionResult Index()
    {
        return View();
    }
}

Names and Order

[Route] will also accept names and order values for the routes:

[RoutePrefix("Users")]
public class HomeController : Controller  
{
    [Route("Index", Name = "UsersIndex", Order = 2)]
    public ActionResult Index() { ... }

    [Route("{id}", Name = "UserDetails", Order = 1)]
    public ActionResult Details(int id) { ... }
}

Order is still very important! In the above example, if we give a route of /Users/Index, we will get an exception because that matches the UserDetails route, which has a higher order. If no order is specified, the routes are inserted into the Route Table in the order they are listed.

We can solve the above conflict by either adding a constraint on UserDetails that ID must be an integer or reordering the routes and placing UsersIndex at a higher order.

Because each of the defined routes is close to their respective action, it is my recommendation that each route be as specific as possible. In other words, when using Attribute Routing, define the routes to be specific to the decorated action and not any other action.

Using Convention and Attribute Routing Together

You can also implement both Attribute and Convention routing at the same time:

public class RouteConfig  
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        ...
        routes.MapMvcAttributeRoutes(); //Attribute routing
        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Notice that the setup above gives all the Order weight to the Attribute routes, since they were added to the Route Table first.

Which is Better?

In this developer's opinion, Attribute Routing is a more flexible solution than Convention Routing, if only because it allows you quite a bit more flexibility and places the routes next to the actions that will actually use them. However there are certainly benefits to using both in tandem, particularly in situations when you know how some routes will look but aren't sure about others.

But don't take my opinion as gospel; try them both for yourself!

Chapter 9 of Professional ASP.NET MVC 5 has some more detail on Attribute Routing vs. Convention Routing, including some good guidelines on when to choose one or the other and how to deal with routing in Areas. Go take a look; having another reference book never hurts.

Happy Coding!

What is Unobtrusive Validation? - ASP.NET MVC Demystified

One of the more useful things MVC includes is Unobtrusive Validation with the usage of the jQuery Validate plugin and the Unobtrusive library. This lightweight library allows us to add validation to our MVC views without any additional client-side coding; we only have to use attributes like RequiredAttribute and RangeAttribute and include the correct script files. In this post, we'll take a look at a simple example of how to use Unobtrusive Validation, and break down how it actually works.

What Does "Unobtrusive Validation" Mean?

It means that we can implement simple client-side validation without writing a ton of validation code, and that we can improve the user experience simply by adding the appropriate attributes and including the appropriate script files.

Consider the following view model (we will use this in the sample project):

ViewModels/Home/AddUserVM.cs
public class AddUserVM  
{
    [DisplayName("First Name:")]
    [Required(ErrorMessage = "Please enter the first name.")]
    public string FirstName { get; set; }

    [DisplayName("Last Name:")]
    [Required(ErrorMessage = "Please enter the last name.")]
    public string LastName { get; set; }

    [DisplayName("Age:")]
    [Range(12, 120, ErrorMessage = "You must be between 12 and 120 years of age.")]
    public int Age { get; set; }

    [DisplayName("Email:")]
    [Required(ErrorMessage = "Please enter an email address.")]
    [EmailAddress(ErrorMessage = "Please enter a valid email address.")]
    public string EmailAddress { get; set; }
}

This class is already set up to handle server-side validation using attributes. Unobtrusive Validation allows us to take the already-existing validation attributes and use them client-side to make our user experience that much nicer.

The Unobtrusive script files are included automatically with new MVC projects in Visual Studio, but if you don't have them you can get them from NuGet.

We need three sets of files to implement Unobtrusive:

  • The jQuery library
  • The jQuery Validate plugin
  • The Unotrusive extensions to Validate

Once we've got those, we can add unobtrusive validation to any page by simply including those script files on that page. Even better, we can use MVC's bundling feature to just create and use a bundle, which is what happens by default in a new MVC app:

App_Start/BundleConfig.cs
public static void RegisterBundles(BundleCollection bundles)  
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                "~/Scripts/jquery-{version}.js"));

    bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                "~/Scripts/jquery.validate*"));
}

Now, all we have to do is place that bundle on the appropriate view to enable unobtrusive validation.

Simple, right? But how does it actually work?

How Does It Work?

Let's first look at what happens without Unobtrusive. Say we have this view:

Views/Home/ServerSide.cshtml
@model UnobtrusiveDemo.ViewModels.Home.AddUserVM

@{
    ViewBag.Title = "Add a User (Server-Side)";
}

<h2>Add a User (Server-Side Validation)</h2>

@using (Html.BeginForm())
{
    <div>
        <div>
            @Html.LabelFor(x => x.FirstName)
            @Html.TextBoxFor(x => x.FirstName)
            @Html.ValidationMessageFor(x => x.FirstName)
        </div>
        <div>
            @Html.LabelFor(x => x.LastName)
            @Html.TextBoxFor(x => x.LastName)
            @Html.ValidationMessageFor(x => x.LastName)
        </div>
        <div>
            @Html.LabelFor(x => x.Age)
            @Html.TextBoxFor(x => x.Age)
            @Html.ValidationMessageFor(x => x.Age)
        </div>
        <div>
            @Html.LabelFor(x => x.EmailAddress)
            @Html.TextBoxFor(x => x.EmailAddress)
            @Html.ValidationMessageFor(x => x.EmailAddress)
        </div>
        <div>
            <input type="submit" value="Save" />
        </div>
    </div>
}

Our controller actions look like this:

Controllers/HomeController.cs
[HttpGet]
public ActionResult ServerSide()  
{
    AddUserVM model = new AddUserVM();
    return View(model);
}

[HttpPost]
public ActionResult ServerSide(AddUserVM model)  
{
    if(!ModelState.IsValid) //Check for validation errors
    {
        return View(model);
    }
    return RedirectToAction("Index");
}

The POST action is super simple; we check for validation errors, and if there are any, we simply return the view.

Disabling Client-Side Validation in the Web.Config

We can disable client-side validation in the entire app via the Web.config file, by setting the AppSetting for ClientValidationEnabled to false:

<appSettings>  
    ...
    <add key="ClientValidationEnabled" value="false"/>
    ...
  </appSettings>

With client-side validation disabled, let's see what HTML this renders for the FirstName property:

<div>  
    <label for="FirstName">First Name:</label>
    <input id="FirstName" name="FirstName" type="text" value="">
</div>  

If we attempt to submit this form, the Controller catches the errors in its action and we get this HTML:

<div>  
    <label for="FirstName">First Name:</label>
    <input class="input-validation-error" id="FirstName" name="FirstName" type="text" value="">
    <span class="field-validation-error">Please enter the first name.</span>
</div>  

Because this input failed validation, we are now rendering the Validation Message as well as placing an error message CSS class on the input. This is pretty straightforward so far.

What happens if we turn client side validation back on? Before submitting the form, we get this HTML:

<div>  
    <label for="FirstName">First Name:</label>
    <input data-val="true" data-val-required="Please enter the first name." id="FirstName" name="FirstName" type="text" value="">
    <span class="field-validation-valid" data-valmsg-for="FirstName" data-valmsg-replace="true"></span>
</div>  

Now we've got some interesting new attributes to look at:

  • data-val specifies that this input needs validation.
  • data-val-required is the error message to be displayed if a value is not provided.
  • data-valmsg-for is the name of the input associated to the error message(s) being displayed.
  • data-valmsg-replace specified whether error messages are to be replaced within this element.

This is part of the magic of Unobtrusive: it uses HTML5-compatible "data-" attributes to store all of the information it needs to perform validation. This is why you don't need to use any other code besides attributes to enable client-side validation with this library.

Of course, so far all the validation we've been doing so far in this demo has been server-side. To enable client-side validation, we have to include the "jquery.validate.unobtrusive" bundle in our view. In our sample project, we have another view that shows how this could work:

Views/Home/Unobtrusive.cshtml
@model UnobtrusiveDemo.ViewModels.Home.AddUserVM

@{
    ViewBag.Title = "Add a User (Unobtrusive)";
}

<h2>Add a User (Unobtrusive Validation)</h2>

@using (Html.BeginForm())
{
    <div>
        <div>
            @Html.LabelFor(x => x.FirstName)
            @Html.TextBoxFor(x => x.FirstName)
            @Html.ValidationMessageFor(x => x.FirstName)
        </div>
        <div>
            @Html.LabelFor(x => x.LastName)
            @Html.TextBoxFor(x => x.LastName)
            @Html.ValidationMessageFor(x => x.LastName)
        </div>
        <div>
            @Html.LabelFor(x => x.Age)
            @Html.TextBoxFor(x => x.Age)
            @Html.ValidationMessageFor(x => x.Age)
        </div>
        <div>
            @Html.LabelFor(x => x.EmailAddress)
            @Html.TextBoxFor(x => x.EmailAddress)
            @Html.ValidationMessageFor(x => x.EmailAddress)
        </div>
        <div>
            <input type="submit" value="Save" />
        </div>
    </div>
}

@section Scripts
{
    @Scripts.Render("~/bundles/jqueryval")
}

The only difference between the ServerSide view and the Unobtrusive view is the inclusion of the jqueryval bundle on the latter.

What we will see now is that with the script included and the ClientValidationEnabled property set, the validation will fire client-side in addition to server-side. This means that the browser doesn't make a round-trip to the server on validation failure, and that the user gets a nicer experience out of it.

Client-Side vs Server-Side Validation - Do We Need Both?

Absolutely! In fact, we only actually need server-side validation, since client-side can be disabled by the browser. However, handling both scenarios provides a nice graceful degredation where the user will still get the validation they need, in the nicest way their browser provides.

Check out the sample project on GitHub, and let me know what you think about this demo in the comments!

Happy Coding!