Sunday, January 10, 2010

Localizing LabelFor in ASP.NET MVC 2

I started the weekend spelunking ModelBinder with Reflector and started toying with the front-end html helpers. The ASP.NET MVC 2 builds on the MVCContrib FluentHtml implementation and includes some fantastic features for eliminating magic strings in your html. If you are not familiar with the implementation, this piece of code summarizes the Html.LabelFor implementation. For a broader view of these modifications, check out Scott Gu's blog series on the broader functionality.

   1: <div class="editor-label">
   2:    <%=Html.LabelFor(m => m.LastName)%
   3: </div>

If you refactor your model implementation, the front-end html gets refactored along with it. The default implementation relies on marking up the model using DisplayNameAttribute to resolve a user-readable name.


   1: [Required]
   2: [DisplayName("Last Name")]
   3: [StringLength(64)]
   4: public string LastName { get; set; }

The problem with Attributes is that there is no reasonable way I’ve seen to create a DisplayName attribute style variant that is resource-aware. This is primarily due to constraints on Attributes in the form of generic and runtime type constraints. In short, there is no way to create a flexible approach with (1) no magic strings involved and (2) a format convention that can vary across different MVC Areas.

After looking over the first approach, I looked at creating a new LabelFor extension method. I had two driving criteria in creating the solution - (1) avoid any magic strings in the views and (2) allow for easy and flexible variation of naming rules associated with resource classes.

I like the result as the core workings are very testable and the flexibility is straight-forward.

The first part of the solution includes the LabelFor extension. You’ll notice it mirrors the LabelFor(Expression<…>) implementation by adding the ResourcePropertyResolver<T> implementation. In order to grab the associated metadata, I am relying on the same ModelMetadata calls that the existing LabelFor implementation uses. It then relies on the existing Label(string) implementation to generate the output.


   1: /// <summary>
   2: /// Our own set of custom label extensions
   3: /// </summary>
   4: public static class CustomLabelExtensions
   5: {
   6:     /// <summary>
   7:     /// Returns an HMTL label element with the content resolved for the given propety according to
   8:     /// the format specified in the parameters
   9:     /// </summary>
  10:     /// <typeparam name="TModel">The model typically inferred from the prage</typeparam>
  11:     /// <typeparam name="TValue">Value type inferred from expression</typeparam>
  12:     /// <typeparam name="TResourceType">Resource type used as the string resolution target</typeparam>
  13:     /// <param name="html">extension method parameter</param>
  14:     /// <param name="resourcePropertyResolver">resource resolver that indicates the resolution target 
  15:     /// and format strings</param>
  16:     /// <param name="expression">expression tree intended to point at the property being named</param>
  17:     /// <returns>localized, resolved label for the property</returns>
  18:     public static MvcHtmlString LabelFor<TModel, TValue, TResourceType>(this HtmlHelper<TModel> html, ResourcePropertyResolver<TResourceType> resourcePropertyResolver, Expression<Func<TModel, TValue>> expression)
  19:     {
  20:         var metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
  21:  
  22:         var resourcePropertyName = string.Format(CultureInfo.InvariantCulture, resourcePropertyResolver.ResourcePropertyFormatPolicy,
  23:                                                  metaData.ContainerType.Name, metaData.PropertyName);
  24:  
  25:         return html.Label(resourcePropertyResolver.GetResourceValue(resourcePropertyName));
  26:     }
  27: }

The next part is a default implementation of ResourcePropertyResolver. Starting at the top, this is a generic class requires the type of your resource class. A resource class is effectively any class that has static properties – the code-gen component of Visual Studio takes the resource table and implements it as a plain old class. Remember that the code generator property defaults to internal types – called out on the Custom Tool property. You can make these all public types by changing the Custom Tool property to PublicResXFileCodeGenerator.

First off, a static readonly type is resolved once for the class to avoid multipel typeof(…) resolutions – an efficiency move here. Next, a couple of string formatting properties. The first calls out the naming policy for properties on the resource class. The default value is ‘[class name]_[property name]’. The second format calls out the value that should be returned when a matching value is not found. Note that both of these are overridable which I will demonstrate in a moment.

Finally, the GetResourceValue method does the heavy lifting of locating the property in the table and formatting output accordingly.


   1: /// <summary>
   2: /// Implements access of the actual resource class and the associated
   3: /// formatting rules for property names and unresolved resource values
   4: /// </summary>
   5: /// <typeparam name="TResourceType">resource class</typeparam>
   6: public class ResourcePropertyResolver<TResourceType>
   7: {
   8:     private static readonly Type _resourceType = typeof(TResourceType);
   9:  
  10:     public virtual string ResourcePropertyFormatPolicy
  11:     { get { return "{0}_{1}"; } }
  12:  
  13:     public virtual string UnresolvedValueFormatString
  14:     { get { return "{0}.{1}"; } }
  15:  
  16:     internal string GetResourceValue(string resourcePropertyName)
  17:     {
  18:         // both public and non-public are members are being searched because resource properties
  19:         // will either be generated as internal or public. Optimize this by making it specific to
  20:         // your case.
  21:         var propertyInfo =
  22:             _resourceType.GetProperty(resourcePropertyName,
  23:                                        BindingFlags.Static  BindingFlags.Public  BindingFlags.NonPublic);
  24:  
  25:         if (propertyInfo == null)
  26:             return String.Format(CultureInfo.InvariantCulture, UnresolvedValueFormatString, _resourceType.Name, resourcePropertyName);
  27:  
  28:         return (string)propertyInfo.GetValue(null, null);
  29:     }
  30: }

Next, let’s create a custom rule that outputs a patterned, more identifiable output with a little helper that simplifies the View markup. In this case, we simply override the rules we want to and then create a single live, static instance that is quickly referencable.


   1: public class WebStringResolver : ResourcePropertyResolver<WebStringTable>
   2: {
   3:     public override string UnresolvedValueFormatString
   4:     {
   5:         get
   6:         {
   7:             return "*** {0}.{1} ***";
   8:         }
   9:     }
  10:  
  11:     private static WebStringResolver _instance = new WebStringResolver();
  12:     public static WebStringResolver Instance 
  13:     {
  14:         get
  15:         {
  16:             return _instance;
  17:         }
  18:     }
  19: }

So finally, this leads us to the modified markup that is quite explicit to which resource table it is tied and no magic strings in place.


   1: <div class="editor-label">
   2:     <%=Html.LabelFor(WebStringResolver.Instance, m => m.LastName)%>
   3: </div>

So there you have it- my diversion from reflector-spellunking into the ModelBinder.

No comments:

Post a Comment

There was an error in this gadget