I have recently been working on a site where localized values are pulled from a resource dictionary, with access to the localized values abstracted away behind an IResourceProvider
interface like the below.
public interface IResourceProvider
{
string Localize(string resourceKey);
}
This is all well and good within the controller code, where I can use IoC to get an instance of this interface, but what about from within the views themselves? That’s where most of the labels, buttons, titles and so forth are actually found, after all…
The ViewData
The first approach I took was to insert an instance of the IResourceProvider
into the ViewData for each request:
public ActionResult Index()
{
ViewData[ResourceProvider.ViewDataKey] = GetResourceProviderFromIoC();
return View();
}
This then meant that I could use the resource provider in my view as below:
@{ var resources = this.ViewData[ResourceProvider.ViewDataKey] as
IResourceProvider; }
<h1>Hello, @resources.Localize("World")</h1>
This works well enough, but that block of code at the top will need to go into every view, and the assignment to the view data dictionary will need to go into every action.
All that repetition is a maintenance nightmare, and if it doesn’t make you feel a little bit dirty as a developer then there’s something wrong with you.
All round: not ideal. Moving on…
HtmlHelper Extension
Another approach that I had seen for similar tasks all over the web was to write an extension method for the HtmlHelper
class - the one that generates all those oh-so-useful clumps of boilerplate HTML.
The extension method needs to source a IResourceProvider
from somewhere and then return the localized value:
public static string LocalizeResource(this HtmlHelper helper, string resourceKey)
{
var provider = GetResourceProviderSomehow();
return provider.Localize(resourceKey);
}
Resources in a view can then be localised with:
<h1>Hello, @Html.LocalizeResource("World")</h1>
This is neater in the view and doesn’t require any changes to controllers, but has a number of problems:
- It has nothing to do with HTML, but you have to call
@Html...
which doesn’t make sense - It is difficult to acquire the
IResourceProvider
through IoC from within the extension method - You still have to include an extra
@using
at the top of the view
Again: not ideal. Moving on…
Extending the Base Class
With MVC it is possible to replace the default base class for the generated view classes with a custom implementation; just inherit from WebViewPage
:
public abstract class LocalizedWebViewPage<T> : WebViewPage<T>
{
public IResourceProvider Resources { get; set; }
}
public abstract class LocalizedWebViewPage : LocalizedWebViewPage<object> { }
Note that we need both the generic and non-generic version; this is to support pages both with and without a strongly-typed Model
The new base class exposes a Resources
property to all generated view classes, but it needs to be hooked up in the web.config file first. It only needs one attribute changed on the pages
node, as shown below.
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="MyApplication.LocalizedWebViewPage"> <!-- replace the pageBaseType type name -->
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
Note: this is in the web.config file under the Views folder, not the one in the root of the project.
Now I can access the Resources
property directly within the razor HTML with no need for extra boilerplate in my views:
<h1>Hello, @Resources.Localize("World")</h1>
Populating the Resources
Those of you paying attention will have noticed that I never actually set the value of the Resources
property, so I only have half a solution so far.
Ideally I want my solution to be able to pull the IResourceProvider
from an IoC container, and to require no additional code in either the controller or the view.
The way I achieved this was to use the ViewData approach from the start of this post to pass the value to the view, and then update the base class to expose it:
public abstract class LocalizedWebViewPage<T> : WebViewPage<T>
{
public IResourceProvider Resources
{
get { return ViewData[ResourceProvider.ViewDataKey] as IResourceProvider; }
}
}
This gets the resource provider out of the view data, but what about getting it in? To avoid including more boilerplate code in every action or controller that needs localization I used a custom action filter to automatically insert the provider from the IoC:
public sealed class LocalizedAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
viewResult.ViewData.Add(ResourceProvider.ViewDataKey, GetProviderFromIoC());
base.OnResultExecuting(filterContext);
}
}
This attribute can now be applied to controllers, individual actions, or globally across the application to support localization of resources in views.
The End Result
After implementing the above I can now decorate my action (or controller, or application) with a single attribute…
[Localized]
public ActionResult Index()
{
return View();
}
…and get localised resources in my views with no messy code…
<h1>Hello, @Resources.Localize("World")</h1>
Not bad!