Angular.NET – Helpers for ASP .NET MVC 4

ASP .NET MVC is a great web-server framework and, in my opinion, a HUGE improvement over regular ASP .NET WebForms. Among many features, it has routing, helper methods to generate views, declarative model validation (data annotations), automatic model binding for different media types (with model validation), support for dependency injection, etc.

On the other hand, AngularJS is a great client-side framework that helps you to create professional applications using JavaScript. It’s loaded with features such as client-side routing, directives for custom behavior like model binding (ngModel) and model validation, filters, message dispatcher, dependency injection, etc.

Currently, creating HTML forms with model bindings and the respective validations and error messages for each field in the form can be a tedious, error-prone task. Suppose we have the following model class:

using System;
using System.ComponentModel.DataAnnotations;

public class Person
{
    public int Id { get; set; }

    [Required]
    [StringLength(25)]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [StringLength(25)]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
}

Let’s look at an example of how you would create a simple but fully functional form (with validations) in AngularJS for this model (for more information, see the form and ngModel directives):

<form autocomplete="off" class="form-horizontal ng-cloak" id="personalForm" method="POST" name="personalForm" ng-submit="save(personalForm)" novalidate>
    <div class="control-group" ng-class="{ error: personalForm.FirstName.$invalid && personalForm.FirstName.$dirty }">
        <label class="control-label" for="FirstName">First Name</label>
        <div class="controls">
            <input autofocus id="FirstName" name="FirstName" ng-maxlength="25" ng-model="FirstName" required type="text" />
            <span class="help-inline" ng-show="personalForm.FirstName.$error.required && personalForm.FirstName.$dirty">The First Name field is required.</span>
            <span class="help-inline" ng-show="personalForm.FirstName.$error.maxlength && personalForm.FirstName.$dirty">The field First Name must be a string with a maximum length of 25.</span>
        </div>
    </div>
    <div class="control-group" ng-class="{ error: personalForm.LastName.$invalid && personalForm.LastName.$dirty }">
        <label class="control-label" for="LastName">Last Name(s)</label>
        <div class="controls">
            <input id="LastName" name="LastName" ng-maxlength="25" ng-model="LastName" required type="text" />
            <span class="help-inline" ng-show="personalForm.LastName.$error.required && personalForm.LastName.$dirty">The Last Name(s) field is required.</span>
            <span class="help-inline" ng-show="personalForm.LastName.$error.maxlength && personalForm.LastName.$dirty">The field Last Name(s) must be a string with a maximum length of 25.</span>
        </div>
    </div>
    <div class="control-group">
        <div class="controls">
            <button class="btn btn-primary" type="submit">Save</button>
            <button class="btn" type="button" ng-click="cancel()">Cancel</button>
        </div>
    </div>
</form>

Now, that was only a form with two fields (I know, I have a several rules and validation messages, but it’s a real-world case). This works like a charm but, as you can see, it requires a great deal of maintenance effort. You need to add all the validation directives, the appropriate help messages and the rules for when to display/hide them. You will need to be careful to keep you form validations in sync with your server side models if any of the following occur:

  • The names of your model properties are modified.
  • The validations for a specific property are added/removed.
  • The values for such validations are changed (for example, the max value for a number or the max length for a string field)

That’s a lot work for me. Since I’m too lazy for all that, I thought there must be something that could save me some time when coding and maintaining my code. It turns out there is a great ASP .NET MVC feature that could come to the rescue: HTML helper extension methods. So, inspired by a solution that I used for Knockout some time ago (Knockout MVC) and by ASP .NET MVC’s jQuery unobtrusive validation, I created a small library for AngularJS that integrated both features (binding and validation generation).

Using this library, the same view can be created in Razor as follows:

@using AxSoft.Angular.Net

@model AxSoft.Model.Person
@using (var form = Html.BeginAngularForm("personForm", "save", "person", new { @class = "form-horizontal ng-cloak" }))
{
    <div class="control-group" @form.NgClassError(m => m.FirstName)>
        @form.Label(m => m.FirstName)
        <div class="controls">
            @form.TextBox(m => m.FirstName, new { autofocus = "" })
            @form.ValidationsFor(m => m.FirstName, true)
        </div>
    </div>
    <div class="control-group" @form.NgClassError(m => m.LastName)>
        @form.Label(m => m.LastName)
        <div class="controls">
            @form.TextBox(m => m.LastName)
            @form.ValidationsFor(m => m.LastName, true)
        </div>
    </div>
    
    <div class="control-group">
        <div class="controls">
            <button class="btn btn-primary" type="submit">Save</button>
            <button class="btn" type="button" ng-click="cancel()">Cancel</button>
        </div>
    </div>
}

A few things to note:

  • You need to import the AxSoft.Angular.Net (or add it to the namespaces node of you web.config file under the Views folder).
  • You have to declare the model type for the extension methods to work properly. Note that this declaration only acts as an indication of the type of model you want to get the properties from, but you don’t need to pass an actual model instance to the view.
  • You can create a form almost exactly as you would using the Html.BeginForm method, supplying the following values:
    • A form name: this will be used for the validation message conditions; also, AngularJS will create a property on your controller’s scope with the same name as the form.
    • The name of the method to be invoked when the form is submitted (optional); this is used to output the ng-submit directive in the form ng-submit="nameOfMethod(nameOfForm)". The form instance is passed to the method so that you can check the validity of the form through form.$valid. The default value is "save"
    • The name of the scope property that will hold the model object (optional).
    • An object or dictionary instance containing custom HTML attributes to be set on the form (optional).

The invocation of Html.BeginAngularForm will return a generic AngularForm object which you can use to create your form controls and bindings.

You can create controls for specific model properties using the methods TextBox, Hidden, TextArea, Password, RadioButton, CheckBox, and Dropdown. These methods will output the corresponding ngModel directive as well as all the registered validations found in the ValidationAttribute‘s of the property.

The method ValidationsFor will generate span elements for each registered validation for the property. Each element will have a CSS class with the value you specify in the AngularConfiguration.HelpCssClass property (the default is "help-inline").

The method Label is similar to Html.LabelFor, with the only advantage of creating a for attribute that will match the id attribute of the control generated for the same property.

Lastly, the method NgClassError will output an ng-class directive that will activate the error CSS class of the element when any of the validations for that property fail to pass. The name of this class will be given by the value of the AngularConfiguration.ErrorCssClass property (the defautl is "error").

Interested? Grab the code from Github and play with it. Any feedback is welcome.

In a future tutorial: how to extend the validation mechanism with custom validation attributes and directives. Stay tuned.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s