top button
Flag Notify
    Connect to us
      Site Registration

Site Registration

Perform Multi-Field Validation Using Custom Data Validation Attributes

+5 votes
380 views

Data annotation based validations validate one property at a time. For example, if you decorate CustomerID property of a class with [Required] data annotation attribute, it validates only the CustomerID property. Although this behavior is what is necessary in most of the cases, at times your outcome of a validation may depend on multiple properties. One can deal with such situations using IValidatableObject interface but the downside is that such a validation may not fit seamlessly with data annotation attributes.

In this article you will learn how to create a custom data validation attribute that performs validation based on multiple properties of the class. Specifically you will learn the following two approaches:

  • Create a custom data validation attribute to decorate a single property.
  • Create a custom data validation attribute to decorate the model class.

Which of these two approaches to use depends on your needs. The former approach is good if your class has, say, 10 properties and your validation requires two or three of them. The later approach is good if your validation depends on the object as a whole.

Custom data validation attribute for a property

Before you start developing such a data validation attributes, have a look at the following screen shot:

image

The above figure shows a Customer entry page that accepts CustomerID, CompanyName, ContactName and Country from the user. It performs validation on the CompanyName such that CompanyName ends with a specific phrase depending on a specific Country. For example, if the Country is USA, CompanyName should end with LLC or for an Indian company it should end with Ltd. and so on. If this multi-field validation fails an error message as shown in the figure is thrown back to the user.

To develop this data validation attribute create a new ASP.NET MVC application. Add ADO.NET Entity Data Model for Customers table of the Northwind database and also add HomeController.

Next, add a class named CompanyNameAttribute as shown below:

 public class CompanyNameAttribute:ValidationAttribute
{
  protected override ValidationResult IsValid(object value, 
                ValidationContext validationContext)
  {
    ...
  }
}

As you can see, CompanyNameAttribute class inherits from System.ComponentModel.DataAnnotations.ValidationAttribute base class. It then overrides IsValid() method. The IsValid() method takes two parameters - value and validationContext. The value parameter supplies the value of the property to which this attribute is attached (CompanyName in our case). The validationContext parameter can be used to grab the object instance under consideration. We can then inspect its other properties (as you will see later). The IsValid() method returns ValidationResult object indicating the validation error (if any).

Now add the following code inside the IsValid() method:

bool flag = false;
object instance = validationContext.ObjectInstance;
Type type = instance.GetType();
PropertyInfo property = type.GetProperty("Country");
object propertyValue = property.GetValue(instance);

The above code declares a Boolean variable - flag - that is set to true if the validation succeeds, else it is set to false. Then ObjectInstance property of validationContext is used to grab the underlying instance of Customer class. Remember that our custom attribute will be applied to the CompanyName property of the Customer class. The GetType() method is used to get t he type information of the object instance. Make sure to import System.Reflection for this code to compile correctly. Then GetProperty() method of Type object is called specifying Country as the property name. This way a PropertyInfo object for Country property will be returned. Finally, GetValue() method is used to retrieve the actual value of the Country property.

So far we haven't performed any validation. Next, add the following code:

if (propertyValue == null)
{
    flag = false;
}
else
{
    switch (propertyValue.ToString())
    {
        case "USA":
            if (value.ToString().EndsWith("LLC"))
            {
                flag = true;
            }
            break;
        case "India":
            if (value.ToString().EndsWith("Ltd"))
            {
                flag = true;
            }
            break;
        default:
            flag = true;
            break;
    }
}

The above code is quite straightforward and simply performs our custom validation mentioned earlier. Depending on the switch conditions the flag variable is set. If Country property is not set altogether then flag is set to false.

Finally, add the concluding piece of code as shown below:

if(!flag)
{
    ValidationResult result = new ValidationResult
                    ("Invalid Company Name for the specified country!");
    return result;
}
else
{
    return null;
}

The above code block checks the flag variable. If it is false, a ValidationResult is created with a custom error message and is returned from the IsValid() method. If flag is true, null is returned.

This completes the custom CompanyNameAttribute. Now, it's time to use this attribute. Add a metadata class to the project as shown below:

public class CustomerMetadata
{
    [CompanyName]
    [StringLength(20)]
    public string CompanyName { get; set; }
}

The above code creates a CustomerMetadata class and defines CompanyName property in it. The CompanyName property is decorated with two data validation attributes - our [CompanyName] attribute and inbuilt [StringLength] attribute.

Then add Customer partial class that links the CustomerMetdata class with the Customer model class.

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{

}

As you can see, [MetadataType] attribute associates the CustomerMetadata class with the Customer model class.

Now, add Index view to the project and place the following markup in it:

@model MultiFieldCustomDataAnnotationDemo.Models.Customer

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    @using(Html.BeginForm("Index","Home",FormMethod.Post))
    {
        <h1>Add New Customer</h1>
        <table border="1" cellpadding="10">
            <tr>
                <td>Customer ID :</td>
                <td>@Html.TextBoxFor(m => m.CustomerID)</td>
            </tr>
            <tr>
                <td>Company Name :</td>
                <td>
                    @Html.TextBoxFor(m => m.CompanyName)
                    @Html.ValidationMessageFor(m => m.CompanyName)
                </td>
            </tr>
            <tr>
                <td>Contact Name :</td>
                <td>@Html.TextBoxFor(m => m.ContactName)</td>
            </tr>
            <tr>
                <td>Country :</td>
                <td>@Html.TextBoxFor(m => m.Country)</td>
            </tr>
            <tr>
                <td colspan="2">
                    <input type="submit" value="Submit" />
                </td>
            </tr>
        </table>
    }

    @Html.ValidationSummary()
</body>
</html>

The view markup is straightforward and uses ValidationMessageFor() and ValidationSummary() helpers to display validation errors.

Run the application and test our validation by entering some invalid CompanyName and Country combinations.

Custom data validation attribute for the whole model class

In the preceding approach you developed [CompanyName] attribute and decorated the CompanyName model property. In this approach you will create [ValidateCustomer] attribute and decorate the Customer class with it. In this case a sample run looks like this:

image

Did you notice the difference? In the precious case ValidationMessageFor() as well as ValidationSummary() emitted the error message. In this case only ValidationSummary() displays the error because we are validating the Customer object as whole rather than its individual properties.

In this case the custom data validation attribute looks like this:

public class ValidateCustomerAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        bool flag = false;
        Customer obj = (Customer)value;
        switch (obj.Country)
        {
            case "USA":
                if (obj.CompanyName.EndsWith("LLC"))
                {
                    flag = true;
                }
                break;
            case "India":
                if (obj.CompanyName.EndsWith("Ltd"))
                {
                    flag = true;
                }
                break;
            default:
                flag = true;
                break;
        }

        return flag;
    }
}

The ValidateCustomerAttribute class inherits from ValidationAttribute class as before. This time, however, it overrides another version of IsValid() method. This version of IsValid() accepts object value and returns a Boolean value indicating success or failure of the validation. Inside, we perform the same validation as earlier and return flag variable. Notice that since [ValidateCustomer] will be applied to Customer class, the whole Customer object under consideration is passed to the IsValid() method as value parameter. Thus all the properties of Customer class can be accessed inside IsValid() method.

To use [ValidateCustomer] attribute add it on the metadata class as follows:

[ValidateCustomer]
public class CustomerMetadata
{
    ...
}

As you can see [ValidateCustomer] is applied on top of the CustomerMetadata class (and hence Customer model class) rather than CompanyName (or any other) property. Just like any other data validation attribute you can customize the error message by setting the ErrorMessage property of the base class.

[ValidateCustomer(ErrorMessage = 
"Invalid Customer data. Please check all the fields.")]
public class CustomerMetadata
{
  ....
}

That's it! You can now run the application and try entering some invalid values. If all is well you should see our custom error message displayed in the ValidationSummary().

posted Oct 27, 2016 by Shivaranjini

  Promote This Article
Facebook Share Button Twitter Share Button LinkedIn Share Button


Related Articles

HTML5 custom data attributes (data-*) are used to store arbitrary pieces of metadata about an element. One way to store such metadata in data-* attributes is to create a separate data-* attribute for each piece of information you wish to store. This approach works well if there are only a few data-* attributes. However, at times you need to store a bunch of metadata in data-* attributes. In such cases instead of creating multiple data-* attributes you can create just one data-* attribute and store all the pieces of  metadata as an object in JSON format. To that end this article illustrates how custom data attributes can be used to store JSON data in an ASP.NET MVC application.

Consider the following view:

image

The above view shows a table that lists records from Customers table of Northwind database. Each row has Order Details button. Click on the Order Details button displays details such as OrderID and ShippingDate for the last order placed by the customer under consideration. These details are displayed in an alert dialog like this:

image

If you see the Customers table, it has only two columns - one showing CustomerID and the other showing CompanyName. From where does the order details such as OrderID and OrderDate came from? These details are stored in a custom data attribute named data-lastorder of each <tr> element. For example, consider the following markup that is revealed in the HTML source of the page in the browser:

image

As you can see the <tr> element has data-lastorder attribute that stores an object in JSON format. The object stores CustomerID, OrderID and OrderDate. The data-lastorder attribute can be accessed using jQuery code. The following jQuery code shows how this can be accomplished.

 $(document).ready(function () {
  $("input:button").click(function (evt) {
    var orderData = $(evt.target).closest("tr").data("lastorder");
    alert("Last Order : #" + orderData.OrderID + " on " + orderData.OrderDate);
  });
});

As you can see the ready() handler uses :button selector to match all the <input> elements of type button. This will return all the Order Details buttons shown in the earlier figure. The code then wires click event handler to the click event of the Order Details buttons. The click event handler finds <tr> element closest to the button being clicked. This is done using the closest() method. The data() method returns the value of a specified custom data attribute. In this case lastorder is specified (you can omit data- from the attribute name). The return value of data() in this case will be a JSON object and is stored in orderData variable. An alert dialog then displays the OrderID and OrderDate properties of the orderData object.

So far so good. But how to emit the data-lastorder attribute to the client browser? Obviously you can't hard-code it. It has to be generated on the fly using server side code. In this case you will have to write such code in the controller and the view.

Let's assume that you have Entity Framework data model for the Customers and Orders tables as shown below:

image

 

Also assume that you have HomeController with Index() method that fetches the data and makes it suitable to go inside data-lastorder attribute. The following code shows how Index() method can do this job:

public ActionResult Index()
{
  NorthwindEntities db = new NorthwindEntities();
  var customers = from c in db.Customers
                  where c.Country == "USA"
                  select c;
  List<Customer> customerList = customers.ToList();
  Dictionary<string, string> orderDict = new Dictionary<string, string>();
  foreach (Customer obj in customerList)
  {
    var order = (from o in db.Orders
                 where o.CustomerID == obj.CustomerID
                 orderby o.OrderDate descending
                 select o).FirstOrDefault();
    string jsonOrder = JsonConvert.SerializeObject(
                       new { CustomerID=order.CustomerID,
                             OrderID=order.OrderID,
                             OrderDate=order.OrderDate });
    orderDict.Add(order.CustomerID, jsonOrder);
  }
  ViewData["orderDict"] = orderDict;
  return View(customerList);
}

The above code creates a database context object (db). A LINQ to Entities query selects all the Customer objects with Country property equal to USA. Then a generic List of Customer objects is obtained by calling ToList() method of the customers variable. A dictionary is created to store order information for all the customers. The key of the dictionary is CustomerID and its value is JSON representation of the order details. Next, a foreach loop iterates through all the customer list. With every iteration the most recent Order is retrieved from the Orders DbSet for a CustomerID. This is done by sorting the results in descending order and then calling FirstOrDefault() method. Then comes the important part. The JsonConvert class of Json.NET library is used to serialize an anonymous object containing order details such as CustomerID, OrderID and OrderDate. The SerializeObject() method accepts an object and returns its JSON representation as a string. The JSON representation of the order data is stored in the dictionary created earlier. The customerList is passed to the Index view as its model whereas orderDict is passed to the view in a ViewData variable.

This completes the controller. The remaining part of the code goes inside the Index view and is shown below:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage
<List<JSONObjectInCustomDataAttribute.Models.Customer>>" %>
...
<table border="1" cellpadding="6">
<% foreach(var customer in Model){ %>
<tr data-lastorder='<%= ((Dictionary<string,string>)ViewData["orderDict"])
                         [customer.CustomerID] %>'>
<td><%= customer.CustomerID %></td>
<td><%= customer.CompanyName %></td>
<td><input type="button" value="Order Details" /></td>
</tr>
<%}%>
</table>

As shown in the above code, @Page directive sets the model for the view to List<JSONObjectInCustomDataAttribute.Models.Customer>. Make sure to change the namespace of the data model class as per your setup. Then a <table> is generated by iterating through the Model. Each <tr> element emitted has data-lastorder attribute and its value is assigned from the orderDict ViewData variable. Recollect that orderDict is a dictionary where key is CustomerID and value is the order data in JSON format. Then CustomerID, CompanyName values are added to two table cells. The third table cell contains the Order Details button.

That's it! You can now run the application and see whether order data is available in data-lastorder attribute as expected.

READ MORE

Recently while developing a sample application in ASP.NET MVC, I came across a peculiar situation. There was a LINQ to SQL class with several extensibility methods defined (OnXXXXChanging kind of methods where XXXX is the name of a property). The methods were performing some validation checks and in case of any violation were throwing exceptions. The LINQ to SQL class was working fine. However, at some places I wanted to display the validation errors (thrown by the class as exceptions) on the ASP.NET MVC views. That is where the problem began. No matter what exception you throw in the class the Validation Summary never displays the error message. This behavior is by design and is intended to hide sensitive exception details from the end user. In this specific, however, I wanted to reveal the exception message to the end user because all exceptions were basically validation errors and I was sure that they are not disclosing any sensitive system information. To overcome the problem I developed a custom action filter. The remainder of this article explains how the custom action filter works.

To understand the problem let's first reproduce the error by developing a sample LINQ to SQL class. Begin by creating a new ASP.NET MVC web application. Once created add a SQL database to it and create an Employee table in it. The schema of the Employee table is shown below:

image

Now add a new LINQ to SQL class to the web application and create the Employee LINQ to SQL class by dragging and dropping the Employees table from the Server Explorer onto the design surface of .dbml file.

image

Now, add a new class in the Models folder and modify it as shown below:

public partial class Employee
{
  partial void OnFirstNameChanging(string value)
  {
    if (value.Length < 3 || value.Length > 50)
    {
      throw new ValidationException("Invalid First Name.");
    }
  }
  partial void OnLastNameChanging(string value)
  {
    if (value.Length < 3 || value.Length > 50)
    {
      throw new ValidationException("Invalid Last Name.");
    }
  }
  ...
}

The code creates a partial class Employee and adds extensibility methods OnFirstNameChanging() and OnLastNameChanging() to it (you can add additional methods for other properties if you so wish). If there are any validation errors the methods simply throw ValidationException.

Add a controller named HomeController as shown below:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Index(Employee emp)
    {
        return View();
    }

}

The Index() action method simply renders the Index view. The Index view consists of three textboxes to enter FirstName, LastName and BirthDate and uses MVC Validations (see markup and screen capture below).

<% using (Html.BeginForm()) { %>
<%= Html.ValidationSummary()%>
<p>First Name :</p>
<%= Html.TextBoxFor(model => model.FirstName)%>
<%= Html.ValidationMessageFor(model => model.FirstName, "*")%>
<p>Last Name :</p>
<%= Html.TextBoxFor(model => model.LastName)%>
<%= Html.ValidationMessageFor(model => model.LastName, "*")%>
<p>Birth Date :</p>
<%= Html.TextBoxFor(model => model.BirthDate)%>
<%= Html.ValidationMessageFor(model => model.BirthDate, "*")%>
<p>
<input id="Submit1" type="submit" value="Submit" />
</p>
<%}%>

image

If you try entering invalid values in FirstName and LastName fields you will get errors as shown below:

image

Notice that though FirstName and LastName fields are showing error (*) there is no descriptive error message at all. The actual error messages ("Invalid First Name" and "Invalid Last Name") are suppressed by MVC framework for security reasons. However, since you are deliberately throwing these exceptions you know that showing them to the end user won't create any problem.

The solution is to create a custom action filter as shown below:

public class ShowExceptionDetailsInValidationSummary : FilterAttribute, IActionFilter
{
    public Type ExceptionType { get; set; }
    public string Keys { get; set; }
    public string ErrorMessage { get; set; }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Controller c = (Controller)filterContext.Controller;
        string[] keys = null;
        Dictionary<string, string> messages = new Dictionary<string, string>();

        if (Keys != null && Keys!=string.Empty)
        {
            keys=Keys.Split(',');
        }

        if (keys == null)
        {
            foreach (string key in c.ModelState.Keys)
            {
                foreach (ModelError err in c.ModelState[key].Errors)
                {
                    if (ExceptionType != null)
                    {
                        if (err.Exception.GetType().Equals(ExceptionType))
                        {
                            if (ErrorMessage == null || ErrorMessage == string.Empty)
                            {
                                messages.Add(key, err.Exception.Message);
                            }
                            else
                            {
                                messages.Add(key, ErrorMessage);
                            }
                        }
                    }
                    else
                    {
                        if (ErrorMessage == null || ErrorMessage == string.Empty)
                        {
                            messages.Add(key, err.Exception.Message);
                        }
                        else
                        {
                            messages.Add(key, ErrorMessage);
                        }
                    }
                }
            }
        }
        else
        {
            foreach (string key in keys)
            {
                if(c.ModelState.Keys.Contains(key))
                {
                    foreach (ModelError err in c.ModelState[key].Errors)
                    {
                        if (ExceptionType != null)
                        {
                            if (err.Exception.GetType().Equals(ExceptionType))
                            {
                                if (ErrorMessage == null || ErrorMessage == string.Empty)
                                {
                                    messages.Add(key, err.Exception.Message);
                                }
                                else
                                {
                                    messages.Add(key, ErrorMessage);
                                }
                            }
                        }
                        else
                        {
                            if (ErrorMessage == null || ErrorMessage == string.Empty)
                            {
                                messages.Add(key, err.Exception.Message);
                            }
                            else
                            {
                                messages.Add(key, ErrorMessage);
                            }
                        }
                    }
                }
            }
        }

        foreach (string key in messages.Keys)
        {
            c.ModelState.AddModelError(key, messages[key]);
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
    }

}

The ShowExceptionDetailsInValidationSummary action filter essentially loops through the Model Errors and then programmatically adds a model error message.

Once the ShowExceptionDetailsInValidationSummary action filter is ready you can decorate the Index() action method with it as shown below :

[HttpPost]
[ShowExceptionDetailsInValidationSummary]
public ActionResult Index(Employee emp)
{
    return View();
}

If you run the application again and try to enter invalid values for FirstName and LastName you will correctly get error messages as shown in the following figure.

image

By default ShowExceptionDetailsInValidationSummary action filter will display all the exception messages in the validation summary. You can also specify that only certain exceptions be displayed.

[ShowExceptionDetailsInValidationSummary(ExceptionType=typeof(ValidationException))]

Further you can also customize the error message and keys using ErrorMessage and Keys properties of ShowExceptionDetailsInValidationSummary class respectively.

READ MORE

ASP.NET MVC offers HTML helpers for displaying field level validation error messages as well as validation summary. However, these validation messages are displayed as a plain string. There is no inbuilt way to include images or HTML markup with the output of the validation helpers. In this article I will show how to overcome this limitation using two techniques so that you can display images along with the validation messages. The techniques I discuss include:

  • Use HTML markup in the validation message to display an image.
  • Using a CSS class to display an image.

Before I delve into more details, let's quickly see the model that this example is going to use:

public class Employee
{
    public int EmployeeID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

The Employee model class contains three properties namely EmployeeID, FirstName and LastName. To keep the model clean you won't add data annotations here. You will add them to metadata class(s) as discussed later.

The Home controller that makes use of this model contains two actions as shown below:

public ActionResult Index()
{
    return View();
}


[HttpPost]
public ActionResult ProcessForm(Employee emp)
{
    if(ModelState.IsValid)
    {
        //do insert here
    }
    return View("Index", emp);
}

The Index() action simply displays the Index view like this:

image

The ProcessForm() action receives an instance of Employee model through model binding. If there are any errors they are shown like this:

image

Notice how images are being displayed by ValidationMessageFor() and ValidationSummary() helpers.

Using HTML markup in the validation message to display an image

Now let's create a metadata class that contains data annotations for the Employee class. This class - EmployeeMetadata - is shown below:

public class EmployeeMetadata
{
    [Required]
    [Range(1, int.MaxValue, 
     ErrorMessage = "<img src='/images/error.png' /> 
      Invalid EmployeeID!")]
    public int EmployeeID { get; set; }

    [Required]
    [StringLength(20, 
     ErrorMessage = "<img src='/images/error.png' /> 
      Invalid first name!")]
    public string FirstName { get; set; }

    [Required]
    [StringLength(20, 
     ErrorMessage = "<img src='/images/error.png' /> 
      Invalid last name!")]
    public string LastName { get; set; }
}

Notice how ErrorMessage property of [StringLength] attribute includes an image HTML markup tag. Once created attach this metadata class to the Employee class using [MetadataType] attribute.

[MetadataType(typeof(EmployeeMetadata))]
public class Employee
{
  ...
}

Add the Index view and change its content as per the following markup.

...
...
@using(Html.BeginForm
("ProcessForm","Home",FormMethod.Post))
{ 
  <table cellpadding="10" border="1">
    <tr>
      <td>@Html.LabelFor(m=>m.EmployeeID)</td>
      <td>
          @Html.TextBoxFor(m=>m.EmployeeID)
          @Html.ValidationMessageFor(m=>m.EmployeeID)
        </td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m => m.FirstName)</td>
        <td>
            @Html.TextBoxFor(m => m.FirstName)
            @Html.ValidationMessageFor(m => m.FirstName)
        </td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m => m.LastName)</td>
        <td>
            @Html.TextBoxFor(m => m.LastName)
            @Html.ValidationMessageFor(m => m.LastName)
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <input type="submit" value="Submit" />
        </td>
   </tr>
 </table>
}
@Html.ValidationSummary()
...
...

So far so good. If you run the application at this point in time you will see this:

image

That's because ASP.NET MVC by default HTML encodes the output of validation helpers. So, your embedded HTML fragment is not treated as actual HTML and no image is displayed.

Now let's fix this problem with a bit of clever code.

Modify all your ValidationMessageFor() calls like this:

@Html.Raw(
HttpUtility.HtmlDecode(
@Html.ValidationMessageFor(m=>m.EmployeeID).ToHtmlString()
))

Notice the above like of code carefully. The return value of ValidationMessageFor() is MvcHtmlString. Calling ToHtmlString() method on it gives you the HTML encoded plain string. This HTML encoded plain string is then decoded using HtmlDecode() method of HttpUtility object. Doing so will give us raw HTML string. This string is finally sent to the response stream using Raw() helper.

Do the same for ValidationSummary() helper:

@Html.Raw(
HttpUtility.HtmlDecode(
@Html.ValidationSummary().ToHtmlString()
))

If you run the application now, you will see the validation errors being displayed as per Figure 2 of this article.

Using CSS class to display an image

In the above technique you embedded HTML fragment in the data annotation attributes. You may want to avoid this "ugly" way of specifying validation errors in some cases. If so, you can still achieve almost identical results using CSS classes.

Add a new metadata class or modify the existing one as shown below:

public class EmployeeMetadata
{
    [Required]
    [Range(1, int.MaxValue, 
     ErrorMessage = "Invalid EmployeeID!")]
    public int EmployeeID { get; set; }

    [Required]
    [StringLength(20, 
     ErrorMessage = "Invalid first name!")]
    public string FirstName { get; set; }

    [Required]
    [StringLength(20, 
      ErrorMessage = "Invalid last name!")]
    public string LastName { get; set; }
}

As you can see, you no longer embed HTML markup in the validation messages. Also revert back the Index view as it was before using the Raw() helper.

Next, add a CSS file and modify it to have the following style rules:

.field-validation-error {
    color: #b94a48;

    background-image: url(Images/error.png); 
    background-repeat:no-repeat;
    padding-left:22px;
    background-size:contain;
}

.validation-summary-errors {
    color: #b94a48;

    background-image: url(Images/error.png); 
    background-repeat:no-repeat;
    padding-left:44px;
    background-size: contain;
}

Remember that these CSS class names are assumed by the validation helpers and hence you should keep them exactly as shown above. The field-validation-error CSS class is applied to ValidationMessageFor() helpers whenever there is any validation error. SImilarly, validation-summary-errors CSS class is applied to ValidationSummary() whenever there is any validation error. Notice the style rules shown in bold letters. You are using a background image for the field level validations and the validation summary. You may need to adjust the padding as per your image dimensions. Note that there will be minor differences between the way images are shown in the previous technique and this technique. For example, in this technique ValidationSummary() will display just a single instance image for all the messages instead of one image per message (as in the previous technique).

That's it! Run the application and see if images are shown as expected.

READ MORE

A common way to perform list, insert, update and delete operations in ASP.NET MVC is to create four separate views. The List view forms the launching view where records are displayed and you can choose to Edit, Delete or Insert a record. However, in some cases you may want to perform all these operations in a single view itself. This task can be accomplished using full page postback or using Ajax. This article discusses the former technique.

Consider the following figure that shows one such arrangement:

image

The above figure shows a list of records from Customers table of Northwind database. You can Insert a new customer by clicking on Insert button. You can select a row for editing by clicking on the Select button. The selected customer is shown below the main table for editing. Similarly you can also delete a customer by clicking on the Delete button.

Model and View Model

Let's see how the above application can be built. Begin by creating a new empty ASP.NET MVCproject in Visual Studio. Then add an ADO.NET Entity Data Model for the Customers table. The Customer entity class is shown below:

image

Then add a new POCO to the Models folder and name it CustomersViewModel. As you will see later, this view model class will be passed from the HomeController to the Index view. The CustomersViewModel class is shown below:

public class CustomersViewModel
{
    public List<Customer> Customers { get; set; }
    public Customer SelectedCustomer { get; set; }
    public string DisplayMode { get; set; }
}

The CustomersViewModel class consists of three properties. The Customers property holds a List of Customer that are to be displayed on the view. The SelectedCustomer property points to a Customer that is selected by the user. If no Customer is selected this property is null. The DisplayMode property indicates the mode of the Customer details area. Possible values are ReadOnly (after selection), ReadWrite (during edit) and WriteOnly (during insert). For the sake of simplicity DisplayMode is created as a string property, you can easily make it to accept an enumeration.

Home controller and its action methods

Then add HomeController in the Controllers folder. The HomeController will contain the following action methods:

  • Index()
  • Select()
  • New()
  • Insert()
  • Edit()
  • Update()
  • Delete()
  • Cancel()

The method names are self-explanatory. All the actions except Index() are called as a result of POST operation. Let's discuss them briefly one by one.

public ActionResult Index()
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                m => m.CustomerID).Take(5).ToList(); 
        model.SelectedCustomer = null;
        return View(model);
    }
}

The Index() action fetches a list of customers and fills it in the Customers view model property. The SelectedCustomer is set to null because there is no selected customer in the beginning. Note that for the sake of simplicity the above code fetches only 5 customers. You can, of course, fetch all if you so wish.

[HttpPost]
public ActionResult New()
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                       m => m.CustomerID).Take(5).ToList();
        model.SelectedCustomer = null;
        model.DisplayMode = "WriteOnly";
        return View("Index", model);
    }
}

The New() action is called when a user hits the Insert button at the top of the page. It fills the Customers list as before. SelectedCustomer is set to null because a new record is to be added. The DisplayMode is set to WriteOnly because we will be accepting new customer details. The following figure shows how the insert area looks like:

image

[HttpPost]
public ActionResult Insert(Customer obj)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        db.Customers.Add(obj);
        db.SaveChanges();

        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                         m => m.CustomerID).Take(5).ToList();
        model.SelectedCustomer = db.Customers.Find(obj.CustomerID);
        model.DisplayMode = "ReadOnly";
        return View("Index", model);
    }
}

The Insert() action is called when a user fills new customer details and clicks on the Save button (see above figure). It receives a Customer object as its parameter. Inside, the Insert() action adds that new Customer to the database. It also sets the currently selected customer to the newly added customer by setting the SelectedCustomer property. The DisplayMode is set to ReadOnly so that the record is displayed in read-only manner.

[HttpPost]
public ActionResult Select(string id)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                    m => m.CustomerID).Take(5).ToList();
        model.SelectedCustomer = db.Customers.Find(id);
        model.DisplayMode = "ReadOnly";
        return View("Index",model);
    }
}

The Select() action method is called when the Select button from a customer table row is clicked. It receives CustomerID as its parameter. Inside, it fills Customers list as before. This time SelectedCustomer property is set to the Customer whose CustomerID is passed. The DisplayMode property is set to ReadOnly to indicate that the details of the selected customer should be displayed in a read-only table below the main customer listing (see below).

image

[HttpPost]
public ActionResult Edit(string id)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                        m => m.CustomerID).Take(5).ToList();
        model.SelectedCustomer = db.Customers.Find(id);
        model.DisplayMode = "ReadWrite";
        return View("Index", model);
    }
}

The Edit() action is called when a user clicks on the Edit button once a Customer is selected. Inside, it sets the SelectedCustomer property to the Customer whose CustomerID is passed to the method. DisplayMode property is set to ReadOnly to display that record in editable table as shown below:

image

[HttpPost]
public ActionResult Update(Customer obj)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        Customer existing = db.Customers.Find(obj.CustomerID);
        existing.CompanyName = obj.CompanyName;
        existing.ContactName = obj.ContactName;
        existing.Country = obj.Country;
        db.SaveChanges();
                
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                      m => m.CustomerID).Take(5).ToList();

        model.SelectedCustomer = existing;
        model.DisplayMode = "ReadOnly";
        return View("Index", model);
    }
}

The Update() action is called when a user modifies an existing Customer data and clicks on the Save button (see above figure). Inside, the code updates an existing Customer and saves the changes back to the database. Then Customers, SelectedCustomer and DisplayMode properties of the view model are set.

[HttpPost]
public ActionResult Delete(string id)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        Customer existing = db.Customers.Find(id);
        db.Customers.Remove(existing);
        db.SaveChanges();

        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                          m => m.CustomerID).Take(5).ToList();

        model.SelectedCustomer = null;
        model.DisplayMode = "";
        return View("Index", model);
    }
}

The Delete() action is called when Delete button in any of the customer row is clicked. It receives CustomerID as its parameter. Inside, it removes the specified Customer and saves the changes back to the database. The SelectedCustomer is set to null because post deletion that customer no longer exists in the database. For the same reason, DisplayMode is set to an empty string.

[HttpPost]
public ActionResult Cancel(string id)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        CustomersViewModel model = new CustomersViewModel();
        model.Customers = db.Customers.OrderBy(
                          m => m.CustomerID).Take(5).ToList();
        model.SelectedCustomer = db.Customers.Find(id);
        model.DisplayMode = "ReadOnly";
        return View("Index", model);
    }
}

The Cancel() action is called when Cancel button from the Edit area is clicked. It receives CustomerID as its parameter. It changes the DisplayMode from ReadWrite to ReadOnly so that the SelectedCustomer is displayed in read-only fashion.

Notice that all the above action methods return Index view and CustomerViewModel object.

Index view

Now, let's see what goes inside the Index view.

@model MasterDetailsDemo.Models.CustomersViewModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <style>
        .SelectedCustomer
        {
            background-color:gray;
            font-weight:bold;
        }
    </style>
</head>
<body>
    <h1>List of Customers</h1>

    <form method="post">
        <input type="submit" 
         value="Insert" formaction="/home/new" />
        <br /><br />
        <table border="1" cellpadding="10">
            <tr>
                <th>CustomerID</th>
                <th>CompanyName</th>
                <th colspan="2">Actions</th>
            </tr>
            @foreach (var item in Model.Customers)
            {
                if (Model.SelectedCustomer != null)
                {
                    if (item.CustomerID == 
                        Model.SelectedCustomer.CustomerID)
                    {
                        @:<tr class="SelectedCustomer">
                    }
                    else
                    {
                        @:<tr>
                    }
                }
                else
                {
                    @:<tr>
                }
                <td>@item.CustomerID</td>
                <td>@item.CompanyName</td>
                <td><input type="submit" 
                     formaction="/home/select/@item.CustomerID" 
                     value="Select" /></td>
                <td><input type="submit" 
                     formaction="/home/delete/@item.CustomerID" 
                     value="Delete" /></td>
                @:</tr>
            }
        </table>
    </form>
    <br /><br />
    @{
        if(Model.SelectedCustomer!=null)
        {
            if (Model.DisplayMode == "ReadOnly")
            {
                Html.RenderPartial
                ("ShowCustomer",Model.SelectedCustomer);
            }
            if (Model.DisplayMode == "ReadWrite")
            {
                Html.RenderPartial
                ("EditCustomer",Model.SelectedCustomer);
            }
        }
        if (Model.DisplayMode == "WriteOnly")
        {
            Html.RenderPartial("InsertCustomer",
            new MasterDetailsDemo.Models.Customer());
        }
    }
</body>
</html>

The Index view is divided into two logical parts. The top part displays a list of customers in a table. Notice that a CSS class SelectedCustomer is applied to the row that contains the selected CustomerID. The bottom part displays a Partial Page based on the value of DisplayMode. This way either show, insert or edit areas are displayed. Notice that there are three partial pages involved:

  • ShowCustomer.cshtml
  • EditCustomer.cshtml
  • InsertCustomer.cshtml

These three partial pages render the read-only, read-write and write-only displays respectively. All of them take Customer object as their model. Let's see each of these partial pages one by one.

ShowCustomer partial page

The following code shows ShowCustomer.cshtml partial page.

@model MasterDetailsDemo.Models.Customer

@using(Html.BeginForm("Edit","Home",FormMethod.Post))
{ 
<table border="1" cellpadding="10">
    <tr>
        <td>Customer ID :</td>
        <td>@Model.CustomerID</td>
    </tr>
    <tr>
        <td>Company Name :</td>
        <td>@Model.CompanyName</td>
    </tr>
    <tr>
        <td>Contact Name :</td>
        <td>@Model.ContactName</td>
    </tr>
    <tr>
        <td>Country :</td>
        <td>@Model.Country</td>
    </tr>
    <tr>
        <td colspan="2">
            <input type="submit" value="Edit" 
                   formaction="/home/edit/@Model.CustomerID" />
            <input type="submit" value="Cancel" 
                   formaction="/home/index" />
        </td>
    </tr>
</table>
}

Notice that the Edit and Cancel buttons submit to /home/edit and /home/index respectively. The other markup from the partial page is quite straightforward and displays CustomerID, CompanyName, ContactName and Country columns for a selected customer.

EditCustomer partial page

The following code shows what goes inside EditCustomer.cshtml:

@model MasterDetailsDemo.Models.Customer

@using (Html.BeginForm("Update", "Home", FormMethod.Post))
{
    <table border="1" cellpadding="10">
        <tr>
            <td>Customer ID :</td>
            <td>@Html.TextBoxFor(m => m.CustomerID, 
                         new { @readonly = "readonly" })</td>
        </tr>
        <tr>
            <td>Company Name :</td>
            <td>@Html.TextBoxFor(m => m.CompanyName)</td>
        </tr>
        <tr>
            <td>Contact Name :</td>
            <td>@Html.TextBoxFor(m => m.ContactName)</td>
        </tr>
        <tr>
            <td>Country :</td>
            <td>@Html.TextBoxFor(m => m.Country)</td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Save" 
                       formaction="/home/update" />
                <input type="submit" value="Cancel" 
                       formaction="/home/cancel/@Model.CustomerID" />
            </td>
        </tr>
    </table>
}

Note that Save button and Cancel button submit to /home/update and /home/cancel respectively.

InsertCustomer partial page

Finally, here is the markup of InsertCustomer.cshtml:

@model MasterDetailsDemo.Models.Customer

@using (Html.BeginForm("Insert", "Home", FormMethod.Post))
{
    <table border="1" cellpadding="10">
        <tr>
            <td>Customer ID :</td>
            <td>@Html.TextBoxFor(m => m.CustomerID)</td>
        </tr>
        <tr>
            <td>Company Name :</td>
            <td>@Html.TextBoxFor(m => m.CompanyName)</td>
        </tr>
        <tr>
            <td>Contact Name :</td>
            <td>@Html.TextBoxFor(m => m.ContactName)</td>
        </tr>
        <tr>
            <td>Country :</td>
            <td>@Html.TextBoxFor(m => m.Country)</td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Save" 
                       formaction="/home/insert" />
                <input type="submit" value="Cancel" 
                       formaction="/home/index" />
            </td>
        </tr>
    </table>
}

The Save and Cancel button submit to /home/insert and /home/index respectively.

That's it! All the parts of the application are in place. Run the application and test whether all the operations work as expected.

READ MORE
...