Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 574
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


.NET


 

RenderAction method had been introduced when ASP.NET MVC 1.0 released in its MvcFuture assembly and then final announced along with the ASP.NET MVC 2.0. Similar as RenderPartial, the RenderAction can display some HTML markups which defined in a partial view in any parent views. But the RenderAction gives us the ability to populate the data from an action which may different from the action which populating the main view.

For example, in Home/Index.aspx we can invoke the Html.RenderPartial(“MyPartialView”) but the data of MyPartialView must be populated by the Index action of the Home controller. If we need the MyPartialView to be shown in Product/Create.aspx we have to copy (or invoke) the relevant code from the Index action in Home controller to the Create action in the Product controller which is painful. But if we are using Html.RenderAction we can tell the ASP.NET MVC from which action/controller the data should be populated. in that way in the Home/Index.aspx and Product/Create.aspx views we just need to call Html.RenderAction(“CreateMyPartialView”, “MyPartialView”) so it will invoke the CreateMyPartialView action in MyPartialView controller regardless from which main view.

But in my current project we found a bug when I implement a RenderAction method in the master page to show something that need to connect to the backend data center when the validation logic was failed on some pages. I created a sample application below.

 

Demo application

I created an ASP.NET MVC 2 application and here I need to display the current date and time on the master page. I created an action in the Home controller named TimeSlot and stored the current date into ViewDate. This method was marked as HttpGet as it just retrieves some data instead of changing anything.

   1: [HttpGet]
   2: public ActionResult TimeSlot()
   3: {
   4:     ViewData["timeslot"] = DateTime.Now;
   5:     return View("TimeSlot");
   6: }

Next, I created a partial view under the Shared folder to display the date and time string.

   1: <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
   2:  
   3: <span>Now: <%
   1: : ViewData["timeslot"].ToString() 
%></span>

Then at the master page I used Html.RenderAction to display it in front of the logon link.

   1: <div id="logindisplay">
   2:     <%
   1:  Html.RenderAction("TimeSlot", "Home"); 
%>
   3:  
   4:     <%
   1:  Html.RenderPartial("LogOnUserControl"); 
%>
   5: </div> 

It’s fairly simple and works well when I navigated to any pages. But when I moved to the logon page and click the LogOn button without input anything in username and password the validation failed and my website crashed with the beautiful yellow page. (I really like its color style and fonts…)

image

 

How ASP.NET MVC executes Html.RenderAction

In this example all other pages were rendered successful which means the ASP.NET MVC found the TimeSolt action under the Home controller except this situation. The only different is that when I clicked the LogOn button the browser send an HttpPost request to the server. Is that the reason of this bug? I created another action in Home controller with the same action name but for HttpPost.

   1: [HttpPost]
   2: [ActionName("TimeSlot")]
   3: public ActionResult TimeSlot(object dummy)
   4: {
   5:     return TimeSlot();
   6: }

Or, I can use the AcceptVerbsAttribute on the TimeSlot action to let it allow both HttpGet and HttpPost.

   1: [AcceptVerbs("GET", "POST")]
   2: public ActionResult TimeSlot()
   3: {
   4:     ViewData["timeslot"] = DateTime.Now;
   5:     return View("TimeSlot");
   6: }

And then repeat what I did before and this time it worked well.

image

Why we need the action for HttpPost here as it’s just data retrieving? That is because of how ASP.NET MVC executes the RenderAction method.

In the source code of ASP.NET MVC we can see when proforming the RenderAction ASP.NET MVC creates a RequestContext instance from the current RequestContext and created a ChildActionMvcHandler instance which inherits from MvcHandler class. Then the ASP.NET MVC processes the handler through the HttpContext.Server.Execute method. That means it performs the action as a stand-alone request asynchronously and flush the result into the  TextWriter which is being used to render the current page. Since when I clicked the LogOn the request was in HttpPost so when ASP.NET MVC processed the ChildActionMvcHandler it would find the action which allow the current request method, which is HttpPost. Then our TimeSlot method in HttpGet would not be matched.

image

 

Summary

In this post I introduced a bug in my currently developing project regards the new Html.RenderAction method provided within ASP.NET MVC 2 when processing a HttpPost request. In ASP.NET MVC world the underlying Http information became more important than in ASP.NET WebForm world. We need to pay more attention on which kind of request it currently created and how ASP.NET MVC processes.

 

Hope this helps,

Shaun

 

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

 

An upgraded article and solution had been posted here. I migrated the project to ASP.NET MVC 4 with some features added.

- Upgraded to ASP.NET MVC 4, Visual Studio 2012.
- Moved the solution to a separated project.
- Moved the procedure originally executed in base controller's ExecuteCore to a separated class. Now can use the this solution without needing inherit from the localization base controller.
- Implemented the localization resource provders so that can use any data source (database, XML file, etc.) to store the localization resource.
- Improved the language selector bar. Now can use partial view as the selected and unselected link content with images, scripts, etc.

Localization is a common issue when we develop a world wide web application. The key point of making your application localizable is to separate the page content from your logic implementation. That means, when you want to display something on the page, never put them directly on the page file (or the backend logic). You should give the content a key which can be linked to the real content for the proper language setting.

Last week I was implementing the localization on my ASP.NET MVC application. This is my first time to do it so I spent about 3 days for investigation, trying and come up with a final solution which only needs 1 day’s job. So let’s take a look on what I have done.

 

Localization supported by ASP.NET MVC

ASP.NET MVC was built on top of the ASP.NET runtime so all feature provided by ASP.NET can be used in MVC without any wheaks such as caching, session state and localization. In the traditional ASP.NET web form ages we were using the resource files to store the content of the application with different cultures and using the ResourceManager class to retrieve them which can be generated by Visual Studio automatically. In ASP.NET MVC they works well.

Let’s create a standard ASP.NET MVC application for an example. The website was in English and we can see all content are hard-written in the view pages and the controller classes.

image

Now what I need to do is to put all contents out of from the pages and the controllers. ASP.NET gives us a special folder named App_GlobalResources which contains the resource files for the content of all cultures. Just right-click the project in the solution explorer window and create the folder under the Add > Add ASP.NET Folders menu.

I created 2 resource files for 2 langauges: English and Chinese. The English would be the default language of this application so I will create Global.resx file firstly, then Global.zh.resx. The middle name ‘zh’ was the culture name of this language. If we need a French version in the future we can simply create Global.fr.resx. The Visual Studio will help us to generate the accessing class for them.

image

Then let’s move some content into the resource files. In the home page there are 3 places need to be changed: the title, message and the description. So we add 3 items in our 2 resource files.

image

The title and the description are defined in the view page so we will change the view page. It will load the content through the access class generated by Visual Studio.

   1: <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
   2:  
   3: <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
   4:     <%
   1: : Resources.Global.HomeIndex_Title 
%>
   5: </asp:Content>
   6:  
   7: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   8:     <h2><%
   1: : ViewData["Message"] 
%></h2>
   9:     <p>
  10:         <%
   1: : Resources.Global.Home_Index_Desc 
%> <a href="http://asp.net/mvc" title='<%: Resources.Global.Home_Index_DescLink %>'>http://asp.net/mvc</a>
  11:     </p>
  12: </asp:Content>

The message was defined in the controller class and passed to the view page through the ViewData so we also need to change the home controller as well.

   1: public ActionResult Index()
   2: {
   3:     ViewData["Message"] = Resources.Global.HomeIndex_Message;
   4:  
   5:     return View();
   6: }

 

Specify the language through the URL

We had moved the content into the resource files but our application does not support localization since there’s no place we can specify the language setting. In order to make it as simple as possible we will make the URL indicate the current selected language, which means if my URL was http://localhost/en-US/Home/Index it will in English while http://localhost/zh-CN/Home/Index will in Chinese. The user can change the language at any pages he’s staying, and also when he want to share the URL it will pass his language setting as well.

In order to do so I changed the application routes, add a new route with a new partten named lang in front of the controller.

   1: public static void RegisterRoutes(RouteCollection routes)
   2: {
   3:     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
   4:  
   5:     routes.MapRoute(
   6:         "Localization", // Route name
   7:         "{lang}/{controller}/{action}/{id}", // URL with parameters
   8:         new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
   9:     );
  10:  
  11:     routes.MapRoute(
  12:         "Default", // Route name
  13:         "{controller}/{action}/{id}", // URL with parameters
  14:         new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  15:     );
  16:  
  17: }

You may noticed that I added a new route rather than modifed the default route, and didn’t specify the default value of the {lang} pattern. It’s because we need the default route render the default request which without the language setting such as http://localhost/ and http://localhost/Home/Index.

If I modied the default route, http://localhost/ cannot be routed; and the http://localhost/Home/Index would be routed to lang = Home, controller = Index which is incorrect.

Since we need the URL control the language setting we should perform some logic before each action was executed. The ActionFilter would be a good solution in this scenario.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web;
   5: using System.Web.Mvc;
   6: using System.Threading;
   7: using System.Globalization;
   8:  
   9: namespace ShaunXu.MvcLocalization
  10: {
  11:     public class LocalizationAttribute : ActionFilterAttribute
  12:     {
  13:         public override void OnActionExecuting(ActionExecutingContext filterContext)
  14:         {
  15:             if (filterContext.RouteData.Values["lang"] != null &&
  16:                 !string.IsNullOrWhiteSpace(filterContext.RouteData.Values["lang"].ToString()))
  17:             {
  18:                 // set the culture from the route data (url)
  19:                 var lang = filterContext.RouteData.Values["lang"].ToString();
  20:                 Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
  21:             }
  22:             else
  23:             {
  24:                 // load the culture info from the cookie
  25:                 var cookie = filterContext.HttpContext.Request.Cookies["ShaunXu.MvcLocalization.CurrentUICulture"];
  26:                 var langHeader = string.Empty;
  27:                 if (cookie != null)
  28:                 {
  29:                     // set the culture by the cookie content
  30:                     langHeader = cookie.Value;
  31:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  32:                 }
  33:                 else
  34:                 {
  35:                     // set the culture by the location if not speicified
  36:                     langHeader = filterContext.HttpContext.Request.UserLanguages[0];
  37:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  38:                 }
  39:                 // set the lang value into route data
  40:                 filterContext.RouteData.Values["lang"] = langHeader;
  41:             }
  42:  
  43:             // save the location into cookie
  44:             HttpCookie _cookie = new HttpCookie("ShaunXu.MvcLocalization.CurrentUICulture", Thread.CurrentThread.CurrentUICulture.Name);
  45:             _cookie.Expires = DateTime.Now.AddYears(1);
  46:             filterContext.HttpContext.Response.SetCookie(_cookie);
  47:  
  48:             base.OnActionExecuting(filterContext);
  49:         }
  50:     }
  51: }

I created an attribute named LocalizationAttribute which inherited from the ActionFilterAttribute and overrided its OnActionExecuting method. I firstly checked the RouteData. If it contains the language setting I will set it to the CurrentUICulture of the CurrentThread, which will indicate the resource manager (generated by Visual Studio based on the resource files) retrieve the related value.

If no language setting in the RouteData I checked the cookie and set it if available. Otherwise I used the user language of the HttpRequest and set into the current thread.

Finally I set the language setting back to the route data so all coming actions would retrieve it and also saved it into the cookie so that next time the user opened the browser he will see his last language setting.

Then I applied the attribute on the home controller so that all actions will perform my localization logic.

   1: namespace ShaunXu.MvcLocalization.Controllers
   2: {
   3:     [HandleError]
   4:     [Localization]
   5:     public class HomeController : Controller
   6:     {
   7:         public ActionResult Index()
   8:         {
   9:             ViewData["Message"] = Resources.Global.HomeIndex_Message;
  10:  
  11:             return View();
  12:         }
  13:  
  14:         public ActionResult About()
  15:         {
  16:             return View();
  17:         }
  18:     }
  19: }

Now if we start the application and add the language setting on the URL we can see the result.

image

image

 

Links for the language selection

Let the user change the language through the URL would not be a good solution. We need to give them some links on top of the pages so that they can change it at any time. In ASP.NET MVC the simplest way is to create a HtmlHelper to render the links for each language.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web;
   5: using System.Web.Routing;
   6: using System.Web.Mvc;
   7: using System.Web.Mvc.Html;
   8: using System.Threading;
   9:  
  10: namespace ShaunXu.MvcLocalization
  11: {
  12:     public static class SwitchLanguageHelper
  13:     {
  14:         public class Language
  15:         {
  16:             public string Url { get; set; }
  17:             public string ActionName { get; set; }
  18:             public string ControllerName { get; set; }
  19:             public RouteValueDictionary RouteValues { get; set; }
  20:             public bool IsSelected { get; set; }
  21:  
  22:             public MvcHtmlString HtmlSafeUrl
  23:             {
  24:                 get
  25:                 {
  26:                     return MvcHtmlString.Create(Url);
  27:                 }
  28:             }
  29:         }
  30:  
  31:         public static Language LanguageUrl(this HtmlHelper helper, string cultureName,
  32:             string languageRouteName = "lang", bool strictSelected = false)
  33:         {
  34:             // set the input language to lower
  35:             cultureName = cultureName.ToLower();
  36:             // retrieve the route values from the view context
  37:             var routeValues = new RouteValueDictionary(helper.ViewContext.RouteData.Values);
  38:             // copy the query strings into the route values to generate the link
  39:             var queryString = helper.ViewContext.HttpContext.Request.QueryString;
  40:             foreach (string key in queryString)
  41:             {
  42:                 if (queryString[key] != null && !string.IsNullOrWhiteSpace(key))
  43:                 {
  44:                     if (routeValues.ContainsKey(key))
  45:                     {
  46:                         routeValues[key] = queryString[key];
  47:                     }
  48:                     else
  49:                     {
  50:                         routeValues.Add(key, queryString[key]);
  51:                     }
  52:                 }
  53:             }
  54:             var actionName = routeValues["action"].ToString();
  55:             var controllerName = routeValues["controller"].ToString();
  56:             // set the language into route values
  57:             routeValues[languageRouteName] = cultureName;
  58:             // generate the language specify url
  59:             var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
  60:             var url = urlHelper.RouteUrl("Localization", routeValues);
  61:             // check whether the current thread ui culture is this language
  62:             var current_lang_name = Thread.CurrentThread.CurrentUICulture.Name.ToLower();
  63:             var isSelected = strictSelected ?
  64:                 current_lang_name == cultureName :
  65:                 current_lang_name.StartsWith(cultureName);
  66:             return new Language()
  67:             {
  68:                 Url = url,
  69:                 ActionName = actionName,
  70:                 ControllerName = controllerName,
  71:                 RouteValues = routeValues,
  72:                 IsSelected = isSelected
  73:             };
  74:         }
  75:  
  76:         public static MvcHtmlString LanguageSelectorLink(this HtmlHelper helper,
  77:             string cultureName, string selectedText, string unselectedText,
  78:             IDictionary<string, object> htmlAttributes, string languageRouteName = "lang", bool strictSelected = false)
  79:         {
  80:             var language = helper.LanguageUrl(cultureName, languageRouteName, strictSelected);
  81:             var link = helper.RouteLink(language.IsSelected ? selectedText : unselectedText,
  82:                 "Localization", language.RouteValues, htmlAttributes);
  83:             return link;
  84:         }
  85:  
  86:     }
  87: }

I created a class to store the information of the language links. This can be used to render a linkage for a language, and it also can be used if we need the selector it be an image linkage, dropdown list or anything we want as well.

The LanguageUrl method takes the main responsible for generating the information that can be used in the selector such as the URL, RouteValues, etc. It loads the RouteData and query string from the incoming request and swich the language part, then generate the URL of current page with that language so that it will render the same page with that language when the user clicked.

The LanguageSelectorLink method takes the responsible for rendering a full Html linkage for this language which we will use it for our simple exmaple.

We need the language select available in all pages so we should put the links in the master page.

   1: <div id="logindisplay">
   2:     <%
   1:  Html.RenderPartial("LogOnUserControl"); 
%>
   3:  
   4:     <%
   1: : Html.LanguageSelectorLink("en-US", "[English]", "English", null) 
%>
   5:     <%
   1: : Html.LanguageSelectorLink("zh-CN", "[??]", "??", null) 
%>
   6: </div> 

Don’t forget to import the namespace of the SwitchLanguageHelper class on top of the master page otherwise the extension method will not work.

image

 

Oops! Log On page crashed

Some web application only can be viewed once logged on, such as some CRM system used inside the company. It means before any action was performed the application should be redirected to the log on page if the user was not identitied. Let’s have a look on what will be in our sample application. Just add the Authorize attribute on the home controller.

image

Oops! It gave me a 404 error which means the application cannot find the resource for /Account/LogOn. This is because our routes. We have 2 routes registered in the system, the Localization route has 4 parttens: lang, controller, action and id; the Default route has 3 parttens: controller, action and id. The incoming request will be checked through all routes based on their rules and once it’s matched the value would be identified by that route. For example if the URL was http://localhost/en-US/Home/Index it matches the Localization route (lang = en-US, controller = Home, action = Index, id is optional). If the URL was http://localhost/ it was not match the Localization route (lang cannot be null or empty) so it matches the Default route since it allows all parttens to be null.

Now let’s have a look on this URL http://localhost/Account/LogOn, the Account could be the lang partten and LogOn could be the controller, since the action and id partten are all optional it matchs the Localization route so it means: language = Account, controller = LogOn, action = Index (by default). But there is no controller named LogOn and action named Index so it’s the reason why it returned 404 error.

Since the logon URL was redirected by the ASP.NET authorizing model which means we cannot add the language data into the URL we have to add a new route individually for it.

   1: routes.MapRoute(
   2:     "LogOn", // Route name
   3:     "Account/{action}", // URL with parameters
   4:     new { controller = "Account", action = "LogOn" } // Parameter defaults
   5: );
   6:  
   7: routes.MapRoute(
   8:     "Localization", // Route name
   9:     "{lang}/{controller}/{action}/{id}", // URL with parameters
  10:     new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  11: );
  12:  
  13: routes.MapRoute(
  14:     "Default", // Route name
  15:     "{controller}/{action}/{id}", // URL with parameters
  16:     new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  17: );

In front of the Localization and Default route I added a new one named LogOn, it only accept the Account controller so for this URL http://localhost/Account/LogOn the controller would be Account and action would be LogOn which is correct. Let’s have a look on the result.

image

 

Refactoring the codes

This application is a fairly simple example, like all example the microsoft provided, it only contains one project which is my website. In the real projects we might not want to organize our application in one project. We would like to separate the webiste, the controllers, the models, and maybe we need the data access and business layers in the different projects. So let's refactor my example a little bit and see what we should do then to make it localizable.

Here I just want to separte the controllers and view models out of my website project but would not create the data access and business logic layer since normally they would not be related with the localization.

Localization is the matter we should solve at the persentation layer. We should never pass any localized information from the business layer. For example, if the password was incorrect when authorizing an user at the business layer we should always return a flag (int or enum) to indicate the error instead of any error message strings.

image

I moved the controllers and models out of the my main website project and let them referenced by the webiste project. I added the necessary assemblies and built it. All worked well except my localization code under the home controller class. I set the message string into the ViewData from the resource class which defined in my website project, now it cannot be accessed through my controller project.

image

So what I should do here is to move the resource files out of the website project since it's at the bottom of the references hierarchy.

image

After moved the resource files to the new project and added the refereneces, I built it again but I got more error. This is because the accessing classes generated for the resource files are defined as "internal" by default which cannot be invoked out of the project. So what I can do is to create the new resource files for localization and update their access model to "Public".

image And this time our application works well.

image

 

Localizing messages in ViewModels

In the ASP.NET MVC application the message can be defined directly on the view pages, controllers and the view models. One of the scenario is to define the error messages on the view model classes through the attributes provided by System.ComponentModel.DataAnnotations. With the DataAnnotations attributes we can add the validation method and the error messages on the propties of the model classes through the AOP approch. For example when loggin on the user name field should be displayed as "User name" and should be mandatory. So the model would be defined like this, whichi doesn’t support localization.

   1: [Required]
   2: [DisplayName("User name")]
   3: public string UserName { get; set; }

We can defined the error messages for the Requeired attribute. And the pre-defined DataAnnotations attributes supports localization which means we can define the resource key and resource type then it will find the relevant resources and returned the content back. Assuming that I had defined 2 resources one for the display name the other for error message, then the attribute would be changed like this.

   1: [Required(ErrorMessageResourceName = "LogOnModel_UserName_Required", 
   2:           ErrorMessageResourceType = typeof(Resources.Global))]
   3: [Display(Name = "LogOnModel_UserName_Required", 
   4:          ResourceType = typeof(Resources.Global))]
   5: public string UserName { get; set; }

You might noticed that I changed the DisplayName attribute to Display attribute. This is because the DisplayName attribute does not support localization. Let's execute and see what's happen.

image

Oops! I think we are running into a lot of problems.

  • The localization doesn't work. You can see on the URL it's in Chinese version but the user name displayed in English.
  • The display name doesn't work. In my model attributes I specified its resource name but now it shown the property's name.
  • The error message was not localized.

Let's explain one by one.

For the first one, the localization didn't work at all. That is because we implemented the localization by the action filter attribute we created before. But the validation logic was performed by the default model binder which was invoked before the action filter attribute was performed. So when the model binder failed the validation and attempted to retrieve the error message from the resource files the culture of the current thread has not been changed by the action filter.

image

In order to make the localization (culture setting) being invokde before the model binder was executed we should move the localization logic to the Controller.ExecuteCode method, which is earlier than the model binder and validation. So I created a new class named BaseController and let it inherited from the abstract Controller class and then overrided the ExecuteCode method with the localization logic. Then I updated all controllers in the application to inherit from this BaseController.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Web.Mvc;
   6: using System.Threading;
   7: using System.Globalization;
   8: using System.Web;
   9:  
  10: namespace ShaunXu.MvcLocalization.Controllers
  11: {
  12:     public abstract class BaseController : Controller
  13:     {
  14:         protected override void ExecuteCore()
  15:         {
  16:             if (RouteData.Values["lang"] != null &&
  17:                 !string.IsNullOrWhiteSpace(RouteData.Values["lang"].ToString()))
  18:             {
  19:                 // set the culture from the route data (url)
  20:                 var lang = RouteData.Values["lang"].ToString();
  21:                 Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
  22:             }
  23:             else
  24:             {
  25:                 // load the culture info from the cookie
  26:                 var cookie = HttpContext.Request.Cookies["ShaunXu.MvcLocalization.CurrentUICulture"];
  27:                 var langHeader = string.Empty;
  28:                 if (cookie != null)
  29:                 {
  30:                     // set the culture by the cookie content
  31:                     langHeader = cookie.Value;
  32:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  33:                 }
  34:                 else
  35:                 {
  36:                     // set the culture by the location if not speicified
  37:                     langHeader = HttpContext.Request.UserLanguages[0];
  38:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  39:                 }
  40:                 // set the lang value into route data
  41:                 RouteData.Values["lang"] = langHeader;
  42:             }
  43:  
  44:             // save the location into cookie
  45:             HttpCookie _cookie = new HttpCookie("ShaunXu.MvcLocalization.CurrentUICulture", Thread.CurrentThread.CurrentUICulture.Name);
  46:             _cookie.Expires = DateTime.Now.AddYears(1);
  47:             HttpContext.Response.SetCookie(_cookie);
  48:  
  49:             base.ExecuteCore();
  50:         }
  51:     }
  52: }

For the second and third problem that is because the DisplayName attributes was defined on .NET 4.0 platform but currently the ASP.NET MVC runtime was built on .NET 3.5 which cannot invoke the assemblies under .NET 4.0.

If you downloaded the source code of the ASP.NET MVC 2 there is a solution named MvcFuturesAspNet4 which will create some assemblies built on .NET 4.0 and support the DisplayName attribute. Here I would like to use another approch to work around it.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.ComponentModel;
   6: using System.ComponentModel.DataAnnotations;
   7:  
   8: namespace ShaunXu.MvcLocalization.Models
   9: {
  10:     public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  11:     {
  12:         private DisplayAttribute display;
  13:  
  14:         public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
  15:         {
  16:             this.display = new DisplayAttribute()
  17:             {
  18:                 ResourceType = resourceType,
  19:                 Name = resourceName
  20:             };
  21:         }
  22:  
  23:         public override string DisplayName
  24:         {
  25:             get
  26:             {
  27:                 return display.GetName();
  28:             }
  29:         }
  30:     }
  31: }

I created another attribute which wapped the DisplayName one. It will create an inner instance of the DisplayAttribute and set the resource key and type accordingly. Then when the DisplayName was invoked it will perform the DisplayAttribute.GetName method which supports localization.

So the view model part should be changed like this below.

   1: [Required(ErrorMessageResourceName = "LogOnModel_UserName_Required",
   2:           ErrorMessageResourceType = typeof(Resources.Global))]
   3: [LocalizationDisplayName("LogOnModel_UserName_Required", 
   4:                          typeof(Resources.Global))]
   5: public string UserName { get; set; }

And the let's take a look.

image

 

Summary

In this post I explained about how to implement the localization on an ASP.NET MVC web application. I utilized the resource files as the container of the localization information which provided by the ASP.NET runtime. And I also explain on how to update our solution while the project was being grown and separated which more usefule when we need to implement in the real projects.

The localization information can be stored in any places. In this post I just use the resource files which I can use the ASP.NET localization support classes. But we can store them into some external XML files, database and web services. The key point is to separate the content from the usage. We can isolate the resource provider and create the relevant interface to make it changable and testable.

 

PS: You can download the source code of the example here.

 

Hope this helps,

Shaun

 

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

 

When migrate your application onto the Azure one of the biggest concern would be the external files. In the original way we understood and ensure which machine and folder our application (website or web service) is located in. So that we can use the MapPath or some other methods to read and write the external files for example the images, text files or the xml files, etc. But things have been changed when we deploy them on Azure. Azure is not a server, or a single machine, it’s a set of virtual server machine running under the Azure OS. And even worse, your application might be moved between thses machines. So it’s impossible to read or write the external files on Azure. In order to resolve this issue the Windows Azure provides another storage serviec – Blob, for us.

Different to the table service, the blob serivce is to be used to store text and binary data rather than the structured data. It provides two types of blobs: Block Blobs and Page Blobs.

  • Block Blobs are optimized for streaming. They are comprised of blocks, each of which is identified by a block ID and each block can be a maximum of 4 MB in size.
  • Page Blobs are are optimized for random read/write operations and provide the ability to write to a range of bytes in a blob. They are a collection of pages. The maximum size for a page blob is 1 TB.

 

In the managed library the Azure SDK allows us to communicate with the blobs through these classes CloudBlobClient, CloudBlobContainer, CloudBlockBlob and the CloudPageBlob.

Similar with the table service managed library, the CloudBlobClient allows us to reach the blob service by passing our storage account information and also responsible for creating the blob container is not exist. Then from the CloudBlobContainer we can save or load the block blobs and page blobs into the CloudBlockBlob and the CloudPageBlob classes.

 

Let’s improve our exmaple in the previous posts – add a service method allows the user to upload the logo image.

In the server side I created a method name UploadLogo with 2 parameters: email and image. Then I created the storage account from the config file. I also add the validation to ensure that the email passed in is valid.

   1: var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
   2: var accountContext = new DynamicDataContext<Account>(storageAccount);
   3:  
   4: // validation
   5: var accountNumber = accountContext.Load()
   6:     .Where(a => a.Email == email)
   7:     .ToList()
   8:     .Count;
   9: if (accountNumber <= 0)
  10: {
  11:     throw new ApplicationException(string.Format("Cannot find the account with the email {0}.", email));
  12: }

Then there are three steps for saving the image into the blob service. First alike the table service I created the container with a unique name and create it if it’s not exist.

   1: // create the blob container for account logos if not exist
   2: CloudBlobClient blobStorage = storageAccount.CreateCloudBlobClient();
   3: CloudBlobContainer container = blobStorage.GetContainerReference("account-logo");
   4: container.CreateIfNotExist();

Then, since in this example I will just send the blob access URL back to the client so I need to open the read permission on that container.

   1: // configure blob container for public access
   2: BlobContainerPermissions permissions = container.GetPermissions();
   3: permissions.PublicAccess = BlobContainerPublicAccessType.Container;
   4: container.SetPermissions(permissions);

And at the end I combine the blob resource name from the input file name and Guid, and then save it to the block blob by using the UploadByteArray method. Finally I returned the URL of this blob back to the client side.

   1: // save the blob into the blob service
   2: string uniqueBlobName = string.Format("{0}_{1}.jpg", email, Guid.NewGuid().ToString());
   3: CloudBlockBlob blob = container.GetBlockBlobReference(uniqueBlobName);
   4: blob.UploadByteArray(image);
   5:  
   6: return blob.Uri.ToString();

Let’s update a bit on the client side application and see the result. Here I just use my simple console application to let the user input the email and the file name of the image. If it’s OK it will show the URL of the blob on the server side so that we can see it through the web browser.

image

Then we can see the logo I’ve just uploaded through the URL here.

image

You may notice that the blob URL was based on the container name and the blob unique name. In the document of the Azure SDK there’s a page for the rule of naming them, but I think the simple rule would be – they must be valid as an URL address. So that you cannot name the container with dot or slash as it will break the ADO.Data Service routing rule. For exmaple if you named the blob container as Account.Logo then it will throw an exception says 400 Bad Request.

 

Summary

In this short entity I covered the simple usage of the blob service to save the images onto Azure. Since the Azure platform does not support the file system we have to migrate our code for reading/writing files to the blob service before deploy it to Azure.

In order to reducing this effort Microsoft provided a new approch named Drive, which allows us read and write the NTFS files just likes what we did before. It’s built up on the blob serivce but more properly for files accessing. I will discuss more about it in the next post.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.