eWorld.UI - Matt Hawley

Ramblings of Matt

ASP.NET MVC: Simplified Localization via ViewEngines

October 22, 2008 16:56 by matthaw

Thanks to Brad, he identified a few areas that made things MUCH simpler from my prior implementation. Notably, while you still have your ViewEngine specify "here's your view's path" there's no longer a need for having a new helper, derived classes, etc. This implementation goes back to the simplified HtmlHelper extension in which you can use in any ViewEngine regardless. It also allows you to "replace" the implementation by just changing the namespace, just as you can do with Html and Ajax.


Simplified View Engine

As previously stated, you still need a new view engine and view, because this is the only point in which you can truly grab which view is being rendered. What's nice now, is that it has been simplified down to using the base implementation and only setting a ViewData field in which the Resource extension method picks up and uses.

   1:  public class LocalizationWebFormViewEngine : WebFormViewEngine
   2:  {
   3:      protected override IView CreateView(ControllerContext controllerContext, 
   4:                                                  string viewPath, string masterPath)
   5:      {
   6:          return new LocalizationWebFormView(viewPath, masterPath);
   7:      }
   9:      protected override IView CreatePartialView(ControllerContext controllerContext, 
  10:                                                                            string partialPath)
  11:      {
  12:          return new LocalizationWebFormView(partialPath, null);
  13:      }
  14:  }
  16:  public class LocalizationWebFormView : WebFormView
  17:  {
  18:      internal const string ViewPathKey = "__ViewPath__";
  20:      public LocalizationWebFormView(string viewPath) : base(viewPath)
  21:      {
  22:      }
  24:      public LocalizationWebFormView(string viewPath, string masterPath) 
  25:                : base(viewPath, masterPath)
  26:      {
  27:      }
  29:      public override void Render(ViewContext viewContext, TextWriter writer)
  30:      {
  31:          // there seems to be a bug with RenderPartial tainting the page's view data
  32:          // so we should capture the current view path, and revert back after rendering
  33:          string originalViewPath = (string) viewContext.ViewData[ViewPathKey];
  35:          viewContext.ViewData[ViewPathKey] = ViewPath;
  36:          base.Render(viewContext, writer);
  38:          viewContext.ViewData[ViewPathKey] = originalViewPath;
  39:      }
  40:  }

As you can see, it's much simpler now. Also, it seems as if there's a bug with the current Beta bits when you have a RenderPartial on your page, there's no isolation and the view's path is not correct later. Simple fix by simply grabbing the original view path, setting it to the new path, rendering, and then reverting back.


Getting back to Extension Methods

And now that we have our path again in our ViewData, we can revert back to using extension methods to extract the path when doing both Global and Local resources. As you can see below, should you be using any other ViewEngine it will always work for Global resources.

   1:  public static class ResourceExtensions
   2:  {
   3:      public static string Resource(this Controller controller, string expression, 
   4:                                                    params object[] args)
   5:      {
   6:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
   7:          return GetGlobalResource(fields, args);
   8:      }
  10:      public static string Resource(this HtmlHelper htmlHelper, 
  11:                                                    string expression, params object[] args)
  12:      {
  13:          string path = (string)htmlHelper.ViewData[LocalizationWebFormView.ViewPathKey];
  14:          if (string.IsNullOrEmpty(path))
  15:              path = "~/";
  17:          ResourceExpressionFields fields = GetResourceFields(expression, path);
  18:          if (!string.IsNullOrEmpty(fields.ClassKey))
  19:              return GetGlobalResource(fields, args);
  21:          return GetLocalResource(path, fields, args);
  22:      }
  24:      static string GetLocalResource(string path, ResourceExpressionFields fields, 
  25:                                                       object[] args)
  26:      {
  27:          return string.Format((string)HttpContext.GetLocalResourceObject(path, 
  28:                                 fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  29:      }
  31:      static string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  32:      {
  33:          return string.Format((string)HttpContext.GetGlobalResourceObject(
  34:               fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
  35:      }
  37:      static ResourceExpressionFields GetResourceFields(string expression, 
  38:                                                                string virtualPath)
  39:      {
  40:          var context = new ExpressionBuilderContext(virtualPath);
  41:          var builder = new ResourceExpressionBuilder();
  42:          return (ResourceExpressionFields)builder.ParseExpression(
  43:                                            expression, typeof(string), context);
  44:      }
  45:  }

Again, thanks to Brad, this implementation seems "less dirty". You can download the source for this here.


kick it on DotNetKicks.com

ASP.NET MVC: Localization Delegated via View Engines

October 22, 2008 14:02 by matthaw

Update: Please see this newer post. It simplifies this solution drastically, and I don't recommend this version anymore!



I've previously blogged about how to add localization support within ASP.NET MVC before, but that codebase was based on Preview 3, and now that the beta is out there, the code just doesn't work anymore. Period. The concepts I had originally had put out there really don't fly well in the Beta world, or, within the "real" world in which people are using more than just the WebFormViewEngine for their rendering. So, why is there this big disconnect now? Well. it's not so much that global resources fail to work, it's more local resources don't really apply "generally" anymore.


While looking into a solution, and bouncing ideas off of Phil & Brad, I came to the realization that, just like in the Matrix, "There is no spoon" or rather, replace "spoon" with "view location" and you have your reasoning. Now, before you start ranting, let me explain. Yes, there are physical locations at which your views are located, but what if you're ViewEngine was pulling the view's from a database? No guarantee that a "general" solution could achieve local resource support based on a HtmlHelper extensino. However, if you left the local resource delegation up to a ViewEngine, you can now have a concept of a location in relevance to it. (And yes, the point may be moot if you use a different Resource Provider than the asp.net built-in one)


So where does that leave us? Well, we need a general way to access both global and local resources within a View, and global resources within a Controller. Ultimately, the code used to get the resources hasn't changed from my original implementation, however, because we no longer have that concept of a view location, the new implementation delegates this decision correctly to the view engine. Also, before I show the proof of concept, I want to warn you that the guts for my view rendering logic is nearly a copy from what is in the WebFormView source.


So here's the overall design is as follows:

  1. Any view engine / views can out-of-the-box use "global resources"
  2. A view engine can decide if it's views can participate in "local resources" by supplying it's own implementation.
  3. A controller can request "global resources" for messages it's setting within ViewData.

Now, because I wanted to provide a default implementation for the WebFormViewEngine, the implementation isn't nearly as clean as one would like, but it does work great, and falls back to using non-derived ViewPage's and ViewUserControl's when necessary.


View Implementation

We start by deriving ViewPage and ViewUserControl to support localization. These new implementations will add a ResourceHelper, just like Ajax / Html / Url, so that within your view, you can directly access a resource.

   1:  public class LocalizedViewPage : ViewPage, ILocalizedView
   2:  {
   3:      public ResourceHelper Resource { get; set; }
   4:  }
   6:  public class LocalizedViewPage<TModel> : ViewPage<TModel>, ILocalizedView
   7:      where TModel : class
   8:  {
   9:      public ResourceHelper Resource { get; set; }
  10:  }
  12:  public class LocalizedViewUserControl : ViewUserControl, ILocalizedView
  13:  {
  14:      public ResourceHelper Resource { get; set; }
  15:  }
  17:  public class LocalizedViewUserControl<TModel> : ViewUserControl<TModel>, ILocalizedView
  18:      where TModel : class
  19:  {
  20:      public ResourceHelper Resource { get; set; }
  21:  }

The interface ILocalizedView is simply an abstraction I put out so I could determine what type of View I was working with in the View Engine.


Resource Helper Implementation

The resource helper implementation is very straight forward. As previously stated, ResourceHelper is the base that adds support for global resources. The WebFormResourceHelper is what the new web form view engine implementation uses to add support for both global and local resources. As you'll see, WebFormResourceHelper is initialized with the virtual path of the currently rendering view so that it can correctly find the local resource.

   1:  public static class ResourceExtensions
   2:  {
   3:      public static string Resource(this Controller controller, 
   4:                                         string expression, params object[] args)
   5:      {
   6:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
   7:          return GetGlobalResource(fields, args);
   8:      }
  10:      internal static string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  11:      {
  12:          return string.Format((string)HttpContext.GetGlobalResourceObject(
  13:                                                      fields.ClassKey, fields.ResourceKey, 
  14:                                                      CultureInfo.CurrentUICulture), args);
  15:      }
  18:      internal static ResourceExpressionFields GetResourceFields(
  19:                                    string expression, string virtualPath)
  20:      {
  21:          var context = new ExpressionBuilderContext(virtualPath);
  22:          var builder = new ResourceExpressionBuilder();
  23:          return (ResourceExpressionFields)builder.ParseExpression(
  24:                                    expression, typeof(string), context);
  25:      }
  26:  }
  28:  public class ResourceHelper
  29:  {
  30:      public virtual string GetString(string expression, params object[] args)
  31:      {
  32:          ResourceExpressionFields fields = GetResourceFields(expression, "~/");
  33:          if (string.IsNullOrEmpty(fields.ClassKey))
  34:              throw new InvalidOperationException(
  35:                       "The resource helper does not support local resources.");
  37:          return GetGlobalResource(fields, args);
  38:      }
  40:      protected string GetGlobalResource(ResourceExpressionFields fields, object[] args)
  41:      {
  42:          return ResourceExtensions.GetGlobalResource(fields, args);
  43:      }
  45:      protected ResourceExpressionFields GetResourceFields(string expression, string virtualPath)
  46:      {
  47:          return ResourceExtensions.GetResourceFields(expression, virtualPath);
  48:      }
  49:  }
  51:  public class WebFormResourceHelper : ResourceHelper
  52:  {
  53:      public WebFormResourceHelper(string virtualPath)
  54:      {
  55:          VirtualPath = virtualPath;
  56:      }
  58:      public string VirtualPath { get; private set; }
  60:      public override string GetString(string expression, params object[] args)
  61:      {
  62:          ResourceExpressionFields fields = GetResourceFields(expression, VirtualPath);
  63:          if (!string.IsNullOrEmpty(fields.ClassKey))
  64:              return GetGlobalResource(fields, args);
  66:          return string.Format((string) HttpContext.GetLocalResourceObject(
  67:                                                             VirtualPath, fields.ResourceKey, 
  68:                                                             CultureInfo.CurrentUICulture), args);
  69:      }
  70:  }

You'll also notice that I've included a controller extension method for extracting global resources. This hasn't changed from the original implementation, but it's straight forward to use.


View Engine Implementation

This is where the rubber meets the road. Since we're now delegating that a ViewEngine can choose to support local resources, the derived view engine implementation is where it supplies it's own ResourceHelper, if it so chooses. In my example, I derive from the WebFormViewEngine and WebFormView, and provide this implementation to supply the WebFormResourceHelper to my localized views. For brevity, and the fact that I didn't write the code for rendering, I'll leave it up to you to see the full implementation for the LocalizedWebFormView in the download. However, the critical portion that you'll see is the following:

   1:  var localizedPage = page as ILocalizedView;
   2:  if (localizedPage != null)
   3:      localizedPage.Resource = new WebFormResourceHelper(ViewPath);
   4:  page.RenderView(context);

And that's it. In your view, you can now derive from LocalizedViewPage and get access to your global and local resources by doing

   1:  // global resource
   2:  <%= Resource.GetString("MvcExample, Welcome") %>
   4:  // local resource
   5:  <%= Resource.GetString("Welcome") %>


How does this fit with other View Engines?

View engines need to simply add a reference to the ResourceHelper so that their view's can gain access to it. Obviously, they'll also need to change their ViewEngine to set an appropriate ResourceHelper when instantiating the view, but those view engines can explicitly determine how they would like to treat resources (even completely ignoring Global & Local resources).


And that's it, our View Engine now is completely in control of where my resources come from. They can use the baked-in ASP.NET resource provider, their own logic, or whatever! Again, this implementation is no where near baked, and I strongly urge you to look at your solution to see how this would fit in, and (obviously) make the code "production worthy". I'm hoping that some of these concepts make it into future MVC releases. You can also download the source for these files here. Enjoy!


kick it on DotNetKicks.com

Categories: .NET | ASP.NET | MVC | Programming
Actions: E-mail | Permalink | Comments (4) | Comment RSSRSS comment feed

Copyright © 2000 - 2014 , Excentrics World