web 2.0

Building Better MVC Code with T4MVC

If you are a ASP.NET web developer than chances are that you have heard of or dealt with problems related to “Magic Strings”. Magic strings are taboo because they introduce a degree of fragility in your code due to the fact that they are not strongly typed. The classic example, is referencing a View Name or Route in your controller action using a string value:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return RedirectToAction("Foo");            
    }

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

As you would expect, when the Index action is invoked the user gets redirected to “Foo”. Unfortunately, if I rename the “Foo” method to “Bar” my code will still compile but it will ultimately fail at runtime. This is all due to the fact that are RedirectToAction method is using the magic string “Foo” which is not strongly typed and therefore not caught by the compiler.

As a workaround to this problem some people have went so far as to declare constants in their code for all the view names and actions. Unfortunately this is not a good solution because there is a great deal of work involved in creating and maintaining all of those constants. So the question becomes “How can I eliminate the magic strings in my code, find errors at compile time, and still make it easy to maintain?”. Well the answer is T4. T4 stands for the Text Template Transformation Toolkit. In the simplest terms, T4 is code that generates code. More specifically in MVC we have the T4MVC template which can be used create strongly typed helpers for referring to your controllers, actions, views, images, scripts and etcetera. For example, with T4MVC the code above can be written as:

 
public partial class HomeController : Controller
{
    public virtual ActionResult Index()
    {
        return RedirectToAction(MVC.Home.Foo());
    }

    public virtual ActionResult Foo()
    {
        return View();
    }
}

So now if I rename “Foo” to “Bar” the compiler will catch the error. In addition to making your controller code better, T4MVC also plays an important role in your view pages. For example consider the following snippet of code:

<head>
    <title>Magic Strings are BAD</title>
    <link rel="stylesheet" type="text/css"  href='/Themes/Emporium/default.css' />    
    <script src="/Scripts/jquery-1.4.2.min.js" type="text/javascript"></script>    
    <script src="/Scripts/jquery-ui-1.8.1.custom.min.js" type="text/javascript"></script>            
    <script src="/Scripts/weblog.js" type="text/javascript"></script>
</head>

<%= Html.ActionLink("Delete Contact", "Delete", "Contact", new { id = Model.ContactID })%>

Although this code is perfectly normal we have to be careful if we rename a script or change the name of a controller action because it will break the page. Luckily, T4MVC to the rescue. Here is the same code using T4MVC generated constants:

<head>
    <title>Magic Strings are BAD</title>
    <link rel="stylesheet" type="text/css" href="<%= Links.Themes.Emporium.default_css %>" media="all" /> 
    <script src="<%= Links.Scripts.jquery_1_4_2_min_js %>" type="text/javascript"></script>
    <script src="<%= Links.Scripts.jquery_ui_1_8_1_custom_min_js %>" type="text/javascript"></script>
    <script src="<%= Links.Scripts.weblog_js %>" type="text/javascript"></script>      
</head>

<%= Html.ActionLink( "Delete Contact", MVC.Contact.Delete( Model.ContactID )) %>

 
So hopefully by this point you are sold on the idea of using T4MVC in your MVC application. Now its just a matter of configuring it. Fortunately, T4MVC is easy to add to your application. You simply download the T4MVC zip file, extract the templates and drop them in the root directory of your MVC project. Once you include the files in your project the T4 template will execute. The templates will inspect the contents of your application and generate strongly typed references for all the assets within it. After that its just a matter of replacing your magic string references with the T4MVC generated code.

T4MVC was created by David Ebbo and was recently absorbed by the MVC Contrib project which I blogged about a couple of weeks ago. For more information and examples of how to utilize T4MVC in your application visit the T4MVC codeplex page. Also check out this intro video from Channel 9:

Get Microsoft Silverlight

Looking for an MVC Grid Control? Try MVC Contrib!

Like most .NET Web Developers I was ecstatic when MVC was released. To put it plainly, I hate WebForms. However I do find myself missing some of the great WebForm controls like the DataGridView. The DataGridView was present in every WebForm application that I wrote. I really appreciated all the subtle bells and whistles that Microsoft added to the grid over the years. I wrote my own grid control for classic ASP and I know firsthand that it is a significant undertaking to make a grid control that if feature rich and flexible enough to handle complex situations. Therefore I was not crazy about taking on the task again…

Initially, I adopted jqGrid as my new de facto grid control. From a end user’s perspective, jqGrid provides a top-notch user experience. Unfortunately, the control is heavily dependent on JavaScript so its not always the best solution for Mobile websites. In addition, jqGrid does require a fair amount of plumbing. Although it’s not difficult to implement it does take time and does not provide the rapid development experience that I grew accustomed to with the DataGridView control.

For about the last 12 months I went old school. Yes, I have manually been coding my tables by looping over a enumerable list and creating rows and columns. Just like I did ten years ago! Its ridiculous. Luckily, I recently stumbled upon the MVC Contrib Grid. In approximately 10 minutes, I downloaded the library, added a reference to the assembly and fully implemented a grid with paging and sorting! You will not believe how easy it is.

MVCContribGrid

Step 1: Creating the Controller Action

public ActionResult Manage()
{
    var categories = Repository.All<Category>();    
    return View(categories);
}


Step 2: Create the View Page

<%@ Import Namespace="MvcContrib.UI.Grid" %>
...
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">    
    <%= Html.Grid(Model).Columns( column => {    
           column.For( x => x.Name );
           column.For( x => x.Description);
    })%>
</asp:Content>  

 

Step 3: Wait! I Need Sorting

A grid is no good unless it can sort. Time to modify the controller action and view to support sorting. First of all, you need to added a reference to MvcContib.Sorting and MvcContrib.UI.Grid. Next modify the method signature to accept a GridSortOptions object. This object contains the sorting information that was submitted from the view page.

 

using MvcContrib.Sorting;
using MvcContrib.UI.Grid;
...
public ActionResult Manage(GridSortOptions sort)
{
    var categories = Repository.All<Category>();
    if (sort.Column != null)            
        categories = categories.OrderBy(sort.Column, sort.Direction);            
    ViewData["sort"] = sort;
    return View(categories);
}
 

Ok, now we just need to modify the grid on the view page to show sortable column headers.

 

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">    
    <%= Html.Grid(Model).Columns( column => {    
        column.For( x => x.Name ).Sortable(true);
        column.For( x => x.Description).Sortable(true);               
        }).Sort((GridSortOptions)ViewData["sort"])%>
    <%= Html.Pager((IPagination)Model) %>
</asp:Content>

 

Step 4: You Want Paging? No Problem.

Paging has never been easier. Just add another argument to your controller action which takes a nullable int named “page”.

 

using MvcContrib.UI.Grid;

...
        
public ActionResult Manage(GridSortOptions sort, int? page)
{
    var categories = Repository.All<Category>();
    if (sort.Column != null)            
        categories = categories.OrderBy(sort.Column, sort.Direction);
    ViewData["sort"] = sort;
    return View(categories.AsPagination(page ?? 1, 10));
}

 

Now we need to add the paging control on the view page:

 

<%@ Import Namespace="MvcContrib.UI.Grid" %>
<%@ Import Namespace="MvcContrib.UI.Paging" %>

...

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">    
    <%= Html.Grid(Model).Columns( column => {    
        column.For( x => x.Name ).Sortable(true);
        column.For( x => x.Description).Sortable(true);
     }).Sort((GridSortOptions)ViewData["sort"])%>
     
    <%= Html.Pager((IPagination)Model) %>
</asp:Content>

Admittedly, the jqGrid does have a superior look and feel and provides paging and sorting without making a post back. Unfortunately, jqGrid does require a little bit of know-how and can be frustrating to implement. Not too mention it has a heavy dependency on JavaScript which can be problematic for mobile compliant websites. In contrast, the MVC Contrib Grid couldn’t be any simpler. So what are you waiting for? Try it yourself…

Tags: ,

ASPNet | dotNet | MVC

Error Handling in MVC with ELMAH

What is ELMAH?

In case you have been living under a rock I will start this block post by giving you a basic introduction to ELMAH. If you already are familiar with ELMAH then just skip to the next section.

The following description was taken verbatim from the ELMAH website

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. It can be dynamically added to a running ASP.NET web application, or even all ASP.NET web applications on a machine, without any need for re-compilation or re-deployment.

Once ELMAH has been dropped into a running web application and configured appropriately, you get the following facilities without changing a single line of your code:

  • Logging of nearly all unhandled exceptions.
  • A web page to remotely view the entire log of recoded exceptions.
  • A web page to remotely view the full details of any one logged exception.
  • In many cases, you can review the original yellow screen of death that ASP.NET generated for a given exception, even with customErrors mode turned off.
  • An e-mail notification of each error at the time it occurs.
  • An RSS feed of the last 15 errors from the log.

So the next time an end user gets the YSOD (Yellow Screen of Death) ….

really_bad_error

You will be able to look at your ELMAH page or read an email to investigate the details! ELMAH FTW

elmah

ELMAH was created by Aziz Atif. It is great people like Aziz that make our day to day development tasks a breeze. I am sure all of us have written exception modules at one point or another. However, I am a firm believer of working smart and not hard. So why recreate the wheel when ELMAH does everything that you could possibly imagine in terms of exception handling.

Thanks Aziz! I am grateful for your contribution! 

Configuration Steps

These configuration steps assume that you are interesting in logging to a MSSQL database and sending emails when an exception occurs. Although SQL and Email are probably the most common configuration there are a wide variety of options available. ELMAH also supports logging to Oracle, Access, SQLLite, VistaDB and XML. You can even send notifications to twitter if the mood strikes you!

  1. Download the appropriate binaries from http://code.google.com/p/elmah/. There are two versions available. An x64 version and an x86 version. Pick the one that is appropriate to your environment.
  2. In your ASP.NET MVC Web application create a new folder called “Lib”. Copy the files Elmah.dll, Elmah.pdb and Elmah.xml into the directory.
  3. Add a reference to the ELMAH assembly . Right click on the project --> Add Reference –> c:\Your Project Path\Lib\Elmah.dll.
  4. Add the following XML to configuration/configSections. The XML below declares the new section groups that we will be creating in the following step. For an application running in Medium trust the requirePermission attribute should be set to false.
    <configSections>
          <sectionGroup name="elmah">
            <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
            <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
            <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
            <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
          </sectionGroup>
        </configSections>
  5. Directly below the configSections node add the following XML
    <elmah>
        <security allowRemoteAccess="0" />
        <errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="connectionString" />
        <errorMail
            from="noreply@somesite.com"
            to="<someUnfortunateDeveloper>@somesite.com"
            subject="<ApplicationName> Error"
            async="true "
            smtpPort="25"
            smtpServer="smtpserver.somesite.com"
            userName="smtpUser"
            password="smtpPassword" />
    </elmah>

    This section gives the configuration details for Email and SQL Logging. The connectionStringName is generally the SQL Connection string that you are using to access your application data. However, you could use a central “Error Logging” database if desired.  For most situations the connectionStringName will be equal to the name declared in the connectionStrings section of your web.config:

    <connectionStrings>    
        <add name="connectionString" connectionString="Data Source=<instance name>;Initial Catalog=<database name>;Integrated Security=True" providerName="System.Data.SqlClient"/>  
    </connectionStrings>
  6. Under the system.web element add the following XML. The configures the HTTP Handler so when the URL http://<siteURL>/elmah.axd is viewed ELMAH will display the errors. The path attribute should be set the same as you specified it in the previous step. For IIS7 you need to register the Elmah Modules and Handlers in both system.web and system.webServer.
    <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />      
    </httpHandlers>
    <httpModules>
        <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
        <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
    </httpModules>
  7. In order to avoid emails being sent while developing you can either specify the deliveryMethod in your system.net/mailSettings/smtp element to be “SpecifiedPickupDirectory” or you can add some code to your Global.asax.cs file to filter out emails based on some logic.
    Method 1: Specifying the deliveryMethod by adding some XML to the configuration node of your web.config:
    <system.net>
      <mailSettings>
        <smtp deliveryMethod="SpecifiedPickupDirectory" from="noReply@somesite.com">
          <specifiedPickupDirectory pickupDirectoryLocation="c:\temp\emails"/>
        </smtp>
      </mailSettings>
    </system.net>
    Method 2: Using custom logic in your Global.asax file.
    void ErrorMail_Filtering(object sender, ExceptionFilterEventArgs e) 
    {
        if (Request.IsLocal) e.Dismiss(); 
    }

    OR

    void ErrorMail_Filtering(object sender, ExceptionFilterEventArgs e)
    {                        
        SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder();            
        csb.ConnectionString = ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString;
        if( csb.DataSource.Equals( "development", StringComparison.CurrentCultureIgnoreCase ) 
          e.Dismiss();            
    }
  8. Create the required tables and stored procedures in the database by running the script SQLServer.sql which was included in the download.  You can delete the beginning part of the script that forces the database into 8.0 compatibility mode. Setting the compatibility mode to SQL 2000 is not really necessary.

    If you are using a service account which has limited rights, then make sure you grant execute to the following stored procedures so ELMAH can log the errors to the ELMAH_Error table:

    grant exec on ELMAH_GetErrorsXML to <SQLUserName> 
    grant exec on ELMAH_GetErrorXml to <SQLUserName> 
    grant exec on ELMAH_LogError to <SQLUserName> 
  9. Add the source file HandleErrorWithELMAHAttribute.cs to your project. This will be used to decorate your controller(s) so errors are sent through ELMAH. If you are using a “Base Controller” then you only need to add the [HandleErrorWithELMAH] attribute to the base controller. If you are not using a base controller, then you will want to add the attribute to each controller. Although there are other ways to send the errors to ELMAH this is the easiest method and is used in Nerd dinner.

    Warning:  Make sure that you remove the [HandleError] attribute from the other controllers if it is specified. If you use the HandleError attribute on the other controllers the errors will not be fed into the ELMAH pipeline and you will not get email/SQL logging support for any errors that occur on that controller.
  10. All modern browsers check for the existence of favicon.ico. If the file, does not exist a 404 error will be logged to ELMAH which is very annoying.  In order to circumvent this issue add a favicon.ico file to the root of your web application and add another route exception to your Global.asax.cs file.
    routes.IgnoreRoute("{*favicon}", new {favicon=@"(.*/)?favicon.ico(/.*)?"});

Additional Step for MVC 1.0 Applications

In MVC 1, there was no default filter for *.axd files.  Therefore, in order for the elmah.axd page to be displayed you need to ignore the route in the RegisterRoutes method of the Global.asax.cs file:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  

Testing Your Configuration

The best way to test ELMAH is to generate an exception. You can do this by entering a non-existent route, or enter some route parameters that will result in an error.  After the error is thrown you can navigate to http://<siteUrl>/elmah.axd to view the errors.  If you are using SQL Logging you can also query the ELMAH_LogError table.

Related Resources

The ELMAH Project Home Page

Scott Hanselman - ELMAH: Error Logging Modules and Handlers for ASP.NET (and MVC too!)

Coding Horror - Exception-Driven Development

New Job Equals New Toys

I recently renounced my role as Database Administrator and accepted a new job as Chief Web Developer. I am really excited about the opportunity because I get to write ASP.NET MVC apps on a full time basis. Not too many people get to do what they love for a living so I consider myself fortunate.

Samsung-MomentAs a result of switching careers and companies, I had to turn in my Blackberry Bold. Since the new job does not require after hours support I finally had the opportunity to buy the phone that I wanted instead of the phone that the company provided for me.

Since I was in the market for a new phone, I spent a fair amount of time researching the various plans and phones that were available. Initially I was thinking about picking up an iPhone but I discovered a few key factors which pushed me towards getting a Android instead:

1. If you want to develop apps for the iPhone you need a Mac OS. In my humble opinion this is bullshit.

2. The iPhone's operating system is completely closed. It is being developed by Apple and for Apple. The only smart phones that will ever run it are made by this one company. Since Apple has complete control over the hardware and software they can force users into situations where they have to buy completely new devices if they want the newest OS. In comparison, the Android OS is open source and is not tied to specific piece of hardware or a vendor.

3. If you want an iPhone you have to go with AT&T. Although I personally have no problems with AT&T, my wife already had a plan with Sprint so I was able to save some money by adding another line to an existing plan. In general, Apple is a control freak. You are forced to play by their rules which is a major turn off for me.

Once I made the decision to buy an Android phone from Sprint I immediately discovered that the HTC Evo was the way to go. Unfortunately, the $10 extra a month for 4g service was a deal breaker for me. Although the 4g service would be nice to have, I just could not justify the additional expense. Therefore I had to choose between the HTC Hero and the Samsung Moment. I decided on the Moment because it has a slightly faster processor and a fold out keyboard. 

Shortly after getting my new phone I realized that I was running an outdated OS. The new 2.1 version of the OS was released on 5/14/2010 so it was time to upgrade! After about ten minutes I was fully upgraded and ready to synchronize my contacts and calendar entries. Since the Android OS is a Google product, my Gmail contacts and calendar entries were automatically copied over. Now that all my data was on the new phone, my Blackberry was officially dead to me. It was nice knowing you ol' friend. :-) 

Killer Apps

Once I got the basic grasp of how my phone worked I started looking for new applications to download. I stumble around the Android Marketplace but I was overwhelmed by the number of applications that were available. After a few quick Google searches and a little bit of experimentation I came up with the following list

Barcode Scanner

Scan barcodes on CDs, books, and other products, then look up prices and reviews, or search for a word in a book and find where it occurs. You can also scan QR Codes containing URLs, contact info, calendar events, etc.

gbook_search21

Immediately after installing this app, I scanned every bar-coded item in sight. For example, I scanned a bottle of hand sanitizer on my desk. Then a list of shopping results was returned where I could purchase the product. The list can then be sorted by price so you can find the best bargain. Pretty cool!

ConnectBot

ConnectBot is a SSH client for the Android. Its basically a Putty clone for the Android OS. Since the Android supports Wifi you can easily admin a Linux box from your phone. The fold out keyboard on the Samsung moment is a must-have if you plan on using ConnectBot for real-world scenarios.

Advanced Task Killer

In order to improve the speed of loading apps, the Android OS keeps programs open. However when you have lots of programs running in the background you dramatically shorten the phones battery life of your phone. The Advanced Task Killer makes the process of closing down applications easy with a single click.

 

Killer Games

Doodle Jump

doodlejumpThe Doodle Jump Android App by Lima is the Android version of a very successful, addicting game from the iPhone App Store. The object of this game is to climb the platforms to get as high up as you can go. Doodle Jump does not have levels but does have multiplayer mode so you can play against a friend.

You move the character by tilting your phone left and right and shoot aliens by tapping your finger on the screen. The game is simple enough that my four year old figured out how to play it in less than a minute. I eventually had to distract him so I could get my phone back…LOL

Labyrinth

A new spin on a classic game...

 

Closing Remarks

In the past I have used my Blackberry as a modem to get internet access while traveling. Apparently, there is an application called PdaNet which can be used to tether an Android phone. I read that Sprint is not fond of people using their phones as tethered modems. Mainly because it is a bandwidth killer. However, I would imagine that it also has a drastic impact on their air card sales. In any case, I plan to experiment with this functionality because I really like having options when I travel.

Also, I would like to find an RDP client for the Android. Since the Android has VPN support I could potentially use the Android to remotely access servers if I am in a pinch. I am not sure how practical this is, but it would be cool to try it out.

Overall, I would highly recommend the Samsung Moment phone if you are shopping for an Android capable device. If money is no object to you then buy the HTC Evo from Sprint. The HTC Evo is the best Android phone on the market followed by the HTC Incredible from Verizon in a close second. In any case, the Android OS is remarkable and I am truly impressed with how much I can actually do with my phone. Once you go Android, you never go back!

How to Build a Custom View Engine with Theme Support

All good blogging platforms have theme support. So while working on WeBlog I initially implemented theme support by using a base controller class. The base controller class was responsible for dynamically setting the master page at runtime. I did this by assigning the action’s MasterName property in the OnActionExecuted event. Here is a short snippet of code which outlines the process.

public class BaseController
{
     protected override void OnActionExecuted(ActionExecutedContext filterContext) {
         var action = filterContext.Result as ViewResult;
         if (action != null) {
             action.MasterName = MyApp.Properties.Settings.Default.Theme;
         }  

         base.OnActionExecuted(filterContext);
     }
}

Although the BaseController concept worked, I never liked that fact that all my other controllers had to inherit from it. As a matter of fact, when I added the BaseController class to my project I made myself an action item to research Custom View Engines as an alternative approach. In case you don’t know, the developers of ASP.NET MVC went to great lengths to make their framework completely flexible. By default, when you create a new MVC project you are using the Web Forms view engine. However, you can rip out the default view engine and register your own. As a matter of fact, there are already a variety of view engines available to us:

 

Since the view engines listed above are open source it was easy to find code to tailor my custom view engine after. In addition, I also found a great article titled Creating Your First MVC ViewEngine by Nick Berardi. In any case, the first step in creating a view engine is defining your search locations. If you have been working with MVC for any length of time, then you know that MVC uses a series of search paths when finding a view. So if you have a view named Index in your Post controller, MVC will look first in the Views\Post folder for the Index.aspx file. If it is not found there, then the Web Forms view engine will look in the Views\Shared folder for the view. Since we are trying to implement themes we have a few more locations that we want MVC to search in. These would be \Themes\{SomeTheme}\Views\Post and \Themes\{SomeTheme}\Shared. In order to add these new search locations we need to set the MasterLocationFormats, ViewLocationFormats and PartialLocationFormats in the constructor of our custom view engine:

public WeBlogViewEngine() : base()
{

    base.MasterLocationFormats = new string[] {
        "~/Themes/{2}/{0}.master", 
        "~/Themes/{2}/Views/Shared/{0}.master"
    };

    base.ViewLocationFormats = new string[] { 
        "~/Themes/{2}/Views/{1}/{0}.aspx",                 
        "~/Themes/{2}/Views/Shared/{0}.aspx",
        "~/Themes/{2}/Views/{1}/{0}.ascx",                
        "~/Themes/{2}/Views/Shared/{0}.ascx",
        "~/Views/{1}/{0}.aspx",
        "~/Views/Shared/{0}.aspx",     
        "~/Views/{1}/{0}.ascx",
        "~/Views/Shared/{0}.ascx"
    };

    base.PartialViewLocationFormats = base.ViewLocationFormats;
}

When looking at the code above you may initially be confused about the {2},{1} and {0}'s in the location strings. To clarify, {0} is the view name or master page name and the {1} is the controller name. However, the interesting one in our case is the "{2}" which represents a theme name. This will probably become clearer when you look at the image below which shows the directory structure used for the WeBlog project:

weblog_structure

There are currently two themes included with WeBlog which are the Zenlike and Default theme. So the “{2}” in the location formats need to be replaced with one of these theme names at runtime. This work is initiated in the FindView method. The FindView method sets the masterName value based on the applications settings. Once the theme name is set, the value is passed to the GetPath method which eventually calls the GetPathfromGeneralName method. In the GetPathFromGeneralName method we take the format string with the “{2}” and replace it with the theme name.

private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKey, ref string[] searchedLocations)
{
    string result = String.Empty;
    searchedLocations = new string[locations.Length];

    for (int i = 0; i < locations.Length; i++)
    {
        string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, controllerName, GetThemeName(controllerContext));

        if (FileExists(controllerContext, virtualPath))
        {
            searchedLocations = _emptyLocations;
            result = virtualPath;
            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
            break;
        }

        searchedLocations[i] = virtualPath;
    }

    return result;
}

So up to this point you probably think that our custom view engine is only giving us the ability to swap out our master page, right? Well, in reality we are actually doing more than this. By using a custom view engine we can also overwrite specific views in our Themes too. This works because we have multiple search locations registered in our view engine which are based on order. For example, in WeBlog I made display and editor templates for Posts which display information like the title, content, rating, tags and categories. The default templates for these items are stored in the \Shared\EditorTemplates and \Shared\DisplayTemplates folders. However, by registering the shared locations for the themes ahead of the other locations we can allow theme developers to override the base templates. Since MVC will quit looking when it finds a matching view, any view we place in our theme folder will be given precedence over the “default” views.

 

Registering the Custom View Engine

 

Registering the view engine is simple. We basically just clear out the default WebForms view engine and insert an instance of our custom view engine. This is done in the Application_Start method of the Global.asax file:

protected void Application_Start()
{
    RegisterViewEngines(ViewEngines.Engines);
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

private static void RegisterViewEngines(ViewEngineCollection viewEngines)
{
    viewEngines.Clear();
    viewEngines.Add(new WeBlogViewEngine());
}

 

Conclusion

Building a custom view engine is a great way to add theme support to an application. It allows you to have full control over the “search paths” used by MVC when it is trying to find a partial, master page or view. If you want to see an working example then just download the WeBlog source code and try it out for yourself!