Creating powerful forms in ASP.NET with proper validation

Creating powerful forms in ASP.NET with proper validation

circle

Oscar de Vaal

At some point in your career, you have to write code to facilitate a form. The .NET Framework is packed with powerful ways to create (secure) forms.

In this article I will begin with the fundamentals of setting up a form. Furthermore I will show you how to create custom attributes and how to use them for both client side and server side validation. Last but not least, I will show you how you can send your form data via Ajax.

Basics

First of all, we need to add the following key to the web.config.

The web.config

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

The model

The model defines the structure of the data.

public class FormModel
{
    [Display(Name = "First name")]
    [Required(ErrorMessage = "Please fill in your first name")]
    [StringLength(50, ErrorMessage = "Your fist name contains too many characters")]
    public string FirstName { get; set; }

    [Display(Name = "Last name")]
    [Required(ErrorMessage = "Please fill in your last name ")]
    [StringLength(50, ErrorMessage = "Your last name contains too many characters ")]
    public string LastName { get; set; }

    [Display(Name = "Phone number")]
    [StringLength(12, ErrorMessage = "Your phone number contains too many digits")]
    public string PhoneNumber { get; set; }

    [Display(Name = "Date of birth")]
    [Required(ErrorMessage = "Please fill in your date of birth")]
    [DataType(DataType.Date)]
    [MinAge(ErrorMessage = "You have to be at least 18")]
    [NoFutureDate(ErrorMessage = "You can’t select a date in the future")]
    public DateTime DateOfBirth { get; set; }
}

The view

@model FormModel

@using (Html.BeginForm("Register", null, FormMethod.Post))
{
    @Html.AntiForgeryToken()

    <div>
        @Html.LabelFor(x => x.FirstName)
        @Html.EditorFor(x => x.FirstName)
        @Html.ValidationMessageFor(x => x.FirstName)
    </div>

    <div>
        @Html.LabelFor(x => x.LastName)
        @Html.EditorFor(x => x.LastName)
        @Html.ValidationMessageFor(x => x.LastName)
    </div>

    <div>
        @Html.LabelFor(x => x.PhoneNumber)
        @Html.EditorFor(x => x.PhoneNumber)
        @Html.ValidationMessageFor(x => x.PhoneNumber)
    </div>

    <button type="submit">
        Submit form
    </button>
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>


The controller

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register([FromBody] FormModel formModel)
{
    if (!ModelState.IsValid)
    {
        return Index(formModel);
    }

    // Persist data

    return Redirect("MyOtherPage");
}

Custom clientside validation

Unfortunately, the default validation attributes doesn’t always work for all the use cases. In this example, we need custom validation for the DateOfBirth property in the FormModel class. The form requires users to be at least 18 years old and show an error message when they fill in a future date. As for a start, we have added the following attributes: MinAge and NoFutureDate.

[Display(Name = "Date of birth")]
[Required(ErrorMessage = "Please fill in your date of birth")]
[DataType(DataType.Date)]
[MinAge(ErrorMessage = "You have to be at least 18 years old")]
[NoFutureDate(ErrorMessage = "You can’t select a date in the future")]
public DateTime DateOfBirth { get; set; }

In order for the validation to work both on the client side and the server side, we need to write code in both C# and JavaScript.

The custom attributes

public class NoFutureDateAttribute : ValidationAttribute, IClientValidatable
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value != null)
        {
            var dateTime = Convert.ToDateTime(value);
            if (dateTime > DateTime.Now)
            {
                return new ValidationResult(ErrorMessage);
            }
        }
        return ValidationResult.Success;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "nofuturedate",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
        };

        yield return rule;
    }
}

public class MinAgeAttribute : ValidationAttribute, IClientValidatable
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var birthday = DateTime.Parse(value.ToString());
        var today = DateTime.Today;

        var age = today.Year - birthday.Year;

        if (birthday > today.AddYears(-age))
        {
            age--;
        }

        if (age < 18)
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "minage",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
        };

        yield return rule;
    }
}

The JavaScript

$(function () {
    jQuery.validator.addMethod('nofuturedate', function (value, element, params) {
        var currentDate = new Date();
        if (Date.parse(value) > currentDate) {
            return false;
        }
        return true;
    }, '');
    jQuery.validator.unobtrusive.adapters.addBool('nofuturedate');

    $.validator.addMethod('minage', function (value, element, params) {
        var birthDay = new Date(value);
        var minAge = new Date(birthDay.getFullYear() + 18, birthDay.getMonth(), birthDay.getDate());
        var currentDate = new Date();
        return (currentDate >= minAge);
    });

    $.validator.unobtrusive.adapters.addBool('minage');

}(jQuery));

Ajax call

If you want your form to call an Ajax request, you need the following notation. The AjaxOptions contains the JavaScript functions that are called.

The view

@using (Ajax.BeginForm("AjaxRegister", null,
    new AjaxOptions
    {
        OnSuccess = "success",
        OnFailure = "failed",
        OnBegin = "onbegin",
        HttpMethod = "Post"
    }, null))
{
    @Html.AntiForgeryToken()

    <div>
        @Html.LabelFor(x => x.FirstName)
        @Html.EditorFor(x => x.FirstName)
        @Html.ValidationMessageFor(x => x.FirstName)
    </div>

    <button type="submit">
        Submit form
    </button>
}

<script>
    function onbegin() {
        $("button").prop("disabled", true);
    }

    function success(response) {
        alert(response);
    }

    function failed() {
        $("button").prop("disabled", false);
    }
</script>

The controller

[HttpPost]
[ValidateAntiForgeryToken]
public string AjaxRegister(FormModel formModel)
{
    return "Success";
}
circle
Oscar de Vaal

.NET Developer