June 28, 2016 by Christoff Truter C# JavaScript ASP.NET Angular MVC
When Microsoft released ASP.NET MVC 3 around October 2010, they introduced a feature called "unobtrusive client validation" into their codebase.
Which basically renders data annotations as defined on a Model / ViewModel property as HTML attributes on its bound element (TextBoxFor, DropDownListFor etc), in turn the jQuery validation plugin along with various jQuery adapters consumes the attributes on the client side (jquery.validate, jquery.validate.unobtrusive).
E.g. this
public class PersonViewModel { [EmailAddress] [Required] public string Email { get; set; }via this
@Html.TextBoxFor(m => m.Email)outputs this
<input data-val="true" data-val-email="The Email field is not a valid e-mail address." data-val-required="The Email field is required." id="Email" name="Email" type="email" value="">when configured like this
<configuration> <appSettings> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" />
Now Angular features its own form validation attributes, e.g ng-pattern, ng-minlength, ng-maxlength, ng-required etc (similar to the existing generated attributes).
My first instinct was to investigate if Microsoft included a way to override the existing data annotations generated attributes (like a custom WebControlAdapter in WebForms) but couldn't find anything like that.
So I ended up mapping the data annotation rendered attributes to their Angular counterparts using a simple Angular directive like seen below.
(function () { angular .module('cstruter.validate.unobtrusive', []) .directive('val', validation); function validation($compile) { return { restrict: 'A', require: 'ngModel', link: function (scope, element, attrs, controller) { // Make DOM changes setAttributes(element, attrs); // Prevent directive from being fired again for the same element element.removeAttr('data-val'); // Apply DOM changes $compile(element)(scope.$parent); } }; } function setAttributes(element, attrs) { var attributes = {}, set = function (name, key, value) { attrs[name] && (attributes[key] = value || attrs[name]); }; // Attribute mappings, ASP.NET MVC to ng-attributes set('valRegex', 'ng-pattern', '/^' + attrs.valRegexPattern + '$/'); set('valMinlengthMin', 'ng-minlength'); set('valMaxlengthMax', 'ng-maxlength'); set('valRequired', 'ng-required', true); set('valRange', 'ng-minlength', attrs.valRangeMin); set('valRange', 'ng-maxlength', attrs.valRangeMax); // Assign Attributes element.attr(attributes); } })();
The directive seen above (when included in the appropriate module and registered in a ScriptBundle) will match on data-val attributes and attempt to map them to their Angular versions.
Next we need to display the error messages somewhere, originally when writing this directive I simply added ngMessages to the DOM via the same directive, but felt that it is a very inflexible approach, so similarly to the attributes I looked for a way to provide custom markup for the ValidationMessageFor extensions.
But ended up being forced to write separate helpers - ValidationAngularMessageFor and BeginAngularForm.
The Angular Form helper can be seen below.
public static MsAspMvc.MvcForm BeginAngularForm(this HtmlHelper htmlHelper,
string actionName, string controllerName, FormMethod method, object htmlAttributes) { RouteValueDictionary routeValues = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); if (!routeValues.ContainsKey("name")) routeValues.Add("name", "form"); htmlHelper.ViewData.Add("formName", routeValues["name"]); return MsAspMvc.FormExtensions.BeginForm(htmlHelper, actionName, controllerName, method, routeValues); }
The reason for writing this helper is simple, I needed the name of the parent form as required by Angular Validators but couldn't retrieve it via the standard helper (via a directive the FormController could have been used using ^Form require), is there a clean way to use the Angular validation without needing the name of the form, anyone?
But perhaps this helper could be used for other Angular specific functionality as well in the future?
You can have a look at the ValidationAngularMessageFor helper over here (not going to post it on here for abbreviation sake), it simply renders the data annotations for a property as ngMessages, like seen below.
<div ng-messages="form.Email.$error" ng-show="form.$submitted || form.Email.$dirty" role="alert"> <div class="error" ng-message="required">The Email field is required.</div> <div class="error" ng-message="email">The Email field is not a valid e-mail address.</div> </div>