Making the ASP.NET Mobile Controls render RTL languages
When you're rendering HTML for an Arabic or Hebrew speaking audience (or Farsi, or Divehi, or Urdu, or Syriac) you usually want to change the direction of the entire page, and that's done by adding dir="rtl" to a containing element of your page, typically the HTML outer element if you want the scrollbar to switch from the left to the right. RTL, of course, means Right To Left, as these languages are typically read right to left.
In ASP.NET 1.x, this was difficult programmatically, necessitating the BODY or HTML tag being runat="server" as you went hunting for it and manually added the dir="rtl" attribute yourself. I talked about this on the blog three years ago.
It gets a little easier in ASP.NET 2.0 with the addition of the ContentDirection enumeration/property that's added to the Panel and WebPart controls.
However, if you're working with the MobileControls, it appears to be quite a bit harder, as the whole "form" metaphor changes. The Mobile stuff in ASP.NET has the notion of multiple "forms" per page where only one is active. It is that active form that does the actual rendering of the <html> element.
That means even if you have "<html xmlns="http://www.w3.org/1999/xhtml">" in your ASPX markup, you might end up with just <html> because it is the mobile:Form control that does the rendering via a PageAdapter that inherits from MobileTextWriter.
The mobile stuff in ASP.NET is very powerful, but very obtuse. I wanted to add dir="rtl" programmatically, but there's no support for custom attributes on a mobile:Form, and the Alignment="right" attribute is semantically different from dir="rtl."
Here's a page with Hebrew on it that has dir="ltr," which is also the default if you include no dir= attribute. Notice that the there are three labels for Account Name, Number, and Amount, and the labels that are within strings that include numbers have the labels on the RIGHT with the colons (:) within the label to the LEFT of the label.
That is, NUMBER COLON LABEL.
While the line without numbers, or more specifically with English included has the Hebrew labels with a colon to the right.
That is LABEL COLON ENGLISHWORD.
This is because the browser, independent of dir= attribute (or lack of one) is trying to "do the right thing" with this mixed content page (that happens to be Unicode). You can override this behavior with markup like dir= or with CSS styles like unicode-bidi: bidi-override or unicode-bidi: embed. More details on that are in the very good W3C I18N FAQ on CSS vs. Markup for BIDI (BiDirectional) support. In this post, I'm just talking about markup.
I tried to do this:
this.ActiveForm.CustomAttributes.Add("dir","rtl")
but I got this funny error message. Why funny you ask? Because it's telling me that I can't set dir="rtl" but if you do a view source (or just notice where the scrollbar is!) you'll see that it DID in fact successfully apply dir="rtl".
This error goes away if I add this to my web.config.
<mobileControls cookielessDataDictionaryType="System.Web.Mobile.CookielessData" allowCustomAttributes="true"/>
However, the CustomAttributes bag isn't the way to effectively manipulate the rendering of the mobile controls.
Instead, I needed creating my own PageAdapter, specifically a DirectionAwareHtmlPageAdapter:
1: namespace Corillian.Web.Mobile
2: {
3: public class DirectionAwareHtmlPageAdapter : System.Web.UI.MobileControls.Adapters.HtmlPageAdapter
4: {
5: public override void RenderForm(System.Web.UI.MobileControls.Adapters.HtmlMobileTextWriter writer, System.Web.UI.MobileControls.Form form)
6: {
7: writer.BeginFile(this.GetFormUrl(form), "text/html", this.Page.Response.Charset);
8:
9: //Added by me to support RTL - sucks!
10: writer.WriteBeginTag("html");
11: if (Corillian.Voyager.Web.Globalization.CurrentCulture.IsRightToLeftCulture(Thread.CurrentThread.CurrentUICulture.Name))
12: {
13: writer.WriteAttribute("dir", "rtl");
14: }
15: writer.Write(System.Web.UI.MobileControls.Adapters.HtmlMobileTextWriter.TagRightChar);
16:
17: form.RenderControl(writer);
18: if (this.Device.RequiresDBCSCharacter)
19: {
20: writer.Write("<!--\u3000-->");
21: }
22: writer.WriteEndTag("html");
23: writer.EndFile();
24: }
25:
26: }
27: }
This kind of sucks, because the only CUSTOM code is between line 9 and 15. The rest is copy/pasted directly out of HtmlPageAdapter using Reflector. I really think the mobile guys (and the whole ASP.NET team) should have considered the need for dynamically changing page direction for both mobile and desktop HTML. Perhaps this adapter technique is what they recommend, or perhaps there's a hidden or simpler way that I haven't figured out yet. If so, I'll post that and retract all this silliness...but until then...
You'll notice the call to a custom method called "IsRightToLeftCulture." I couldn't find anywhere in the Globalization namespace where one could just ASK a CultureInfo object if it referred to a RTL or LTR language. I know this was the case in .NET 1.1, and I'm 99% sure it doesn't exist in .NET 2.0 either. I'd love to be told otherwise.
UPDATE: I can use System.Globalization.CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft() in .NET 2.0 so there's no need for this function under 2.0.
So, here is this little gem, but what price my immortal soul?
1: /// <summary>
2: /// Returns true if the supplied culture (or its parent) is read from Right-to-Left
3: /// </summary>
4: /// <returns>True if the supplied culture (or its parent) is read from Right-to-Left</returns>
5: public static bool IsRightToLeftCulture(string culture)
6: {
7: //Only look at the parent culture.
8: string myCulture = culture.Split(new char[]{'-'})[0];
9: switch (myCulture)
10: {
11: case "ar": //Arabic
12: case "fa": //Farsi
13: case "div": //Divehi
14: case "syr": //Syriac
15: case "he": //Hebrew
16: case "ur": //Urdu
17: return true;
18: }
19: return false;
20: }
Now that I have this new PageAdapter, I need to tell the world about it by editing my web.config:
<mobileControls cookielessDataDictionaryType="System.Web.Mobile.CookielessData" allowCustomAttributes="true">
<device name="DirectionAwareHtmlDeviceAdapters" inheritsFrom="HtmlDeviceAdapters" pageAdapter="Corillian.Web.Mobile.DirectionAwareHtmlPageAdapter,App_Code"/>
</mobileControls>
Note the Assembly Qualified Name (QN) in the pageAdapter attribute. See how the ClassName,AssemblyName uses "App_Code" for the assembly name? That's because my DirectionAwareHtmlDeviceAdapter.cs file is in my App_Code directory. That's one of the well-known directories that automatically gets compiled when there's a .cs file in it. The assembly that's generated is App_Code.dll. A little confusing. Alternatively, I could have put the class in a separate and more manually-compiled assembly and referenced it from there.
Now that the dir="rtl" is added to the HTML element the whole page renders correctly with the labels and all page elements not only right aligned, but rendered right-to-left. The direction changes dynamically based on the browser's Accept-Language HTTP Header.
Thanks to Patrick Cauldwell and Travis Illig for their help debugging today!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
System.Globalization.CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft()
My 2-cents,
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
CultureInfo culture = CultureInfo.CurrentCulture;
string cssName = "~/CSS/";
cssName += (culture.TextInfo.IsRightToLeft) ? "RTL" : "LTR";
cssName += ".css";
HtmlLink link = new HtmlLink();
link.Href = this.ResolveUrl(cssName);
link.Attributes["rel"] = "stylesheet";
link.Attributes["type"] = "text/css";
this.Page.Header.Controls.Add(link);
}
The RTL.css file is this:
bodyWhile the LTR.css looks like this:
{
direction:rtl;
}
.first
{
clear:right;
float:right;
}
.middle
{
clear:none;
float:right;
}
.last
{
clear:left;
float:right;
}
body
{
direction:ltr;
}
.first
{
clear:left;
float:left;
}
.middle
{
clear:none;
float:left;
}
.last
{
clear:right;
float:left;
}
Comments are closed.
Thanks for your great articles, I check out your blog everyday to see what's new. I'm not getting into the podcast thing yet, Not enough time in the day with all the other stuff I have going. But I'm trying to figure out how to do it when I get my Zune...