web 2.0

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!

How to Localize an ASP.NET MVC Application

While working on WeBlog this week I decided that I needed to start thinking about localization. If you have never heard the term “localization” before then its just a fancy way of saying that I want my application to be multi-lingual. In order to localize an application in .NET, you generally need to create a separate resource files for each language you want to support. In an ASP.NET MVC application, the resource files should be placed in a folder called App_GlobalResources. The folder can be created by right clicking on your project and selecting

Add –> Add ASP.NET Folder –> App_GlobalResources.

The resource files follow a naming convention. The first part of the name is the user defined part, for WeBlog we called it “Strings” but it could be whatever you want. The second part of the string is the Culture. For English the culture is “en”, for French the culture is “fr”, and so on and so forth. Here are few examples:

Prefix Language Culture Filename
Strings General Spanish es Strings.es.resx
Labels French fr Labels.fr.resx
Text French Canadian fr-ca Text.fr-ca.resx
Strings Spanish (Mexico) es-mex Strings.es-mex.resx

 
In Weblog the default language is English. However, from looking at the screenshot below you will notice that there is no file named Strings.en.resx. That is because when ASP.NET does not have a resource file matching the culture it will use a default file. The default file is the one with no culture specified (Strings.resx). 

image 

Once your resource file has been added to the project you will want to make sure that the properties are correct. First of all, make sure you set the “Access Modifier” to “Public”. This allows the resources to accessible from other assemblies. Internaly, it changes the “Custom Tool” used to generate the code behind from  ResXFileCodeGenerator to “PublicResXFileCodeGenerator”. Also confirm that the “Build Action” is set to “Embedded Resource”.

When you edit the resource file you will see three columns which are Name, Value and comment. The Name field is essentially the variable name and you will use it as a reference in your code. The files in your project should have a 1 to 1 match. Here is a side by side comparison of my Spanish and English resource file.

image 

If you speak Spanish then please don’t laugh at my translations. I have been using Google Translate and I have been told that the results are not very accurate. Luckily, there is a developer on the WeBlog team who does speak Spanish and has been regularly correcting the resource file. Thanks Marc :-)   

Automatic or Manual Mode?

If you want localization to happen automatically based on the user’s culture then you should add the following setting to your web.config file under the system.web section.

<globalization culture="auto" uiCulture="auto" />

 
Alternatively, you could set the language based on a parameter in the URL or by allowing the user to specify the language in a configuration screen. In order for this to work you have to manually set the language for each request. There is a good example of how to do this on codeproject.com.

For WeBlog I opted for the automatic language detection. Once the web.config file was modified it was time to move on to the next step…

Decorating Your Models

In MVC the process of localizing your forms is easy if you use data annotations. In particular you will want to use the Required and LocalizedDisplayName attributes as shown below. I initially tried using the standard DisplayName attribute but apparently it does not support localization. Luckily, Alex Adamyan took on the task of creating the LocalizedDisplayName attribute which fixes the problem. Thanks Alex!

public class CommentModel {
    public Guid? ID { get; set; }
    public Guid? PostID { get; set; }
    public Guid? UserID { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Strings), ErrorMessageResourceName = "Required")]
    [RegularExpression(@"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$", ErrorMessage = "Not a valid email")]
    [LocalizedDisplayName("Email", NameResourceType = typeof(Resources.Strings))]
    public string Email { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Strings), ErrorMessageResourceName = "Required")]
    [LocalizedDisplayName("FullName", NameResourceType= typeof(Resources.Strings))]
    public string Author { get; set; }

    public string IPAddress { get; set; }

    [DataType(DataType.Url)]
    [LocalizedDisplayName("Website", NameResourceType = typeof(Resources.Strings))]
    public string Website { get; set; }

    [Required(ErrorMessageResourceType = typeof(Resources.Strings), ErrorMessageResourceName = "Required")]
    [LocalizedDisplayName("Comments", NameResourceType = typeof(Resources.Strings))]
    public string Content { get; set; }

    public DateTime DateTime { get; set; }
    public CommentStatus Status { get; set; }
}


Once you have decorated your model with attributes you can start working on the view page. To make life easy I used the LabelFor and ValidationMessageFor methods throughout my view. By using these methods, MVC will lookup the values for the validation messages and labels from the Model. Based on the users culture, the ASP.NET runtime will then locate these values in the corresponding resource file and return them to the view:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WeBlog.Models.CommentModel>" %>

    <% using (Html.BeginForm() ) {%>
        <%: Html.ValidationSummary(true) %>
        
        <fieldset>    
            
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Author) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Author) %>
                <%: Html.ValidationMessageFor(model => model.Author) %>
            </div>

            <div class="editor-label">
                <%: Html.LabelFor(model => model.Email) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Email, new { size = 40 })%>
                <%: Html.ValidationMessageFor(model => model.Email) %>
            </div>
                                  
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Website) %>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Website, new { size = 40 })%>
                <%: Html.ValidationMessageFor(model => model.Website) %>
            </div>
            
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Content) %>
            </div>
            <div class="editor-field">
                <%: Html.TextAreaFor(model => model.Content, new { cols = 50, rows = 8 } )%>
                <%: Html.ValidationMessageFor(model => model.Content) %>
            </div>
                        
            <p>
                <input type="submit" value=''<%: Resources.Strings.Save %> />
            </p>
        </fieldset>

    <% } %>

 

Testing Localization Using Google Chrome

Once you have everything ready, its time to test. With Google Chrome it is easy to switch between languages. Simply click on the wrench icon, choose options, minor tweaks, Change fonts and language settings. When the Fonts and Languages dialog appears you can add new languages or set the order of preference. In order to view a website in Spanish, simply highlight the Spanish language line item and move it to the top of the list.

image

If you did everything correctly you should now be able to render your form in Spanish and English. Here are some screenshots from Weblog:

image 

image

Related Resources

Display and Editor Templates in ASP.NET MVC 2

In the first version of ASP.NET MVC, I found myself creating a lot of HTML Helpers and Partial Views in order to promote code re-use and to standardize formatting of certain data types across views. For example, let's say that I want every date in my application to use the formatting of “MM/dd/yyyy”. In MVC 1.0, I would have made the following HTML Helper:

public static string FormatDate(this HtmlHelper helper, DateTime value ) {
    return value.ToString( "MM/dd/yyyy" );
}

Once the HTML Helper was ready, I would add the markup to my views:

<%= Html.FormatDate( Model.Date ) %>

Although this code is reusable it can be hard to maintain when you have a large application. It is especially painful to implement when you already have dozens of views and you need to replace the old "HTML.Encode( Model.Date )" code with the new helper method. Fortunately, there is a better way! MVC 2.0 introduced the concept of Display and Editor Templates.

First of all, If you want to take advantage of Display and Editor Templates you must use the DisplayFor and EditorFor methods. Unfortunately, these methods are not used by default when you create a new View using the MVC 2.0 tooling. However, if we are aware of the magic that Display and Editor Templates can provide you would probably format all of your views to use these methods. Here is an example:

<%= Html.DisplayFor( x => Model.Date ) %>

When the code above executes we will get the default serialization for a DateTime value. If we are happy with the results, then no further work is required. However, since we are trying to standardize on our “MM/dd/yyyy” formatting we will have to perform a few additional steps. First of all, we need to create a special folder in our MVC project called DisplayTemplates under the Views/Shared directory:

displaytemplate_dir

Inside that folder we will create a new view named DateTime. We will check the “partial view” and the “strongly-typed view” options…

 addview

Finally, we will modify the contents of the "DateTime.ascx” partial view to contain the following:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DateTime>" %>
<%: Model.ToString( "MM/dd/yyyy" ) %>

When this code runs and MVC encounters a DateTime field being outputted with the DisplayFor method we will get our "MM/dd/yyyy" formatting auto-magically!

So now you may be thinking to yourself….”How does this work?” Well, at runtime when MVC finds the line of code using the "Html.DisplayFor( x => Model.Date )" syntax it will search the Shared/Views/DisplayTemplates folder for any partial view that corresponds to the DateTime date type. In our case, it will find a file named "DateTime.ascx". It will then pass the Model.Date value to the view and format it however we specified it!

So why is this useful? Imagine the scenario where you have a very large MVC app with hundreds of views. The boss comes to your office on a Friday afternoon, twenty minutes before your shift ends and asks if you can change the default formatting of all the DateTime fields. If you were using the DisplayFor syntax, you could simply follow the procedure above and still make it to happy hour in plenty of time. If you are using a HTML Helper, you would probably get stuck in rush hour traffic and all of your friends would be drunk by the time you ordered your first beer!

Finally, keep in mind that MVC 2 also has the EditorFor methods. They work exactly the same way as DisplayTemplates except the views must reside in the Shared/Views/EditorTemplates directory instead. Editor templates are really useful for replacing your stock DateTime edit controls with a jQuery UI DateTime picker. This is a task which many of us have lived through countless times before.

Also remember that templating can be used with complex data types as well. In WeBlog, we are using display and editor templates for blog posts and various other object types. It has really helped to centralize code and increase maintainability.

Tags: , ,