top button
Flag Notify
    Connect to us
      Site Registration

Site Registration

Emitting Exceptions From LINQ To SQL Classes In ASP.NET MVC Validation Summary

+4 votes
374 views

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.

posted Oct 21, 2016 by Shivaranjini

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


Related Articles

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

At times you need to pass data from an action method belonging to one controller to an action method belonging to another controller. There are three ways to accomplish this task. They are:

  • Pass data as query string parameters
  • Pass data in TempData dictionary
  • Pass data as route parameters

 Let's quickly see how each of these three approaches work.

Pass data as query string parameters

This approach is possibly the most primitive one and involves no special consideration from your side. The action method sending the data can use Redirect() method or RedirectToAction() method to transfer the control to the receiving action method. The following code shows how this is done:

public ActionResult Index()
{
    Customer data = new Customer()
    {
        CustomerID = 1,
        CustomerName = "Abcd",
        Country = "USA"
    };
    string url=string.Format("/home2/index?customerid={0}
               &customername={1}&country={2}",
               data.CustomerID,data.CustomerName,data.Country);
    return Redirect(url);
}

The above code shows Index() action from Home1 controller. The Index action method instantiates Customer object - the model class - that looks like this:

public class Customer
{
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string Country { get; set; }
}

It then forms a URL pointing to the Index() action from Home2 controller. Notice how data from Customer object is transferred in the form of query string parameters. In the above example there is no logic for generating model data but in a more realistic case you may have such a logic in this method. And once the data is generated you can pass it to the another controller using query string. The Redirect() method then takes the control to the Index() action of Home2 controller.

The Index() action of Home2 can receive the data as shown below:

public ActionResult Index()
{
    Customer data = new Customer();
    data.CustomerID = int.Parse(Request.QueryString["CustomerID"]);
    data.CustomerName = Request.QueryString["CustomerName"];
    data.Country = Request.QueryString["Country"];
    return View(data);
}

As you can see Request.QueryString collection is being used to read the values passed in the query string. Once a Customer object is formed, it is passed to the Index view.

This technique has an advantage that it is quite simple and requires no additional configuration. However, it's bit crude technique and you should avoid it if any of the other techniques can be used.

Pass data using TempData dictionary

In this technique you store the data to be passed in the TempData dictionary in the sender action method. The receiving action method reads the data from the TempData dictionary. You might be aware that TempData dictionary can hold data till it is read and this can carry data across multiple requests. The following code shows how this is done:

public ActionResult Index()
{
    Customer data = new Customer()
    {
        CustomerID = 1,
        CustomerName = "Abcd",
        Country = "USA"
    };
    TempData["mydata"] = data;
    return RedirectToAction("Index", "Home2");
}

As you can see Index() action instantiates a Customer object as before. This time, however, it stores the Customer object in a TempData key named mydata. The RedirectToAction() method is then used to take the control to the Index() action of Home2 controller.

Inside the Index() of Home2, you can read the value as follows:

public ActionResult Index()
{
    Customer data = TempData["mydata"] as Customer;
    return View(data);
}

The above code reads the Customer object from TempData dictionary and passes it to the Index view.

The TempData technique doesn't require any additional setup but it requires that session state be enabled. Also, TempData is designed to store arbitrary pieces of data. If you are planning to send model objects through TempData, that may be a deviation from standard design practices.

Pass data as route parameters

In this technique you need to do an additional work of defining a route in the system. For example, if you wish to pass the Customer data in the form of route parameters you need to define a route like this:

routes.MapRoute(
    name: "Default2",
    url: "{controller}/{action}/
          {customerid}/{customername}/{country}",
    defaults: new { controller = "Home2", action = "Index" }
);

As shown above, the route includes {customerid}, {customername}, and {country} route parameters and the route is mapped with Index() action of Home2 controller. Once the above configuration is done you can pass data from the sender action as follows:

public ActionResult Index1()
{
    Customer data = new Customer()
    {
        CustomerID = 1,
        CustomerName = "Abcd",
        Country = "USA"
    };
    return RedirectToAction("Index", "Home2", data);
}

Notice that, this time the Customer object is passed as the third parameter of RedirectToAction() method. This way Customer data will be passed in the form of route parameter values. To receive this data the receiver action method should write something like this:

public ActionResult Index()
{
    Customer data = new Customer();
    UpdateModel(data);
    return View(data);
}

// OR

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

As shown above you can either use UpdateModel() method to transfer values from the route to the Customer object or you can have a parameter to the Index() action method.

This technique is quite clean and makes use of MVC specific parts (route and route parameters). It doesn't have any dependency on session state as in the case of TempData technique. On the other hand you need to create a route to deal with the route parameters.

Note about query string and route parameters

Before I conclude this post, it would be interesting to see a small thing about how RedirectToAction() deals with query string and route parameters.

Let's assume that your sender action is like this:

public ActionResult Index()
{
    Customer data = new Customer()
    {
        CustomerID = 1,
        CustomerName = "Abcd",
        Country = "USA"
    };
    return RedirectToAction("Index", "Home2", data);
}

Notice that RedirectToAction() method passes Customer data object to Index() of Home2 as the third parameter.

The interesting thing to note is - If you haven't defined any route to match the data MVC sends it as query string. And if you have defined a route, it passes the values as route parameters. You can confirm this by observing the browser address bar once the Index() of Home2 renders the view. The following figure shows the difference:

image

That also means in the query string technique discussed earlier, you could have used exactly same code in the receiving action as in the case of route parameter technique.

In addition to the three techniques discussed above you can also use Session object but that's not discussed here separately since TempData technique anyway depends on the session storage.

That's it for now. Keep coding!

READ MORE

Displaying images from wellknown URLs is quite straightforward. At times, however, you need to display images that are available as raw binary data. Consider, for example, that you are building a Captcha system that generates images on the fly. These images won't reside on the server as physical files. They will be generated and held in memory using System.Drawing classes (or something similar). To display them you can't point the src attribute of an <img> element to a particular URL as such.

There can be multiple ways to deal with this problem. This article discusses a couple of them.

The first approach that I discuss involves sending a Base64 representation of the image through ViewBag. The action method under consideration generates such a Base64 version of the image (often called Data URL) and then pass it to the view via a ViewBag property. Here is how this is done:

public ActionResult Index()
{
    string path = Server.MapPath("~/images/computer.png");
    byte[] imageByteData = System.IO.File.ReadAllBytes(path);
    string imageBase64Data=Convert.ToBase64String(imageByteData);
    string imageDataURL= string.Format("data:image/png;base64,{0}", imageBase64Data);
    ViewBag.ImageData = imageDataURL;
    return View();
}

The above code shows Index() action method of HomeController. For the sake of simplicity it uses a physical image file rather than dynamically generated image. The image file is read as a byte array using ReadAllBytes() method. In a more realistic situation you will replace the first two lines with the image generation logic of your own.

Once the image content is read as a byte array, it is converted into a Base64 string using ToBase64String() method of Convert class. This Base64 string is used to form a data URL as shown. Notice how the data URL has data:image/png;base64 at the beginning. This way the browser knows that the src attribute value itself contains the image data. Make sure to change the image type (.png / .jpg / .gif etc.) as per your needs, Then a ViewBag variable named ImageData is set to this data URL.

The Index view makes use of this ViewBag property as shown below:

<img src="@ViewBag.ImageData" />

The following figure shows a sample run of the above code:

image

Let's see another technique to achieve the same result. This technique calls for creation of another action method. Instead of passing image data through a ViewBag property the src attribute of the <img> element will point to the second action method you create. Here is how this approach works:

public ActionResult GetImage()
{
    string path = Server.MapPath("~/images/computer.png");
    byte[] imageByteData = System.IO.File.ReadAllBytes(path);
    return File(imageByteData, "image/png"); 
}

Here, the GetImage() action method reads the image file into a byte array. It then uses File() method of the Controller base class to send the contents to the caller. The first parameter is a byte array that represents the file content and the second parameter indicates the MIME content type. Make sure to change the content type as per your needs.

To use the GetImage() action method you will write this markup in the view:

<img src='@Url.Action("GetImage", "Home")'/>

The src attribute of the image tag points to /Home/GetImage. If you run the application the result would be the same as in earlier case.

That's it! Keep coding!!

READ MORE

ASP.NET MVC organizes view files in the Views folder. This arrangement works fine for most of the applications. However, in some cases you may need to load views from database rather than physical disk file. Consider an example wherein you are building a portal or a content management system. You may want to allow the administrator to create or modify views and then you may want to load these views dynamically. Such dynamically created / modified views may not be a nice fit into the folder based arrangement of MVC. In such cases the recommended way is to store views in a database and load them on the fly. This article discusses how t his can be accomplished.

The Views database table

In order to load views from a database you first need to create a table (I have named it - Views) as shown below:

image

You can add the Views table in any existing database or create a database in App_Data specifically to house Views table. The Views table has four columns - Id, ViewName, ViewPath and ViewContent. The ViewName column stores a developer friendly name of a view. This name is purely for your own use and identification. The ViewPath column stores the path of a view from the root folder. For example, /Views/Home/Index.cshtml. Note that ViewPath is not a ~ qualified path. This simplifies your code that checks existence of a view (you will see that later). The ViewContent column holds the actual content of a view. This can be HTML markup and / or Razor code.

Entity Framework data model to read view data

Next, you need to have some way to read the data stored in the Views table shown above. You can write a POCO using plain ADO.NET or use EF model to do that. I my example I am using EF generated model. You also need to add a model class for the Employees table of the Northwind database. Again, this can be a POCO or EF designer generated class. I am using EF designer generated class. The two EF entities mentioned above are shown in the following figure:

image

Controller and view content

Now, add Home controller to the Controllers folder and modify its Index() action method as follows:

public ActionResult Index()
{
    NorthwindEntities db=new NorthwindEntities();
    List<Employee> model = db.Employees.ToList();
    ViewBag.Message = "This view is loaded from database!";
    return View(model);
}

The Index() action simply pulls all the Employee records into a List and passes that List to the view. It also sets Message property on the ViewBag. The Message property has been added just for the sake of testing.

Then add Index view to the project and modify it as shown below:

@model List<ViewsInDbDemo.Models.Employee>

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <h1>List of Employees</h1>
    <h2>@ViewBag.Message</h2>
    <table border="1" cellpadding="10">
        @foreach(var item in Model)
        {
            <tr>
                <td>@item.EmployeeID</td>
                <td>@item.FirstName</td>
                <td>@item.LastName</td>
            </tr>
        }
    </table>
</body>
</html>

The Index view is quite straightforward and simply outputs the model data (Employees) and ViewBag message (Message) to the response stream. You can run the application and see whether the view displays the Employee data and Message as expected.

Now comes the important part! Copy this whole view content and paste it into the ViewContent column of the Views table that you created earlier. And then DELETE this Index.cshtml file. That's because we want to load views from database and not from physical disk files. Also set the ViewPath column to /Views/Home/Index.cshtml.

Creating Custom VirtualPathProvider and VirtualFile

The key part of our solution is the creation of a custom VirtualPathProvider class and a VirtualFile class. A VirtualPathProvider does the job of picking the views from disk, database or any other location. In our case it will pick views from database. The views picked from database are wrapped in a custom VirtualFile so that MVC framework can further deal with it.

To create a VirtualPathProvider class, add a new class to the project as shown below:

public class BinaryIntellectVirtualPathProvider : VirtualPathProvider
{
    public override bool FileExists(string virtualPath)
    {
        var view = GetViewFromDatabase(virtualPath);

        if (view == null)
        {
            return base.FileExists(virtualPath);
        }
        else
        {
            return true;
        }
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        var view = GetViewFromDatabase(virtualPath);

        if (view == null)
        {
            return base.GetFile(virtualPath);
        }
        else
        {
            byte[] content = ASCIIEncoding.ASCII.
                             GetBytes(view.ViewContent);
            return new BinaryIntellectVirtualFile
                          (virtualPath,content);
        }
    }

    public override CacheDependency GetCacheDependency
     (string virtualPath,Enumerable virtualPathDependencies, 
      DateTime utcStart)
    {

        var view = GetViewFromDatabase(virtualPath);

        if (view !=null)
        {
            return null;
        }

        return Previous.GetCacheDependency(virtualPath, 
           virtualPathDependencies, utcStart);
    }

    private View GetViewFromDatabase(string virtualPath)
    {
        virtualPath = virtualPath.Replace("~", "");

        ViewsDbEntities db = new ViewsDbEntities();
        var view = from v in db.Views
                    where v.ViewPath == virtualPath
                    select v;
        return view.SingleOrDefault();
    }
}

The BinaryIntellectVirtualPathProvider class inherits from VirtualPathProvider base class. The VirtualPathProvider class resides in System.Web.Hosting namespace. It then overrides a few methods of the base class namely FileExists() and GetFile(). There is also a helper method GetViewFromDatabase().

The GetViewFromDatabase() method retrieves a requested view from the database (Views table) and returns it to the caller. The same method can be used to determine whether a view exists in the database or not. For example, if GetViewFromDatabase() returns null it indicates that a requested view doesn't exist in the Views table.

The FileExists() overridden method checks whether a view exists in the database. If it doesn't then it calls the FileExists() of the base class, otherwise it returns true.

The GetFile() overridden method does the job of retrieving a view from the database and returning it to the caller. The method first checks whether the requested view exists in the Views table. If it doesn't then it calls the GetFile() method of the base class. If the view exists in the database it creates a new instance of  BinaryIntellectVirtualFile class (discussed shortly). The virtual path and the view content (ViewContent column) are passed to the constructor of the BinaryIntellectVirtualFile class. Notice that the view content is passed after converting it into a byte array. This is because BinaryIntellectVirtualFile class writes this content into a MemoryStream and MemortStream requires its content as byte array.

Now add a custom VirtualFile class class as shown below:

public class BinaryIntellectVirtualFile : VirtualFile
{
    private byte[] viewContent;

    public BinaryIntellectVirtualFile(string virtualPath, 
       byte[] viewContent) : base(virtualPath)
    {
        this.viewContent = viewContent;
    }

    public override Stream Open()
    {
        return new MemoryStream(viewContent);
    }
}

The BinaryIntellectVirtualFile class inherits from VirtualFile base class (System.Web.Hosting) and overrides Open() method. Notice that the constructor of BinaryIntellectVirtualFile receives view content as a byte array. The Open() method constructs a new MemoryStream based on this byte array and returns the MemoryStream to the caller.

Register the VirtualPathProvider

The final step is to register the custom VirtualPathProvider with the MVC framework. You do this in Global.asax as shown below:

protected void Application_Start()
{
  ...
  HostingEnvironment.RegisterVirtualPathProvider
    (new BinaryIntellectVirtualPathProvider());
}

To register a custom VirtualPathProvider you use HostingEnvironment class from System.Web.Hosting namespace. The RegisterVirtualPathProvider() method accepts a new instance of a VirtualPathProvider class (BinaryIntellectVirtualPathProvider in my example).

 You can run the application and see the view loaded from database in action. Here is a sample run:

image

That's it for now! Keep coding.

READ MORE
...