Sunday, July 7, 2013

Changing Culture in WPF

I know my blog has been a bit empty over the last 6 weeks or so.  I've been working heavily on my C# Interfaces course for Pluralsight which released last week.  So now it's time to catch up on some articles that I've been intending to write.

This time around, we'll look at a bit of a hack that makes changing the culture in a running WPF application a bit easier than the majority of the solutions out there.  The sample project can be downloaded here: http://www.jeremybytes.com/Downloads.aspx#WPFCC.

Using the Machine Culture is Easy
.NET provides us a fairly straightforward way to support different languages and cultures in our applications. By default, our applications use the culture for the machine that they run on.  This means that if we provide resource files with different strings (or images or whatever) that are appropriate for a language, we get the behavior basically for free.

For example, we can provide a resource string in a resx file:


We have resource files set up for "en-US", "en-GB", and "en-AU":


Then in our XAML code, we just need to reference that resource string rather than providing a hard-coded string:


Notice that our "Text" property of this TextBlock references the static "Greeting" string resource.  So, when our application runs, it will automatically get the culture-specific value.

Here's the output:


And we can see "Howdy, Y'all!" is used in our TextBlock.  If our machine is set to a different culture, then we would get that culture-specific string (or the default culture if it's not available).

Note: This isn't a tutorial on how to localize your application, so you'll need to dig into that yourself.  We're just getting the basics in place so we can look at the "hack".

[Update Jan 2014: If you do want to learn how to localize an application in .NET, you can check out my Introduction to Localization and Globalization in .NET course available on Pluralsight.]

Changing the Culture is Hard
So, we've seen how easy it is to use the machine culture.  And, in fact, it's not hard to put in a custom culture (different from the machine) as the application is starting up.  But if we want to change the culture while the application is running, we run into a bit of a problem.

The "free" behavior that we get above (culture-specific resource strings from a file) only happens when the WPF window is being created.  This means that even if we change the culture of our thread while our application is running, the open windows will continue to use the already-bound resource strings -- no automatic updates.

The Scenario
The first question you might ask is "Why do we want to change culture in a running app?"  Well, I ran across this issue while helping a friend working on a project.  He was building a kiosk application in WPF that needed to support multiple languages.  The idea is that the user would walk up to the kiosk, press the button for the language they wanted, and then go from there.

If you cruise the internet, you will come across some interesting solutions.  Some people have built libraries where you call a method to rebind all of the resources of an open window.  I'm not a big fan of this solution just because it seems overly complicated.  And some of the libraries required you to put an attached property on all of the UI controls.

The most basic recommendation to change the culture of your application is to shut down and restart the app.  The idea behind this is that you update a configuration file with the new culture that you want.  Then you restart the application (which is never as easy as it sounds).  When the app starts up, it uses the new culture from configuration.

The "restart" solution sounded like the better solution of these two, but then I had a kind of crazy idea.

The Application MainWindow Property
In my sample code for my session on Dependency Injection (Dependency Injection: A Practical Introduction), I manually create the MainWindow for a WPF application.

Here's what the simplified code looks like (from App.xaml.cs):


The Application.Current.MainWindow is a property on our application that tells us what the main window is.  When the main window is closed, the application itself shuts down.  In this example, we create a new instance of the MainWindow class (which is defined by MainWindow.xaml) and then assign it to the MainWindow property of the application.  Then we Show the MainWindow.

The effect of this is that the MainWindow class is shown.  And when it is closed, the application shuts down.

So, I started to wonder if we could hack around with the MainWindow property a little bit so that we could simply create a new window (with a different culture) rather than shutting down the entire application.

The Hack: Resetting the Main Window
In the App.xaml.cs file, I added a new static method that would swap out the main window of the application.  Here's the code:


Let's step through this.

First, we set the CurrentCulture and the CurrentUICulture for the current thread.  The CurrentCulture defines the culture that the thread operates on.  The CurrentUICulture determines which resource file is going to be used.  For example, if we set the CurrentUICulture to a CultureInfo for "en-US", it will use the resources that are defined in the "Resources.en-US.resx" file (which gets translated into its own assembly/folder when the app is compiled).

We want to set the CurrentUICulture *before* we create the window.  This makes sure that the correct resources will be picked up.

After setting the culture, we create a local variable ("oldWindow") that points to the currently assigned MainWindow. We grab a reference so that we can programmatically close this window later.

Then we create a new MainWindow instance (again, this is coming from MainWindow.xaml).  This will be created with the resources for the new culture.  We assign it to the MainWindow property, and it becomes the main window for the application.

Next, we show the (new) MainWindow.

As a last step, we close the "old" window -- the one with the previous culture.  Since this is no longer the application's "MainWindow", it does not shut down the application.

The result is that the application will have a noticeable "window opening / window closing" effect, but we've managed to do this without shutting down the application.

The Results
Let's see the result in our sample application.  First, as we've seen, we have a TextBlock that uses the "Greeting" resource string.  In addition to that, we have 3 other text blocks.  In the code, we show the current culture, and we assign a date to display with both the short date format and the long date format.

Here's the code for that:


"displayDate.ToString("d")" gives us the short date format, which is "MM/dd/yyyy" in en-US.  "displayDate.ToString("D")" gives us the long date format, which is "dddd, MMMM dd, yyyy" in en-US.

We also have 3 buttons to change the culture.  Each button is similar.  Here's the click handler for the "US" button:


We can see that this calls the ChangeCulture method we looked at earlier.  This will re-create the window.  The constructor of the MainWindow calls "UpdateUIElements" to put the appropriate values in the text blocks.

Here are the outputs if we click each button in turn:




We can see that the "Greeting" text is different for each window.  Also, the short date and long date formats are appropriate to the culture.

As a bit of a humorous side note: I originally used today's date for the sample.  But since today is July 7, 2013, it would be very difficult to tell the difference between "07/07/2013" and "07/07/2013".

Wrap Up
So, this is a bit of a hack to re-create the main window for a WPF application.  But it is easier than restarting the application with new configuration values.  And with the kiosk scenario, it isn't a problem if we "reset" the entire application when the language is changed.

You might be able to use this same hack in a Windows Forms application (although getting to the application is a little less straight-forward).  It may also work in Silverlight by resetting the RootVisual (but I have not tried that).  I seriously doubt that it would work in a Windows 8 App Store app; this is also based on speculation since I have not done much with those types of apps.

Sometimes we come up with creative solutions to specific problems.  This one probably isn't very widely applicable, but it's fun to see what we can come up with nonetheless.

Happy Coding!

8 comments:

  1. Do OnStartUp() need to update to following one so that Current culture is used properly to populate Greeting string from Resource.resx file instead of Resources.en.resx.

    For example if I change Format to hi-IN. In this case above OnStartUp() will not populate greeting string from Resource.resx

    If if looked, CultureInfo.CurrentCulture is showing hi-IN. Following is the changed code. Please correct if it is wrong.

    protected override void OnStartup(StartupEventArgs e)
    {
    base.OnStartup(e);
    Thread.CurrentThread.CurrentCulture = CultureInfo.CurrentCulture;
    Thread.CurrentThread.CurrentUICulture = CultureInfo.CurrentCulture;
    Application.Current.MainWindow = new MainWindow();
    Application.Current.MainWindow.Show();
    }

    ReplyDelete
    Replies
    1. When the application starts up, the thread automatically gets the CurrentCulture, so you don't need to set it in the OnStartup() method. You probably need to add a Resources.hi-IN.resx file if that's the culture you're trying to use. If .NET can't find the specified culture, then it defaults to the en-US resource file (Microsoft is a bit US-centric).

      For more information, you might want to look at my Pluralsight course on Localization and Globalization (http://www.pluralsight.com/courses/intro-to-localization-globalization-dotnet). You can sign up for a 10-day free-trial which would allow you to watch the course.

      Delete
  2. Nice and easy solution, thanks. For a good user experience I'd say that the "context" of the application must be restored, but as you can keep all view models alive it should not be too difficult.

    ReplyDelete
  3. Hi I have a multi windows app. I am trying to do the selection from a child window. if i use your code, it will change the main window's language, child window doesn't change until i close and reopen. is this normal? any idea how i can fix it? thank you

    ReplyDelete
    Replies
    1. Yes this is normal. The easiest way to get the new culture/language is to close and reopen all windows (including the child windows). Other solutions are a bit more complex and often include libraries that need custom properties added to the UI controls for data binding. That's why I use the hack that is shown in this article. It doesn't address all scenarios (such as needing to maintain data when the form is reopened), but it works well for the scenario described here.

      Delete