Introduction
Validating user input has always been a challenging task for web developers. Not only do we want validation logic executing in the browser, but also we must validate the logic running on the server. The client-side logic gives users instant feedback on the information they entered into a web page and is an expected feature in today’s applications. Meanwhile, the server validation logic is in place because you should never trust information arriving from the network.
In this article, we will see how custom validation logic works in data annotations in MVC framework. We will see how we can implement our custom logic in data annotation attributes. If you are new to data annotation validation please read my first article for built in data annotation validation.
In this article, we will try to implement our custom validation logic for the address field in which the address cannot accept more than 50 words.
Overview
For this article, we created an application ASP.NET MVC application name DataAnnotationsValidations (you can download the source code for better understanding) and we are using Student Model Class that contains student relation information in which we are going to validate using Data Annotation.
- public class StudentModel
- {
- public Guid StudentId { get; set; }
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public DateTime DateOfBirth { get; set; }
- public string Address { get; set; }
- public string ContactNo { get; set; }
- public string EmailId { get; set; }
- public string ConfirmEmail { get; set; }
- public string UserName { get; set; }
- public string Password { get; set; }
- }
We have added a student Controller and added Post action method to add new student. In this post action method, we will apply and test the data annotation validation.
Student Controller
- using System.Web.Mvc;
- using DataAnnotationsValidations.Models;
-
- namespace DataAnnotationsValidations.Controllers
- {
- public class StudentController : Controller
- {
-
- public ActionResult Index()
- {
- return View();
- }
-
-
- public ActionResult Create()
- {
- return View();
- }
-
-
- [HttpPost]
- public ActionResult Create(StudentModel student)
- {
- try
- {
- if (ModelState.IsValid)
- {
-
- return RedirectToAction("Index");
- }
- return View();
- }
- catch
- {
- return View();
- }
- }
-
- }
- }
We have added a student view for Create action method when we run that view it will look like this.
Create Student View
- @model DataAnnotationsValidations.Models.StudentModel
-
- @{
- ViewBag.Title = "Add Student";
- }
-
- <h3>Add New Student</h3>
-
- @using (Html.BeginForm())
- {
- @Html.AntiForgeryToken()
-
- <div class="form-horizontal">
- <hr />
- @Html.ValidationSummary(true, "", new { @class = "text-danger" })
- <div class="form-group">
- @Html.LabelFor(model => model.StudentId, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.StudentId, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.StudentId, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.FirstName, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.FirstName, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.DateOfBirth, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.DateOfBirth, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.DateOfBirth, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.Address, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.Address, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.Address, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.ContactNo, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.ContactNo, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.ContactNo, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.EmailId, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.EmailId, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.EmailId, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.ConfirmEmail, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.ConfirmEmail, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.ConfirmEmail, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.UserName, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.UserName, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
- @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
- </div>
- </div>
-
- <div class="form-group">
- <div class="col-md-offset-2 col-md-10">
- <input type="submit" value="Create" class="btn btn-danger" />
- </div>
- </div>
- </div>
- }
This is the initial set up we need to run this data annotation validation project. Now we are going the discuss the validation available in data annotation one by one.
Note
Data annotations are attributes we can find in the System.ComponentModel.DataAnnotations namespace. These attributes provide server-side validation and the framework also supports client-side validation.
Custom Annotations
Imagine we want to restrict the address field value of a student to limited number of words. For example, we might say 50 words is more than enough for an address field. You might also think that this type of validation (limiting a string to a maximum number of words) is something we can reuse with other modules in our application. If so, the validation logic is a candidate for packaging into reusable attributes.
So making the attributes reusable we are going to place all the custom annotation logic in one common place/folder, say Attributes, as we know all the validation annotations (such as Range, StringFormat ) ultimately derive from the ValidationAttribute base class. This base class is abstract and resides under System.ComponentModel.DataAnnotations namespace.
To keep validation logic in one place I have added an Attributes folder name in the application solution and added a class MaxWordAttributes class in it like this:
- using System.ComponentModel.DataAnnotations;
-
- namespace DataAnnotationsValidations.Attributes
- {
- public class MaxWordAttributes : ValidationAttribute
- {
- }
- }
To implement the validation logic, we need to override one of the IsValid methods provided by the base class. Overriding the IsValid version takes ValidationContext as a parameter. The ValidationContext parameter gives us access to the model type, model object instance, and friendly display name of the property we are validating, among other pieces of information.
- public class MaxWordAttributes : ValidationAttribute
- {
- protected override ValidationResult IsValid(object value, ValidationContext validationContext)
- {
- return ValidationResult.Success;
- }
- }
The first parameter to the IsValid method is the value to validate. If the value is valid you can return successful validation results, but before we can determine whether the value is valid we need to know the count of the words in that field. So for that a programmer should pass how many values is too many for that property, to do that we need to create a constructer to accept a number of words.
- public class MaxWordAttributes : ValidationAttribute
- {
- private readonly int _maxWords;
- public MaxWordAttributes(int maxWords)
- {
- _maxWords = maxWords;
- }
- protected override ValidationResult IsValid(object value, ValidationContext validationContext)
- {
- return ValidationResult.Success;
- }
- }
Now we have parameterized the maximum word count so we can implement our validation logic and catch the validation error.
- public class MaxWordAttributes : ValidationAttribute
- {
- private readonly int _maxWords;
- public MaxWordAttributes(int maxWords)
- {
- _maxWords = maxWords;
- }
- protected override ValidationResult IsValid(object value, ValidationContext validationContext)
- {
- if (value == null) return ValidationResult.Success;
- var textValue = value.ToString();
- return textValue.Split(' ').Length > _maxWords
- ? new ValidationResult("Too long Address!")
- : ValidationResult.Success;
- }
- }
Here we just split the value and compare the count with maxWords. If it is greater than that it should return validation error message otherwise a successful result.
Here the problem with the last line new ValidationResult("Too long Address!") is we can see this is just a hardcoded error message. To write a good quality code we never want a hardcoded error message. We always want to pass the error message and that error message should display instated of this hardcoded one.
To do that we just need to change our code a bit.
- public class MaxWordAttributes : ValidationAttribute
- {
- private readonly int _maxWords;
- public MaxWordAttributes(int maxWords)
- : base("{0} has to many words.")
- {
- _maxWords = maxWords;
- }
- protected override ValidationResult IsValid(object value, ValidationContext validationContext)
- {
- if (value == null) return ValidationResult.Success;
- var textValue = value.ToString();
- if (textValue.Split(' ').Length <= _maxWords) return ValidationResult.Success;
- var errorMessage = FormatErrorMessage((validationContext.DisplayName));
- return new ValidationResult(errorMessage);
- }
- }
We made two changes to our above code.
- First we place the default error message to the base constructer. We should pull default error message from a resource file if we build an international stander application.
- Second notice how the default error message includes a parameter placeholder ({0}). The placeholder exists because the second change, the call inherited FormatErrorMessage method, will automatically format the string using the display name property.
FormatErrorMessage ensures that we use a correct error message string even if the string is localized into a resource file. We can use our custom annotation attributes in the following ways.
- [DataType(DataType.MultilineText)]
- [MaxWordAttributes(50)]
- public string Address { get; set; }
In first use we will get default error message if validation failed and in the second use we will get the message that we set in our model.
- [DataType(DataType.MultilineText)]
- [MaxWordAttributes(50,ErrorMessage="There are too many words in {0}.")]
- public string Address { get; set; }
Remote
The ASP.NET MVC framework adds an additional Remote validation attribute. This attribute is in System.Web.Mvc namespace.
The Remote attributes enable us to perform client-side validation with server callback. Take for example the UserName property is of a StudentModel, We are not going to allow the user name that already exists in our database. This means to say for every student there must be a unique UserName. To achieve this we need to use Remote attributes.
- [Remote("CheckUserName","Student")]
- public string UserName { get; set; }
Inside the attributes we can set the name of the action and the name of the controller the client call should call. The client code will send the value the user enters for the UserName property automatically and an overload of the attributes constructor allow us to specify additional fields to send to the server.
- public JsonResult CheckUserName(string userName)
- {
- var studentUserList = new List<string> {"Manish", "Saurabh", "Akansha", "Ekta", "Rakesh", "Bhayia Jee"};
-
- var result = studentUserList.Any(userName.Contains);
- return Json(result, JsonRequestBehavior.AllowGet);
- }
For example, if we have a user hardcoded string list we can easily replace that string list with the value we get from database. When we run the application and try to enter the value that is inside the string list it will show validation failed error message.
Conclusion
In this article, we learned about the custom data annotation validation in ASP.Net MVC framework. If you have any questions or comments regarding this article, please post it in the comment section of this article. I hope you like it.