ASP.NET MVC 5.1 - Professional ASP.NET MVC 5 (2014)

Professional ASP.NET MVC 5 (2014)

Appendix

ASP.NET MVC 5.1

—by Jon Galloway

What's In This Chapter?

· What's in ASP.NET MVC 5.1 and Visual Studio 2013 Update 2

· Facts about Enum support

· How to perform Attribute Routing with Custom Constraints

· Working with Bootstrap and JavaScript enhancements

This appendix describes some of the top features in MVC 5.1, and how you can start using them in your MVC applications.

SAMPLE CODE FOR THIS APPENDIX AND BEYOND

The sample project covering the posts in this series is available for download from GitHub at https://github.com/jongalloway/stardotone. Other referenced samples are in the ASP.NET sample repository athttp://aspnet.codeplex.com/sourcecontrol/latest#Samples/ReadMe.txt.

ASP.NET MVC 5.1 RELEASE DESCRIPTION

ASP.NET MVC 5 was released with Visual Studio 2013 in October 2013. In keeping with the policy of rapid point releases, the ASP.NET team released ASP.NET MVC 5.1, Web API 2.1, and Web Pages 3.1 as NuGet package upgrades for existing projects in January 2014. These updates were bundled with Visual Studio 2013 Update 2 in April 2014.

The top MVC 5.1 features in this release are as follows:

· Attribute Routing improvements

· Bootstrap support for editor templates

· Enum support in views

· Unobtrusive validation for MinLength/MaxLength attributes

· Supporting the ‘this’ context in Unobtrusive Ajax

· Various bug fixes

This release also includes Web API 2.1, of which the main features are:

· Global error handling

· Attribute routing improvements

· Help page improvements

· IgnoreRoute support

· BSON media-type formatter

· Better support for async filters

· Query parsing for the client formatting library

· Various bug fixes

This appendix was adapted from my blog post series, which includes discussions about both MVC 5.1 and Web API 2.1 and is available at http://aka.ms/mvc51. This appendix focuses on MVC 5.1; for more information on Web API 2.1 you can consult the blog post series and release notes.

Getting MVC 5.1

The easiest way to get MVC 5.1 is through the new project templates in Visual Studio 2013 Update 2. Visual Studio 2013 Update 2 (sometimes abbreviated as Visual Studio 2013.2) includes updated project templates with MVC 5.1, so all of your new projects will include the features in this chapter. However, any previously created projects will require upgrading, which fortunately, is easy to do because you just do a NuGet upgrade.

Upgrading MVC 5 Projects from MVC 5.1

The ASP.NET project templates have changed over the years; they're now mostly a collection of composable NuGet packages. You can update these packages more frequently and use them without needing to install anything that will affect your development environment, other projects you're working on, your server environment, or other applications on your server.

You don't need to wait for your hosting provider to support ASP.NET MVC 5.1, ASP.NET Web API 2.1, or ASP.NET Web Pages 3.1—if they supported 5/2/3 they support 5.1/2.1/3.1. Easier said, if your server supports ASP.NET 4.5, you're set.

You also don't need to have Visual Studio 2013 Update 2 to upgrade to MVC 5.1, although you should, if at all possible. New features for ASP.NET MVC 5.1 views require you to run a recent Visual Studio update to get editing support. You're installing the Visual Studio updates when they come out, so that's not a problem, right?

If you don't have Visual Studio 2013 Update 2, here's how you can get MVC 5.1 support in previous releases of Visual Studio:

· For Visual Studio 2012, you should have ASP.NET and Web Tools 2013 Update 1 for Visual Studio 2012 (available at http://go.microsoft.com/fwlink/?LinkId=390062). You would need this for ASP.NET MVC 5 support in Visual Studio 2012, so no real change there.

· For Visual Studio 2013, you need Visual Studio 2013 Update 1 to get nice editor support for the new ASP.NET MVC 5.1 Razor View features (for example, Bootstrap overloads).

Upgrading an MVC 5 Application to 5.1

This section shows you how to upgrade an MVC 5 application to MVC 5.1 by installing the new NuGet packages. This example was created using the Web API template with Visual Studio 2013 so you can play with some of the Web API 2.1 features if you're interested.

Note

This section is not applicable to projects created using Visual Studio 2013 Update 2. Projects created with Visual Studio 2013 Update 2 will already include MVC 5.1 and Web API 2.1 without requiring any NuGet updates.

If you have Visual Studio 2013 Update 2 installed, you can just create a new ASP.NET Project using the Web API template and skip to the next section, titled “Enum Support in ASP.NET MVC Views.”

1. Open the New Project dialog Select ASP.NET Web Application Select the Web API template as shown in Figure A.1. Click OK.image

Figure A.1

2. Open the Manage NuGet Packages dialog box (see Figure A.2) by choosing Tools Manage NuGet Packages and check for updates.image

Figure A.2

3. Because this is a throw-away project, you can just click Update All. If you're upgrading a real project, I recommend reviewing the package updates before installing them. This is especially important for the JavaScript libraries, as the upgrade from jQuery 1.x to 2.x has some breaking changes. Figure A.3 shows the results of updating all packages in the application.image

Figure A.3

ENUM SUPPORT IN ASP.NET MVC VIEWS

This section will examine the support for Enums in MVC 5.1. You will create a simple model class, scaffold a view, then improve the view by adding a custom Editor Template.

1. Begin by creating a Person model class (as explained in Chapter 4) with a Salutation Enum:using System.ComponentModel.DataAnnotations;namespace StarDotOne.Models{ public class Person { public int Id { get; set; } public Salutation Salutation { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } //I guess technically these are called honorifics public enum Salutation { [Display(Name = “Mr.”)] Mr, [Display(Name = “Mrs.”)] Mrs, [Display(Name = “Ms.”)] Ms, [Display(Name = “Dr.”)] Doctor, [Display(Name = “Prof.”)] Professor, Sir, Lady, Lord }}

Note

Note that that some of the Salutation values are using the Display attribute to set a friendly display name for a model property. See the “Display and Edit Annotations” section of Chapter 6 for more information.

2. I delete my HomeController and views and scaffold a new HomeController using the Person class. Run the application and click the Add link to view the scaffolded Create view as shown in Figure A.4.

Oh, no! No dropdown on Salutation!

Just kidding. That's to be expected for a project created with the MVC 5 scaffolder.

image

Figure A.4

3. To get the dropdown, you change the scaffolded view code for the Salutation from the generic Html.EditorFor to use the new Html.EnumDropDownListFor helper. The scaffold templates included in Visual Studio 2013 Update 2 automatically uses theHtml.EnumDropDownListFor when appropriate, so this step will not be necessary.

So in Create.cshtml, you need to change this line:

@Html.EditorFor(model => model.Salutation)

to this:

@Html.EnumDropDownListFor(model => model.Salutation)

4. Now refresh the page to view the Enum dropdown (as shown in Figure A.5):image

Figure A.5

5. You can update your application so that all Enum values are shown using the Enum view helpers by taking advantage of EditorTemplates and DisplayTemplates, as explained in the “Custom Templates” section of Chapter 16. You can find examples of them in the Enum Sample on CodePlex: https://aspnet.codeplex.com/SourceControl/latest#Samples/MVC/EnumSample/EnumSample/Views/Shared/.

6. Grab the EditorTemplates and DisplayTemplates templates from the above Enum Sample link and copy them into the /Views/Shared directory in your project as shown in Figure A.6:image

Figure A.6

7. Change the Create.cshtml view back to how it was originally scaffolded, using Html.EditorFor. That way the view engine searches for a matching EditorTemplate for the object type, finds Enum.cshtml, and uses it to render all Enum model properties. Refreshing the Create view shows that the Enum is being displayed using the dropdown, as shown in Figure A.7.image

Figure A.7

8. The Enum Sample referenced above also includes an EditorTemplate to display Enums using a radio button list rather than a dropdown. Use the override in Html.EditorFor to specify the EditorTemplate, like this:

@Html.EditorFor(model => model.Salutation, templateName: "Enum-radio")

Now all enum values display with a radio button rather than a dropdown list (see Figure A.8).

image

Figure A.8

ATTRIBUTE ROUTING WITH CUSTOM CONSTRAINTS

ASP.NET MVC and Web API have offered both simple and custom route constraints since their first release. A simple constraint looks something like this:

routes.MapRoute("blog", "{year}/{month}/{day}",

new { controller = "blog", action = "index" },

new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" });

In the previous case, “/2014/01/01” would match but “/does/this/work” would not because the values don't match the required pattern. If you needed something more complex than a simple pattern match, you would use a custom constraint by implementingIRouteConstraint and defining the custom logic in the Match method—if it returns true, the route is a match.

public interface IRouteConstraint

{

bool Match(HttpContextBase httpContext, Route route, string parameterName,

RouteValueDictionary values, RouteDirection routeDirection);

}

Route Constraints in Attribute Routing

One of the top new features in ASP.NET MVC 5 and Web API 2 is the addition of Attribute Routing. Rather than defining all your routes in /App_Start/RouteConfig.cs using a series of routes.MapRoute()calls, you can define routes using attributes on your controller actions and controller classes. You can take your pick of whichever works better for you; you can continue to use traditional routing, attribute routing instead, or both.

Attribute routing previously offered custom inline constraints, like the following:

[Route("temp/{scale:values(celsius|fahrenheit)}")]

Here, the scale segment has a custom inline Values constraint that will only match if the scale value is in the pipe-delimited list—that is, this code will match /temp/celsius and /temp/fahrenheit but not /temp/foo. You can read more about the Attribute Routing features that shipped with ASP.NET MVC 5, including inline constraints like the previous code, in Ken Egozi's post Attribute Routing in ASP.NET MVC 5 at http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx.

Although inline constraints allow you to restrict values for a particular segment, they're both a little limited (they can't operate over the entire URL), and more complex logic isn't possible at that scope.

Now with ASP.NET MVC 5.1, you can create a new attribute that implements a custom route constraint. The next section gives an example.

ASP.NET MVC 5.1 Example: Adding a Custom LocaleRoute

Here's a simple custom route attribute that matches based on a list of supported locales.

First, create a custom LocaleRouteConstraint that implements IRouteConstraint:

public class LocaleRouteConstraint : IRouteConstraint

{

public string Locale { get; private set; }

public LocaleRouteConstraint(string locale)

{

Locale = locale;

}

public bool Match(HttpContextBase httpContext,

Route route,

string parameterName,

RouteValueDictionary values,

RouteDirection routeDirection)

{

object value;

if (values.TryGetValue("locale", out value)

&& !string.IsNullOrWhiteSpace(value as string))

{

string locale = value as string;

if (isValid(locale))

{

return string.Equals(

Locale, locale,

StringComparison.OrdinalIgnoreCase);

}

}

return false;

}

private bool isValid(string locale)

{

string[] validOptions = new[] { "EN-US", "EN-GB", "FR-FR" };

return validOptions.Contains(locale. ToUpperInvariant());

}

}

IRouteConstraint has one method, Match. That's where you write your custom logic, which determines whether a set of incoming route values, context, and so on, match your custom route. If the Match function returns true, routes with this constraint are eligible to respond to the request; if the function returns false the request does not map to routes with this constraint.

In this case, you have a simple isValid matcher, which takes a locale string (in this example, “FR-FR”) and validates it against a list of supported locales. In more advanced use, this may query against a database-backed cache of locales your site supports, or it may use some other more advanced method. If you are working with a more advanced constraint, especially a locale constraint, I recommend Ben Foster's article “Improving ASP.NET MVC Routing Configuration” at http://ben.onfabrik.com/posts/improving-aspnet-mvc-routing-configuration.

It's important that the real value in this case runs more advanced logic than a simple pattern match—if that's all you're doing, you could use a regex inline route constraint (for example, {x:regex(^\d{3}-\d{3}-\d{4}$)}) as explained in Table 9.2.

Now you have a constraint, but you need to map it to an attribute to use in Attribute Routing. Note that separating constraints from attributes gives a lot more flexibility. For example, you can use this constraint on multiple attributes.

Here's a simple one:

public class LocaleRouteAttribute : RouteFactoryAttribute

{

public LocaleRouteAttribute(string template, string locale)

: base(template)

{

Locale = locale;

}

public string Locale

{

get;

private set;

}

public override RouteValueDictionary Constraints

{

get

{

var constraints = new RouteValueDictionary();

constraints.Add("locale",

new LocaleRouteConstraint(Locale));

return constraints;

}

}

public override RouteValueDictionary Defaults

{

get

{

var defaults = new RouteValueDictionary();

defaults.Add("locale", "en-us");

return defaults;

}

}

}

Now that you have a complete route attribute, you can place it on a controller or action:

using System.Web.Mvc;

namespace StarDotOne.Controllers

{

[LocaleRoute("hello/{locale}/{action=Index}", "EN-GB")]

public class ENGBHomeController : Controller

{

// GET: /hello/en-gb/

public ActionResult Index()

{

return Content("I am the EN-GB controller.");

}

}

}

And here's our FR-FR controller:

using System.Web.Mvc;

namespace StarDotOne.Controllers

{

[LocaleRoute("hello/{locale}/{action=Index}", "FR-FR")]

public class FRFRHomeController : Controller

{

// GET: /hello/fr-fr/

public ActionResult Index()

{

return Content("Je suis le contrôleur FR-FR.");

}

}

}

Before running this, you need to verify that you have Attribute Routes enabled in your RouteConfig:

public class RouteConfig

{

public static void RegisterRoutes(RouteCollection routes)

{

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapMvcAttributeRoutes();

routes.MapRoute(

name: "Default",

url: "{controller}/{action}/{id}",

defaults: new { controller = "Home",

action = "Index",

id = UrlParameter.Optional }

);

}

}

Now, as you can see in Figure A.9, a request to /hello/en-gb/ goes to the ENGBController and a request to /hello/fr-fr/ goes to the FRFRController.

image

Figure A.9

Because you've set the default locale in the LocaleRouteAttribute to en-us, you can browse to it using either /hello/en-us/ or just /hello (shown in Figure A.10).

image

Figure A.10

If you've been paying close attention, you may be thinking that you could have accomplished the same thing using an inline route constraint. The real benefit over a custom inline constraint is when you're doing more than operating on one segment in the URL; for example, when you're performing logic on the entire route or context. One great example is using a custom attribute based on a user's locale selection (set in a cookie, perhaps) or using a header.

So, to recap:

· Previously, you could write custom route constraints using “Traditional” code-based routing, but not in Attribute Routing.

· Previously, you could also write custom inline constraints, but mapped just to a segment in the URL.

· In MVC 5.1, you can now operate custom route constraints at a higher level than just a segment on the URL path; for example, headers or other request context.

A very common use case for headers in routing is versioning by header. The ASP.NET team has posted a sample application demonstrating how to version by header in ASP.NET Web API 2.1 athttp://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample/ReadMe.txt.

Keep in mind that even though the general recommendation is to use ASP.NET Web API for your HTTP APIs, many APIs still run on ASP.NET MVC for a variety of reasons (including having existing / legacy systems' APIs built on ASP.NET MVC, developers' familiarity with MVC, mostly having MVC applications with relatively few APIs that want to stay simple, developer preferences, and so on). Therefore, versioning ASP.NET MVC HTTP APIs by headers is probably one of the top use cases of custom route attribute constraints for ASP.NET MVC as well.

BOOTSTRAP AND JAVASCRIPT ENHANCEMENTS

MVC 5.1 includes a few small but very useful enhancements for working with Bootstrap and JavaScript in your Razor views.

EditorFor Now Supports Passing HTML Attributes

The new ASP.NET project templates all include Bootstrap themes (except for the Empty template, which is unstyled). Bootstrap uses custom class names for everything—styling, components, layout, and behavior. What made it frustrating was you couldn't pass classes down to the Html.EditorFor HTML helper and have them used in the default templates. This left you with a few suboptimal choices:

· You could use specific HTML Helpers like Html.TextBoxFor. While these specific helpers do allow you to pass HTML attributes, they don't benefit from some of the other nice features in HTML.EditorFor, like data attribute support for display and input validation.

· You could write custom templates to override all the default templates.

· You could give up using the Bootstrap classes and style things yourself.

In the 5.1 release, you can now pass HTML attributes as an additional parameter to Html.EditorFor. This allows you to apply custom Bootstrap styles while retaining all the advantages of templated editors. Here's an example of why that's useful.

In the “Enum Support in ASP.NET MVC Views” section earlier in this appendix, we scaffolded a simple create controller and associated views. The Create view ended up looking like Figure A.11:

image

Figure A.11

That's okay, but it's not taking advantage of any of the Bootstrap form styling (for example, focus indication, element sizing, groups, and so on) and it won't do anything special with custom Bootstrap themes. A great start is to just to add the "form-control” class to the form elements. That involves changing from this:

@Html.EditorFor(model => model.FirstName)

to this:

@Html.EditorFor(model => model.FirstName,

new { htmlAttributes = new { @class = "form-control" }, })

When you make that update to the textboxes, you get the view in Figure A.12:

image

Figure A.12

You'll notice some subtle improvements, like the focus highlight on the FirstName field, nicer textbox size and validation layout for Age, and so on. These are just simple things with a very basic model, but they you give a quick idea of the various improvements.

Also nice is that you can pass the attributes on Html.EditorFor when displaying the entire model. The following code updates the entire form section to just use one EditorFor call, passing in the model:

@using (Html.BeginForm())

{

@Html.AntiForgeryToken()

<div class="form-horizontal">

<h4>Person</h4>

<hr />

@Html.ValidationSummary(true)

@Html.EditorFor(model => model,

new { htmlAttributes = new { @class = "form-control" }, })

<div class="form-group">

<div class="col-md-offset-2 col-md-10">

<input type="submit" value="Create"

class="btn btn-default" />

</div>

</div>

</div>

}

To ensure the Id property didn't display and to use the custom radio Enum display template (as explained in the “Enum Support in ASP.NET MVC Views” section), the following code adds two annotations to the model. Here's how the model and associated Enum look:

public class Person

{

[ScaffoldColumn(false)]

public int Id { get; set; }

[UIHint("Enum-radio")]

public Salutation Salutation { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public int Age { get; set; }

}

//I guess technically these are called honorifics

public enum Salutation : byte

{

[Display(Name = "Mr.")] Mr,

[Display(Name = "Mrs.")] Mrs,

[Display(Name = "Ms.")] Ms,

[Display(Name = "Dr.")] Doctor,

[Display(Name = "Prof.")] Professor,

Sir,

Lady,

Lord

}

That gives you the exact same output as shown in Figure A.12. What's cool is that the EditorFor method passed the form-control class to each element in the form, so each input tag got the form-control class. That means that I could apply additional Bootstrap classes, as well as my own custom classes in that same call:

@Html.EditorFor(model => model, new { htmlAttributes =

new { @class = "form-control input-sm my-custom-class" }, })

Client-Side Validation for MinLength and MaxLength

MVC 5.1 now provides client-side validation support for MinLength and MaxLength attributes. We had client-side validation for StringLength before, but not for MinLength and MaxLength. Personally, I feel like neither approach is clearly superior—StringLength lets you set both min and max and is more widely supported, but MinLength and MaxLength allow you to specify them separately and give different validation messages for each. Regardless, the good news is that whichever you use, they're both supported on both server and client.

To test that out, we'll add some MinLength and MaxLength attributes to the Person class.

public class Person

{

[ScaffoldColumn(false)]

public int Id { get; set; }

[UIHint("Enum-radio")]

public Salutation Salutation { get; set; }

[Display(Name = "First Name")]

[MinLength(3, ErrorMessage =

"Your {0} must be at least {1} characters long")]

[MaxLength(100, ErrorMessage =

"Your {0} must be no more than {1} characters")]

public string FirstName { get; set; }

[Display(Name = "Last Name")]

[MinLength(3, ErrorMessage =

"Your {0} must be at least {1} characters long")]

[MaxLength(100, ErrorMessage =

"Your {0} must be no more than {1} characters")]

public string LastName { get; set; }

public int Age { get; set; }

}

I get immediate feedback on what the website thinks of a potential stage name I've been considering, as shown in Figure A.13.

image

Figure A.13

Three Small but Useful Fixes to MVC Ajax Support

MVC 5.1 includes a few bug fixes for MVC Ajax forms:

· Support "this” context for Ajax actions/forms

· Unobtrusive.Ajax no longer interferes with the cancel convention on validation

· LoadingElementDuration previously didn't work; this is now corrected

Support “this” context for Ajax actions/forms

The first fix allows callbacks from Unobtrusive Ajax to have access to the initiating element. That's pretty handy when you have multiple potential callers; for example, a list of items that contain Ajax.ActionLink calls. In the past, I've had to write unnecessarily complicated JavaScript to wire things up manually because I couldn't take advantage of the OnBegin, OnComplete, OnFailure, and OnSuccess options. For example:

<script type="text/javascript">

$(function () {

// Document.ready -> link up remove event handler

$(".RemoveLink").click(function () {

// Get the id from the link

var recordToDelete = $(this).attr("data-id");

if (recordToDelete != "') {

// Perform the ajax post

$.post("/ShoppingCart/RemoveFromCart",

{"id": recordToDelete },

function (data) {

// Successful requests get here

// Update the page elements

if (data.ItemCount == 0) {

$('#row-" + data.DeleteId)

.fadeOut('slow');

} else {

$('#item-count-" + data.DeleteId)

.text(data.ItemCount);

}

$('#cart-total').text(data.CartTotal);

$('#update-message').text(data.Message);

$('#cart-status')

.text('Cart ("

+ data.CartCount + ")');

});

}

});

});

</script>

Now that Unobtrusive Ajax supports “this” context, I have the option of wiring up the Ajax call and success callbacks separately and tersely because they have access to the calling element for the ID.

The history of this bug fix is interesting, as well. On a question that came up on StackOverflow, someone posted a suggested one-line fix on a CodePlex issue, and it got fixed in this source code commit:http://aspnetwebstack.codeplex.com/SourceControl/changeset/8a2c969ab6b41591e6a7194028b5b37a562c855a.

Unobtrusive.Ajax supports cancel convention on validation

jQuery Validation supports a convention in which a button with class="cancel" will not cause validation. Previously, Unobtrusive Ajax interfered with this behavior, so if your form was created using Ajax.BeginForm, cancel buttons would trigger validation.

LoadingElementDuration support

MVC 3 included a LoadingElementDuration parameter which is passed as an AjaxOption. This is used in conjunction with a LoadingElementId to show a message for Ajax forms which require time to display. However, this element was being incorrectly passed to jQuery as a string rather than an integer, so the element was always displayed using the default 400ms duration. This is corrected in MVC 5.1.

These three fixes are relatively small—in fact, two of them are only one line code changes - but are definitely useful if you're using MVC Ajax forms.

SUMMARY

This appendix reviews some of the top features in MVC 5.1. As a reminder, you can grab the source for these samples—as well as some additional samples demonstrating Web API 2.1 features—at https:github.com/jongalloway/StarDotOne and the official ASP.NET / Web API samples in the ASP.NET sample repository at http://aspnet.codeplex.com/sourcecontrol/latest.