Custom client side validaties in ASP.NET MVC4

Seeing anything you like? Google translate does not work out? Drop me a note and I will translate this post.

Kijk voor een vervolg op deze blog naar: Unittests voor MVC4 model validaties

Eén van de meest onderschatte aspecten van gebruikersvriendelijke websites is het toepassen van validaties. Validaties kunnen teruggevonden worden op drie locaties:

  1. Validaties diep de businesslaag (of zelfs in de datalaag)
  2. Validaties op het ASP.NET MVC (view-)model welke op de server gevalideerd worden door de controller
  3. Validaties welke op de client uitgevoerd worden via JavaScript.

De validaties uit 1 zijn lastig om te valideren zonder een daadwerkelijke aanroep van de onderliggende lagen van de website.

Voor de twee andere varianten geldt dat niet. De validaties uit 2 zijn geschreven in .Net code, de validaties uit 3 zijn geschreven in JavaScript. ASP.NET MVC maakt het mogelijk om eenzelfde soort validatie aan elkaar te laten refereren. Hierdoor kan via de Razor view engine de HTML/Javascript voor de client side validatie aangemaakt worden als een eigenschap op het (view-)model een server side validatie implementeert…

Hier volgt een stappenplan voor het schrijven van een Custom Client Side Validatie.

Stap 1

Eigenlijk overbodig, deze opties staan tegenwoordig standaard aan,  maar in de web.config moet het toepassen van client validaties aan staan. Controleer dus of de volgende twee opties op true staan:


<appSettings>

…

<add key="ClientValidationEnabled" value="true" />

<add key="UnobtrusiveJavaScriptEnabled" value="true" />

</appSettings>


Stap 2

Laten we eerst de server side validatie aanmaken. Ik heb als voorbeeld bij gebrek aan inspiratie een validatie geschreven die de minimale lengte van een string property controleert. De server side validatie is volgt:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute
{
    private const string DefaultErrorMessage = "{0} must be at least {1} characters long.";

    public ValidateMinimalLengthAttribute()
        : base(DefaultErrorMessage)
    {
    }

    public int MinimalLength { get; set; }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, name, MinimalLength);
    }

    public override bool IsValid(object value)
    {
        var valueAsString = value as string;

        return (string.IsNullOrEmpty(valueAsSString)) || valueAsString.Length >= MinimalLength;
    }
}

Stap 3

Deze validatie is direct toe te passen op ons model:

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

    public string Name { get; set; }

    [Display(Name = "City")]
    [ValidateMinimalLength(MinimalLength = 10)]
    public string Address1City { get; set; }
}

Stap 4

En deze validatie kan op de controller gecontroleerd worden:

public class CustomerController : Controller
{
    public ActionResult Create()
    {
        return View(new CustomerModel());
    }

    [HttpPost]
    public ActionResult Create(CustomerModel model)
    {
        if (ModelState.IsValid)
        {
            // model is valid
        }
        else
        {
            // model is invalid
        }

        return View();
    }
}

Het toevoegen van server side validaties is dus tamelijk eenvoudig. Laten we nu aan de client side validatie gaan werken…

Stap 5

Om te beginnen moeten we aan de bestaande validatie kennis meegeven dat er een client side validatie beschikbaar komt. We laten de ValidateMinimalLengthAttribute de interface IClientValidatable implementeren:


[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute, IClientValidatable
{
  …

  public override string FormatErrorMessage(string name)
  {
    …
  }

  public override bool IsValid(object value)
  {
    …
  }

  public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
    ModelMetadata metadata,  ControllerContext context)
  {
    //// from metadata: ShortDisplayName > DisplayName > PropertyName
    var messageNameDisplayName = (!string.IsNullOrWhiteSpace(metadata.ShortDisplayName))
               ? metadata.ShortDisplayName
               : metadata.DisplayName;

    var messageName = (!string.IsNullOrWhiteSpace(messageNameDisplayName))
               ? messageNameDisplayName
               : metadata.PropertyName;

    var rule = new ModelClientValidationRule()
    {
      ErrorMessage = FormatErrorMessage(messageName),
      ValidationType = "minimallength",
    };

    rule.ValidationParameters.Add("minlength", MinimalLength);

    yield return rule;
  }
}

Deze implementeert de methode GetClientValidationRules welke een opsomming van uit te voeren validaties opgeeft. In ons geval geven we aan dat er op de client een validatie genaamd “minimallength” gerefereerd moet worden en we laten de eventueel te tonen errormessage doorgeven. Ook laten we MinimalLength integer doorgeven. Deze informatie zal door de Razor viewengine gebruikt worden om de HTML aan te maken voor deze unobtrusive validatie. We definiëren in de view:

<div>
  @Html.LabelFor(model => model.Address1City)
</div>
<div>
  @Html.EditorFor(model => model.Address1City)
  @Html.ValidationMessageFor(model => model.Address1City)
</div>

De uiteindelijk gegenereerd HTML ziet er dan zo uit:

<div>
  <label for="Address1City">City</label>
</div>
<div>
  <input data-val="true"
         data-val-minimallength="City must be at least 10 characters long."
         data-val-minimallength-minlength="10"
         id="Address1City"
         name="Address1City"
         type="text"
         value="" />
  <span data-valmsg-for="Address1City"
        data-valmsg-replace="true"></span>
</div>

Let op! Wat we hier zien zijn slechts enkele extra HTML attributen op een input veld. Dit is nog niet voldoende voor de validatie want we hebben nog niet eens de javascript functie geschreven!

Laten we dus snel verder gaan.

Stap 6

We moeten dus een functie schrijven die de lengte valideert en deze functie moet geregistreerd worden onder de naam ‘minimallength’. De volgende javascript voldoet hieraan:

// validation name must be equal to the ValidationType in the ModelClientValidationRule

$(function () {
    jQuery.validator.addMethod('minimalLength', function (value, element, param) {
        var minlength = param.minlength;

        if (value != null && value.length > 0 && value.length < minlength) {
            return false;
        }
        return true;
    });

    jQuery.validator.unobtrusive.adapters.add('minimallength', ['minlength'], function (options) {
        var params = {
            minlength: options.params.minlength
        };

        options.rules['minimalLength'] = params;

        if (options.message) {
            options.messages['minimalLength'] = options.message;
        }
    });
} (jQuery));

Wat we hier zien is de functie (met de hoofdletter in de naam) en de registratie ervan. Interessant is om te zien hoe de parameter ‘minlength’ doorgegeven wordt tijdens de registratie, tesamen met de message.

Stap 7

Maar deze code moet wel op de client beschikbaar gesteld zijn, en wel na het laden van bibliotheken van de jQuery validaties en de unobtrusive code.

Hiervoor kan handig meegelift worden met de ‘~/bundles/jqueryval’ bundle. In de bundle configuratie vullen we de validatie bundle aan met ons eigen script (deze heb ik in een aparte Validations map gestopt):

bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
            "~/Scripts/jquery.unobtrusive*",
            "~/Scripts/jquery.validate*",
            "~/Validations/MinimalLength.js"
            ));

En, heel prettig, laat deze bundle nu net al standaard gerefereerd en dus geladen worden door views welke met de standaard Create en Edit templates aangemaakt zijn:

@model MvcApplication1.Models.CustomerModel
@{
    ViewBag.Title = "Create";
}
…
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Stap 8

Zo, nu kan alles getest worden. Start de website en ga naar de pagina waarin de validatie actief moet zijn. Controleer eerst even of de data-val attributen geplaatst zijn. Probeer vervolgens eens de client side validatie uit:

clientsideval1

Bonus

Ok, het werkt prima, maar hoe valideer je nu of de server side variant ook nog werkt? Schakel hiervoor in de web.config de ClientValidationEnabled uit. Hierdoor blijft de attributen voor unobtrusive script dus achterwege:

<div class=”editor-label”>
  <label for="Address1City">City</label></div>
<div>
<input id="Address1City"
       name="Address1City"
       type="text"
       value=""   />
<span>City must be at least 10 characters long.</span>
</div>

Conclusie

Client side validatie geeft een hele goede web-ervaring voor gebruikers van jouw site. Met de komst van MVC 3 is het toepassen van unobtrusive validaties een eenvoudige en mooie aanvulling van toe te passen technieken geworden voor de moderne webontwikkelaar. En deze validaties zijn keer op keer her te gebruiken over meerdere projecten.

3 thoughts on “Custom client side validaties in ASP.NET MVC4

  1. Hoi Sander,

    Zou je een blog kunnen wagen aan de vraag hoe een custom validatie zich gedraagt als MVC zelf al client side validatie voor een veld heeft? De gekke situatie doet zich namelijk voor met velden van het type decimal dat de controls in het edit form voor deze decimal in een nederlandstalige pagina de waarden automatisch met een komma weergeeft maar vervolgens niet accepteerd als input.omdat er een komma in zit. Moet je dan de validatie aanpassen of moet je naar een custom model binder?

    1. Hallo Willem,
      Het artikel was nog geschreven in een tijd dat javascript validaties niet bepaald eenvoudig ware te bouwen 🙂

      Wat betreft jouw vraag, normaal ondersteunen (datetimepickers/numeric) controls meerdere talen en daarmee meerdere formaten. Lees wat de browser achterhaalt via de agent (zie ook de recente blog van Scott Hanselman, http://www.hanselman.com/blog/StopDoingInternetWrong.aspx bij de vlaggetjes).

      Wel is het zo dat het op de server nog steeds relevant is om de input te testen indien de invoer niet ‘typed’ doorkomt (dwv. dat de mapping van de invoer naar een datetime of decimal property met de hand moet gebeuren).

      Succes,

      Sander

Reacties zijn gesloten.