One of the interesting side-effects of installing ASP.NET MVC 3 is the appearance of Microsoft.Web.Infrastructure in the GAC. Inside the assembly is a DynamicModuleUtility class that will let you do the following:
using System; using System.Web; using Microsoft.Web.Infrastructure.DynamicModuleHelper; [assembly:PreApplicationStartMethod(typeof(MyAppStart), "Start")] public class CoolModule : IHttpModule { // implementation not important // imagine something cool here } public static class MyAppStart { public static void Start() { DynamicModuleUtility.RegisterModule(typeof(CoolModule)); } }
The significant line of code is the line with RegisterModule. The DynamicModuleUtility will let you install an HTTP module into the ASP.NET pipeline without making any changes to web.config file. Registration must occur during the pre application startup up phase, so you'll probably mix dynamic modules with WebActivator for maximum flexibility. The ability to dynamically register modules opens up some interesting options for plugins and infrastructure libraries.
QUnit is an easy to use testing framework* for an easy to test language. Once you've downloaded the library you can load up an .html page with all of your scripts, and the boilerplate qUnit HTML:
<h1 id="qunit-header">Validator Tests</h1> <h2 id="qunit-banner"></h2> <div id="qunit-testrunner-toolbar"></div> <h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> <div id="qunit-fixture"> <input id="prefix_first" name="prefix.first" type="text" /> <input id="prefix_second" name="prefix.second" type="text" /> </div>
Notice we place two inputs inside of qunit-fixture. QUnit will position the qunit-fixture off screen so you'll never see this markup (and it won't interfere with your test output), but you can manipulate the elements inside the fixture from tests.
If we wanted to exercise our client validation script with tests, we could choose to isolate the "greaterdate" validation function that we wrote in the last post, and the tests might look something like this:
module("DateGreaterThan Tests", { setup: function () { this.greaterDate = jQuery.validator.methods["greaterdate"]; this.firstInput = $("#prefix_first"); this.secondInput = $("#prefix_second"); } }); test("Validation function is registered", function () { ok(this.greaterDate); }); test("Let empty values pass", function () { ok(this.greaterDate(this.firstInput.val(), this.firstInput[0], "second")); }); test("Let greater date pass", function () { this.firstInput.val("2/1/2011"); this.secondInput.val("12/12/2010"); ok(this.greaterDate(this.firstInput.val(), this.firstInput[0], "second")); }); test("Fail on equal date", function () { this.firstInput.val("2/1/2011"); this.secondInput.val("2/1/2011"); equals(this.greaterDate(this.firstInput.val(), this.firstInput[0], "second"), false); }); test("Fail on lesser date", function () { this.firstInput.val("2/1/2011"); this.secondInput.val("2/1/2012"); equals(this.greaterDate(this.firstInput.val(), this.firstInput[0], "second"), false); });
Notice you can use setup (and teardown) functions with QUnit, and QUnit will automatically reset the qunit-fixture.
* I have to admit when I look at some of the tests I see written with QUnit, I feel queasy inside. This mainly happens when I see the start and stop methods used for testing asynchronous code. I prefer hammet's approach of "Mock As You Wish".
The validator we looked at in the last post only supports server side validation. To support client side validation we need a little more code on the server, and some custom client script.
One approach to client validation is to have the validation attribute implement the IClientValidatable interface, which requires a single method: GetClientValidationRules.
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable { public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule(); rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()); rule.ValidationParameters.Add("other", OtherProperty); rule.ValidationType = "greaterdate"; yield return rule; } // ...
The method returns an object containing all the data we need on the client. The runtime will serialize this data into HTML attributes on the target input element. Data includes the name of the "other" property, the name of the adapter the client needs for deserializing the data, and the error message to display in case of validation failure. Rendered output would look something like:
<input data-val="true" data-val-greaterdate="EndDate must be greater than StartDate" data-val-greaterdate-other="StartDate" ... />
Note: the validation attribute went from being a "GreaterThan" attribute working against any IComparable to a "DateGreaterThan" attribute. In .NET, it's easy to compare integers, longs, DateTimes and decimals using the same code as they all implement IComparable. In JavaScript the situation is a bit thornier, so to avoid the messy parsing we'd need to deduce the type of the underlying data, we'll just stick with dates.
Here is one way to implement the client script needed for validation:
/// <reference path="jquery-1.4.4-vsdoc.js" /> /// <reference path="jquery.validate-vsdoc.js" /> /// <reference path="jquery.validate.unobtrusive.js" /> jQuery.validator.unobtrusive .adapters.addSingleVal("greaterdate", "other"); jQuery.validator.addMethod("greaterdate", function (val, element, other) { var modelPrefix = element.name.substr( 0, element.name.lastIndexOf(".") + 1) var otherVal = $("[name=" + modelPrefix + other + "]").val(); if (val && otherVal) { if (Date.parse(val) <= Date.parse(otherVal)) { return false; } } return true; } );
(Update: should have been searching by name, not #id).
The first line is registering an adapter. An adapter is the code that pulls data from the HTML attributes and turns them into something jQuery.validate will understand. All the hard work is taken care of in the unobtrusive extensions provided by MVC 3. All we need to do is tell the extensions we need a single valued adapter, and the single value to look for is the "other" value (so the adapter will look inside the data-val-greaterdate-other attribute). Note the names we use on the server side ("greaterdate" and "other") have to match the adapter name and parameter name in script.
The last block of code registers the actual function used by jQuery.validate when the time comes to validate. You have to be careful if you are using model prefixes on the client side - the other property name might be "StartDate", but the input for editing StartDate has the name "Foo.StartDate". If you assume the source input uses the same model prefix as the other input, then the substr manipulation in the above code will find and use the proper prefix when locating the other input (it also works when there is no model prefix).
How do you know if the function will work? We'll apply some qUnit tests in the next post.
Let's say you want to create a GreaterThan validation attribute and use it like so:
public class Trip { [Required] public DateTime StartDate { get; set; } [Required] [GreaterThan("StartDate")] public DateTime EndDate { get; set; } }
The implementation would look something like this:
public class GreaterThanAttribute : ValidationAttribute { public GreaterThanAttribute(string otherProperty) :base("{0} must be greater than {1}") { OtherProperty = otherProperty; } public string OtherProperty { get; set; } public override string FormatErrorMessage(string name) { return string.Format(ErrorMessageString, name, OtherProperty); } protected override ValidationResult IsValid(object firstValue, ValidationContext validationContext) { var firstComparable = firstValue as IComparable; var secondComparable = GetSecondComparable(validationContext); if (firstComparable != null && secondComparable != null) { if (firstComparable.CompareTo(secondComparable) < 1) { return new ValidationResult( FormatErrorMessage(validationContext.DisplayName)); } } return ValidationResult.Success; } protected IComparable GetSecondComparable( ValidationContext validationContext) { var propertyInfo = validationContext .ObjectType .GetProperty(OtherProperty); if (propertyInfo != null) { var secondValue = propertyInfo.GetValue( validationContext.ObjectInstance, null); return secondValue as IComparable; } return null; } }
Notice the IsValid override available in .NET 4 adds a ValidationContext typed parameter, and this parameter makes it easier to look "outside the attribute" at the associated model type and model instance (validationContext.ObjectType.GetProperty(OtherProperty)).
The validation context does have a shortcoming when it comes to model metadata, however. The context brings along the DisplayName value for the current property, but there is nothing available to easily get to metadata for the rest of the model. For example, when formatting the error string it would be nice to use the friendly name of both properties involved in validation, but we only have DisplayName for one. One possible solution is to dig it our from the model metadata provider.
protected string GetOtherDisplayName(ValidationContext validationContext) { var metadata = ModelMetadataProviders.Current.GetMetadataForProperty( null, validationContext.ObjectType, OtherProperty); if (metadata != null) { return metadata.GetDisplayName(); } return OtherProperty; }
Next up: adding client validation and using jQuery.validate.
Cory Ander wanted to make sure his users enter a date value that is reasonably close to the current date. He's writing an MVC 3 app and envisioned using a validation attribute like the following.
public class TransactionDispute { [RecentDate] public DateTime TransactionDate { get; set; } // ... }
Corey came up with the following.
public class RecentDateAttribute : ValidationAttribute { public RecentDateAttribute() :base("{0} must be recent") { MinimumDate = DateTime.Now.AddDays(-1); } protected DateTime MinimumDate { get; set; } protected override ValidationResult IsValid( object value, ValidationContext validationContext) { var date = (DateTime) value; if (date < MinimumDate) { return new ValidationResult( FormatErrorMessage(validationContext.DisplayName) ); } return ValidationResult.Success; } }
It seems to work - what could possibly be wrong?
I’ve seen the HTML helpers in ASP.NET MVC drive developers to madness. For example:
@Html.ActionLink("Detail", "Detail", "Car", new { id = 5 })
... produces ...
<a href="/Home/Detail?Length=3" id="5">Detail</a>
Notice "Length=3" in the query string? That's probably not what you expected. We see this output because we are calling the ActionLink overload where “Car” is interpreted as route data and the anonymous object we want to use for route data is instead interpreted as the HTML attributes parameter.
If you add one more parameter (a null on the end):
@Html.ActionLink("Detail", "Detail", "Car", new { id = 5 }, null)
... it renders ...
<a href="/Car/Detail/5">Detail</a>
And this is probably the output you want to see, because the third parameter went from being routing data to controller name, the fourth parameter went from being HTML attributes to route data, and the last parameter is now HTML attributes. All because we passed an empty (null) value as an additional parameter.
Kind of weird, don't you think?
But it doesn't stop with the overloads.
Tell me if I'm missing something obvious...
Let's say you want to write a custom HTML helper to render an image tag with a specific purpose.
@Html.FlightChart("TopDestinations")
Let's also say the image tag needs to start with its src attribute set to a well known image. Sounds easy, but if you want to ensure the path is correct you might want to make a call to Url.Content - except the Url helper isn't available inside an Html helper method.
public static MvcHtmlString FlightChart( this HtmlHelper helper, string chartName) { var rollerPath = "?"; // Resolve(~/Content/roller.gif) ? var div = new TagBuilder("div"); var image = new TagBuilder("img"); image.MergeAttribute("data-chart", chartName); image.MergeAttribute("src", rollerPath); div.InnerHtml = image.ToString(TagRenderMode.SelfClosing); return MvcHtmlString.Create(div.ToString()); }
You could always make FlightChart an extension method for UrlHelper, but it feels strange to produce HTML markup from a Url helper. Another option is to construct a new instance of UrlHelper inside the the Html helper.
var rollerPath = new UrlHelper(helper.ViewContext.RequestContext) .Content("~/Content/roller.gif");
But it seems silly to create a new UrlHelper when one is already available somewhere else inside of the request. What if we grabbed a reference to the existing UrlHelper?
var urlHelper = ((Controller) helper.ViewContext.Controller).Url; var rollerPath = urlHelper.Content("~/Content/roller.gif");
Eek! That's even worse!
What if we just forget about UrlHelper and turn to our old friend VirtualPathUtility?
var rollerPath = VirtualPathUtility.ToAbsolute("~/Content/roller.gif");
Better - but don't try to run any unit tests on the above code.
Just like web form views, razor views don’t “build” when you build an MVC project in Visual Studio. By default, the parsing and compilation of a view doesn’t happen until runtime, meaning you might not know about a syntax error in a view until you hit the application with a web browser.
Since it’s first release, ASP.NET MVC has provided the ability to parse and compile views at build time by manipulating the underlying MSBuild (.csproj) file. The option works for both web forms (.aspx) and Razor (.cshtml) views.
1. Right-click the MVC project and select “Unload”
2. Right-click the unloaded project and select “Edit”
3. Locate the <MvcBuildViews> element and set its value to “true”.
4. Save the file, then right-click the unloaded project and reload.
With MvcBuildViews set to true, you’ll find out about any syntax problems when you compile a project. Also, Al Gonzalez has a Visual Studio macro to turn the above steps into a one-click operation.
Building views will significantly increase your overall build time, so you might consider only setting the option to true for release builds.
<PropertyGroup
Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<MvcBuildViews>true</MvcBuildViews>
</PropertyGroup>
If you happen to have an Entity Framework edmx file directly inside your MVC project, you might see the following error when you eagerly build views.
Could not load type 'System.Data.Entity.Design.AspNet.EntityDesignerBuildProvider'.
This error message is easy to avoid because we do not need to process the .edmx file with a build provider in MVC. Simply add the following configuration into the root level web.config, and all will be well.
<system.web>
<compilation debug="true" targetFramework="4.0">
<buildProviders>
<remove extension=".edmx"/>
</buildProviders>
...
Happy building!