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.
We will see how we can implement our custom logic in data annotation attributes. If you are new in 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 address field in which address cannot accept more than 50 words.
Overview
For this article we create 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 in order 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 Creating 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 to 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 framework also supports client side validation.
Custom Annotations Imagine we want to restrict the address field value of a student to a limited number of words. For example we might say 50 words is more than enough for 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 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 result, 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 constructor to accept 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!") we can see this is just a
hardcoded error message. To write a good quality code we never want
hardcoded error message. We always want to pass the error message and that error message should display instead 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 in our above code.
- First we place the default error message to the base constructor. 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 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 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
frameworkadds an additional Remote validation attribute. This attributes 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. Which is 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 should call. The client code will send the value the user enters for the
UserName property automatically and an overload of the attributes constructor allows 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 user has hard coded 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 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 question or comments regarding this article, please post it in the comment section of this article. I hope you like it.