In addition to blogging, I'm also using Twitter. Follow me @matthawley
Note: This post has been updated to work with MVC 2 RTM. The example code is a bit smelly (use POCOs, please!) but the localization helpers do correctly work with the WebFormViewEngine.
You're localizing your application right? Sure, I bet we ALL are - or at least, we're all storing our strings in resource files so that later we can localize. I know, I don't either :) but that doesn't mean if you're working on a large application that needs to be localized in many different languages, you shouldn't be thinking about it. While localization was possible in 1.0/1.1, ASP.NET 2.0 introduced us to a new expression syntax that made localization much easier, simply writing the following code
1: <asp:Label Text="<%$ Resources:Strings, MyGlobalResource %>" runat="server" />
2: <asp:Label Text="<%$ Resources:MyLocalResource %>" runat="server" />
Of course, you could always use the verbose way and call out to the HttpContext to get local and global resources, but I really enjoy writing the expression syntax much better as it truly implies that the code knows the context of your view / page. So, you could write both of the above examples like
1: <%= HttpContext.Current.GetGlobalResourceString("Strings", "MyGlobalResources",
2: CultureInfo.CurrentUICulture) %>
3: <%= HttpContext.Current.GetLocalResourceString("~/views/products/create.aspx",
4: "MyLocalResource", CultureInfo.CurrentUICulture) %>
So now, you've started on that next big project and have been given the green light to use ASP.NET MVC, but ... your application needs to be localized in Spanish as well. In the current bits, there's really no way of using localized resources aside from (gasp!) using the Literal server control or the verbose method. But, you're moving to MVC to get away from the web forms model & nomenclature, so those are not an option any longer. Well, taking my earlier example of PRG pattern, I decided to "localize" it in an example of your project. First off, you'll need to create your global and local resources. Add a "App_GlobalResources" folder to the root. Add a Strings.resx file, and start to enter your text. Next, we'll add 2 local resources for our views. Under /Views/Products, create a "App_LocalResources", and 2 .resx files named "Create.aspx.resx" and "Confirm.aspx.resx".
Okay, now you're all set. Let's start converting our code to use the resources. You'll see that I'm using a new extension method (code will come later) in both the controller actions and in the view itself.
1: public class ProductsController : Controller
2: {
3: public ActionResult Create()
4: {
5: if (TempData["ErrorMessage"] != null)
6: {
7: ViewData["ErrorMessage"] = TempData["ErrorMessage"];
8: ViewData["Name"] = TempData["Name"];
9: ViewData["Price"] = TempData["Price"];
10: ViewData["Quantity"] = TempData["Quantity"];
11: }
12: return RenderView();
13: }
14:
15: public ActionResult Submit()
16: {
17: string error = null;
18: string name = Request.Form["Name"];
19: if (string.IsNullOrEmpty(name))
20: error = this.Resource("Strings, NameIsEmpty");
21:
22: decimal price;
23: if (!decimal.TryParse(Request.Form["Price"], out price))
24: error += this.Resource("Strings, PriceIsEmpty");
25:
26: int quantity;
27: if (!int.TryParse(Request.Form["Quantity"], out quantity))
28: error += this.Resource("Strings, QuantityIsEmpty");
29:
30: if (!string.IsNullOrEmpty(error))
31: {
32: TempData["ErrorMessage"] = error;
33: TempData["Name"] = Request.Form["Name"];
34: TempData["Price"] = Request.Form["Price"];
35: TempData["Quantity"] = Request.Form["Quantity"];
36: return RedirectToAction("Create");
37: }
38:
39: return RedirectToAction("Confirm");
40: }
41:
42: public ActionResult Confirm()
43: {
44: return RenderView();
45: }
46: }
Next, convert views over to use the new Resource extension method, below is the Create view:
1: <% Html.BeginForm("Submit"); %>
2: <% if (!string.IsNullOrEmpty((string)ViewData["ErrorMessage"])) { %>
3: <div style="color:red;"><%= ViewData["ErrorMessage"] %></div>
4: <% } %>
5: <%= Html.Resource("Name") %> <%= Html.TextBox("Name", ViewData["Name"]) %><br />
6: <%= Html.Resource("Price") %> <%= Html.TextBox("Price", ViewData["Price"]) %><br />
7: <%= Html.Resource("Quantity") %> <%= Html.TextBox("Quantity", ViewData["Quantity"]) %><br />
8: <input type="submit" value="<%= Html.Resource("Save") %>" />
9: <% Html.EndForm(); %>
Here's the Confirm view:
1: <%= Html.Resource("Thanks") %><br /><br />
2: <%= Html.Resource("CreateNew", Html.ActionLink<ProductsController>(c => c.Create(),
3: Html.Resource("ClickHere"))) %>
As you can see, I'm using a mixture of resource expressions both within the controller and view implementation. Here are the main implementations:
1: // default global resource
2: Html.Resource("GlobalResource, ResourceName")
3:
4: // global resource with optional arguments for formatting
5: Html.Resource("GlobalResource, ResourceName", "foo", "bar")
6:
7: // default local resource
8: Html.Resource("ResourceName")
9:
10: // local resource with optional arguments for formatting
11: Html.Resource("ResourceName", "foo", "bar")
As you can see, it supports both Global Resources and Local Resources. When working within your controller actions, only Global Resources work as we don't have a concept of a "local resource." The implementation for Html.Resource is actually a wrapper around the verbose method I previously mentioned. It does, however, take into consideration the expression syntax and the context of where the code is calling from to smartly determine the correct resource call to make. A gotcha in the codebase is that this code will only work with the WebFormViewEngine out of the box for local resources. The reason for this is that the code needs a way to find the associated virtual path for the view it's currently rendering, which is only available for the WebFormsView. Should you be using another View Engine, you'll have to modify the codebase to use derived IView type to find the virtual path. So, here's the code:
1: public static string Resource(this HtmlHelper htmlhelper, string expression, params object[] args)
2: {
3: string virtualPath = GetVirtualPath(htmlhelper);
4: return GetResourceString(htmlhelper.ViewContext.HttpContext, expression, virtualPath, args);
5: }
6:
7: public static string Resource(this Controller controller, string expression, params object[] args)
8: {
9: return GetResourceString(controller.HttpContext, expression, "~/", args);
10: }
11:
12: private static string GetResourceString(HttpContextBase httpContext, string expression, string virtualPath, object[] args)
13: {
14: ExpressionBuilderContext context = new ExpressionBuilderContext(virtualPath);
15: ResourceExpressionBuilder builder = new ResourceExpressionBuilder();
16: ResourceExpressionFields fields = (ResourceExpressionFields)builder.ParseExpression(expression, typeof(string), context);
17:
18: if (!string.IsNullOrEmpty(fields.ClassKey))
19: return string.Format((string)httpContext.GetGlobalResourceObject(fields.ClassKey, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
20:
21: return string.Format((string)httpContext.GetLocalResourceObject(virtualPath, fields.ResourceKey, CultureInfo.CurrentUICulture), args);
22: }
23:
24: private static string GetVirtualPath(HtmlHelper htmlhelper)
25: {
26: WebFormView view = htmlhelper.ViewContext.View as WebFormView;
27:
28: if (view != null)
29: return view.ViewPath;
30:
31: return null;
32: }
And just so you know I'm not lying - here's the output in English and Spanish!
Since this example code is so lengthy, I've zipped up the main code to make things much easier for you to bring into your solution.
Currently rated 4.9 by 17 people
- Currently 4.9/5 Stars.
- 1
- 2
- 3
- 4
- 5