Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an Internationalized Wizard in WPF

0.00/5 (No votes)
17 Dec 2008 1  
Reviews a localizable WPF Wizard user interface written in both C# and VB.NET.

GlobalizedWizardShots.jpg

Table of Contents

Introduction

This article reviews a WPF application that contains a Wizard-style user interface. It explains how the application was globalized, so that it can run in various cultures and show its display text in any language. The application uses the Model-View-ViewModel (MVVM) design pattern as its core architecture. It was written twice: in C# and VB.NET. Both of those Visual Studio solutions are available for download at the top of this article. Note, the application was built and tested in Visual Studio 2008 with Service Pack 1.

Background

A “Wizard” is a user interface that presents the user with a set of steps that must be completed in a sequential order. For example, Wizards are commonly seen in application installers where the user must complete a series of steps and press a “Next” button to proceed from step to step. It is a useful UI design pattern for areas of an application that are not frequently used, or if you need to conserve screen real estate by revealing one piece of the UI at a time.

The Wizard reviewed in this article is fully internationalized. Internationalization of software applications consists of two separate tasks: globalization and localization. The process of ensuring that an application’s user interface can be displayed in various cultures using various languages (natural languages, not programming languages) is referred to as “globalization”. Translating the resources used by the UI (such as display text, images, etc.) is called “localization”. This article reviews a simple, yet effective, technique for internationalizing a WPF user interface.

Part of the globalization strategy employed in the application relies on the use of the Model-View-ViewModel (MVVM) design pattern. MVVM is a pattern that leverages the fundamental platform features of WPF to enable simple, loosely coupled, and highly testable client application architectures. This article does not attempt to explain the MVVM pattern, but simply shows it in use. If you are not already familiar with MVVM, don’t worry. The simplicity of the pattern should prevent it from obfuscating the core focus of what we examine.

For more information about any of these topics, be sure to visit the web pages listed in the “References” section towards the end of this article.

Quick History of this Article

This article’s demo application was created by Karl Shifflett and Josh Smith. Karl flew from Seattle to New York to visit Josh so that they could get together and share ideas about WPF, software architecture, internationalization, MVVM, and software development in general. When they were not enjoying the culinary excellence offered by New York City, they were sitting side-by-side working on the program reviewed in this article.

The app was originally written in VB.NET, Karl’s language of choice, and later on, Josh translated it to C#, his language of choice. Following that weekend, Josh was on vacation, so he took the burden of writing this article upon himself.

We realized that an article about creating internationalized software would not be very convincing unless it was accompanied by translated resources that actually appear in the UI at run time. Since neither of us knows any language other than American English, we turned to our fellow WPF Disciples and a Microsoft developer for some help. Big thanks go to Corrado Cavalli for translating the display text to Italian, Laurent Bugnion for translating it to French, and Marco Goertz, Microsoft Cider Team Senior Development Lead, for translating it to German.

The Demo Application

This article discusses an application that allows the user to order a cup of coffee by stepping through a set of Wizard pages. When the application first loads up, the user sees a simple window that allows him/her to launch the Wizard, as seen below:

DemoApp1.png

After clicking on the button in that window, the Wizard opens up and shows the Welcome page.

DemoApp2.png

Clicking on the Next button navigates to the first page in which the user must select options for his or her cup of coffee. After the drink size is selected, the price of the order appears in the lower left corner.

DemoApp3.png

After clicking the Next button again, the user is allowed to pick some “extras” for the cup of coffee being ordered. Since none of the options on this page are required for the order to be processed, the Next button is enabled at all times. If the user selects any extra flavorings, the price increases by fifty cents per shot of flavoring.

DemoApp4.png

Clicking the Next button again brings you to the Summary page, which is a read-only listing of all the selected options made in the previous pages. Since this is the final step in the workflow, the Next button text changes to “Finish” (or the equivalent word in another language).

DemoApp5.png

Once the Finish button is clicked, the Wizard closes and the user returns to the initial window. The cost of the user’s cup of coffee is displayed in this window. If the user cancelled out of the order, instead of clicking the Finish button, the window would indicate so.

DemoApp6.png

Wizard Architecture

The Wizard design is based on the Model-View-ViewModel (MVVM) design pattern. Each step in the workflow is represented by a separate ViewModel class, all of which derive from the CoffeeWizardPageViewModelBase class. All of these “Page ViewModels” are managed by the CoffeeWizardViewModel class, which exposes them via the Pages property, as seen below:

C#

public ReadOnlyCollection<CoffeeWizardPageViewModelBase> Pages
{
    get
    {
        if (_pages == null)
            this.CreatePages();
        return _pages;
    }
}

void CreatePages()
{
    var typeSizeVM = new CoffeeTypeSizePageViewModel(this.CupOfCoffee);
    var extrasVM = new CoffeeExtrasPageViewModel(this.CupOfCoffee);
    var pages = new List<CoffeeWizardPageViewModelBase>();
    pages.Add(new WelcomePageViewModel());
    pages.Add(typeSizeVM);
    pages.Add(extrasVM);
    pages.Add(new CoffeeSummaryPageViewModel(
         typeSizeVM.AvailableBeanTypes,
         typeSizeVM.AvailableDrinkSizes,
         extrasVM.AvailableFlavorings,
         extrasVM.AvailableTemperatures));
    _pages = new ReadOnlyCollection<CoffeeWizardPageViewModelBase>(pages);
}

VB.NET

Public ReadOnly Property Pages() As ReadOnlyCollection(Of CoffeeWizardPageViewModelBase)
    Get
        If _objPages Is Nothing Then
            Me.CreatePages()
        End If
        Return _objPages
    End Get
End Property

Private Sub CreatePages()
    Dim objTypeSizePageViewModel As New CoffeeTypeSizePageViewModel(Me.CupOfCoffee)
    Dim objExtrasPageViewModel As New CoffeeExtrasPageViewModel(Me.CupOfCoffee)
    Dim obj As New List(Of CoffeeWizardPageViewModelBase)
    With obj
        .Add(New WelcomePageViewModel)
        .Add(objTypeSizePageViewModel)
        .Add(objExtrasPageViewModel)
        .Add(New CoffeeSummaryPageViewModel( _
             objTypeSizePageViewModel.AvailableBeanTypes, _
             objTypeSizePageViewModel.AvailableDrinkSizes, _
             objExtrasPageViewModel.AvailableFlavorings, _
             objExtrasPageViewModel.AvailableTemperatures))
    End With
    _objPages = New ReadOnlyCollection(Of CoffeeWizardPageViewModelBase)(obj)
End Sub

The read-only list of steps on the left side of the Wizard displays an item for each page in the workflow. That list is implemented as an ItemsControl whose ItemsSource property is bound to the Pages property of CoffeeWizardViewModel. The ItemsControl has an ItemTemplate assigned to it, which renders each item and highlights the item that corresponds to the current page in view. When running the application in German, it looks something like this:

WizardStepListing.png

An abridged version of the markup for that list of pages, contained in CoffeeWizardView.xaml, is seen below:

<DataTemplate x:Key="wizardStepTemplate">
  <Border x:Name="bdOuter" Opacity="0.25">
    <Border x:Name="bdInner" Background="#FFFEFEFE">
      <TextBlock x:Name="txt" Text="{Binding Path=DisplayName}" />
    </Border>
  </Border>
  <DataTemplate.Triggers>
    <DataTrigger Binding="{Binding Path=IsCurrentPage}" Value="True">
      <Setter
        TargetName="txt"
        Property="FontWeight"
        Value="Bold"
        />
      <Setter
        TargetName="bdInner"
        Property="Background"
        Value="BurlyWood" 
        />
      <Setter
        TargetName="bdOuter"
        Property="Opacity"
        Value="1" 
        />
    </DataTrigger>
  </DataTemplate.Triggers>
</DataTemplate>

<ItemsControl 
  ItemsSource="{Binding Path=Pages}" 
  ItemTemplate="{StaticResource wizardStepTemplate}" 
  />

The relationships between the ViewModel classes seen thus far are shown in the following diagram:

WizardViewModelsDiagram.png

Displaying and Navigating the Pages

CoffeeWizardViewModel exposes two properties of type ICommand that allow the CoffeeWizardView control to provide the user with a means of navigating between the pages. Those properties are called MoveNextCommand and MovePreviousCommand. When either of these commands execute, the CurrentPage property is assigned whichever Page ViewModel should currently be in view.

CoffeeWizardView displays the appropriate View for the current Page ViewModel because it contains a HeaderedContentControl whose Content property is bound to the CurrentPage property. When the HeaderedContentControl’s Content property is set to a ViewModel object, a typed DataTemplate, whose DataType matches the “current” ViewModel type, is used to provide the correct View for visualizing that ViewModel instance. The HeaderedContentControl in CoffeeWizardView is declared like this:

<HeaderedContentControl 
  Content="{Binding Path=CurrentPage}" 
  Header="{Binding Path=CurrentPage.DisplayName}" 
  />

These four templates are inside the <CoffeeWizardView.Resources> collection:

<!-- These four templates map a ViewModel to a View. -->
<DataTemplate DataType="{x:Type viewModel:WelcomePageViewModel}">
  <view:WelcomePageView />
</DataTemplate>

<DataTemplate DataType="{x:Type viewModel:CoffeeTypeSizePageViewModel}">
  <view:CoffeeTypeSizePageView />
</DataTemplate>

<DataTemplate DataType="{x:Type viewModel:CoffeeExtrasPageViewModel}">
  <view:CoffeeExtrasPageView />
</DataTemplate>

<DataTemplate DataType="{x:Type viewModel:CoffeeSummaryPageViewModel}">
  <view:CoffeeSummaryPageView />
</DataTemplate>

The following code comes from CoffeeWizardViewModel. It shows how the MoveNextCommand decides if it can execute, and what happens when it executes.

C#

public ICommand MoveNextCommand
{
    get
    {
        if (_moveNextCommand == null)
            _moveNextCommand = new RelayCommand(
                () => this.MoveToNextPage(),
                () => this.CanMoveToNextPage);
        return _moveNextCommand;
    }
}

bool CanMoveToNextPage
{
    get { return this.CurrentPage != null && this.CurrentPage.IsValid(); }
}

void MoveToNextPage()
{
    if (this.CanMoveToNextPage)
    {
        if (this.CurrentPageIndex < this.Pages.Count - 1)
            this.CurrentPage = this.Pages[this.CurrentPageIndex + 1];
        else
            this.OnRequestClose();
    }
}

VB.NET

Public ReadOnly Property MoveNextCommand() As ICommand
    Get
        If _cmdMoveNextCommand Is Nothing Then
            _cmdMoveNextCommand = New RelayCommand( _
                AddressOf ExecuteMoveNext, _
                AddressOf CanExecuteMoveNext)
        End If
        Return _cmdMoveNextCommand
    End Get
End Property

Private Function CanExecuteMoveNext(ByVal param As Object) _
    As Boolean
    If Me.CurrentPage IsNot Nothing Then
        Return Me.CurrentPage.IsValid
    Else
        Return False
    End If
End Function

Private Sub ExecuteMoveNext(ByVal parm As Object)
    If CanExecuteMoveNext(Nothing) Then
        Dim intIndex As Integer = Me.CurrentPageIndex
        If intIndex < Me.Pages.Count - 1 Then
            Me.CurrentPage = Me.Pages(intIndex + 1)
        Else
            OnRequestClose()
        End If
    End If
End Sub

The CoffeeWizardView control makes use of the MoveNextCommand property by binding a button’s Command property to it, as seen below:

<Button
  Grid.Column="1"
  Grid.Row="0"
  Command="{Binding Path=MoveNextCommand}"
  Style="{StaticResource moveNextButtonStyle}" 
  />

Introducing the Data Model

The demo application has two projects in it. The CoffeeLibrary project contains Model type definitions. The CupOfCoffee class contains the business data and price calculation logic upon which the rest of the application depends. Granted, the price calculation logic is rather crude and simplistic, but that is irrelevant for the purposes of this demonstration. The following diagram shows the types contained in the business library:

ModelDiagram.jpg

CupOfCoffee implements the INotifyPropertyChanged interface because when any of the properties are set, it could potentially cause the object’s Price property to change. CoffeeWizardView makes use of this when it binds a TextBlock’s Text property to the Price property, in order to show the current price of the user’s cup of coffee. That is the only place where the View directly accesses the Model. In all other cases, the View always binds to the ViewModel objects that wrap the underlying CupOfCoffee instance. We could have made the ViewModel re-expose the Price property and raise its PropertyChanged event when the CupOfCoffee does, but there was no distinct advantage in doing so.

All of the ViewModel classes have a reference to a CupOfCoffee object, though not all of them necessarily make use of it. To be more specific, the Welcome page and Summary page do not need to access the CupOfCoffee object created by the user. For a high-level understanding of how the Views, ViewModels, and Model classes relate to each other, consult the following diagram:

Entities.jpg

Presenting Options via OptionViewModel

The pages that present a list of options to the user make use of the RadioButton and CheckBox controls to provide either mutually exclusive or multiple choice lists, respectively. Each option in a list corresponds to a value from an enumeration defined in the CoffeeLibrary assembly, such as the BeanType enum. It might be tempting to display the name of each enum value in the UI, but that would prevent the UI from being localizable. Also, to facilitate consistent use of the MVVM pattern throughout the application, you should not set the Content property of the RadioButton/CheckBox to an enum value because in doing so, you create a tight coupling between the ViewModel and its associated View.

Our solution to this problem lies in the use of a ViewModel class that represents an option in the View. To that end, we created the OptionViewModel<TValue> class. It is a small type whose sole purpose is to create a UI-friendly wrapper around an option displayed in a list. It allows you to specify a localized display name for the option, the underlying value that it represents, and an optional value used to control the order in which the options are sorted, and subsequently displayed. In addition, it also has an IsSelected property to which the IsChecked property of a RadioButton or CheckBox can be bound. The following diagram shows OptionViewModel<TValue> and how it is used by other ViewModel classes:

OptionViewModelDiagram.jpg

The CoffeeTypeSizePageViewModel class has two properties that make use of this type: AvailableBeanTypes and AvailableDrinkSizes. The AvailableDrinkSizes property, and associated methods, is listed below:

C#

/// <summary>
/// Returns a read-only collection of all drink sizes that the user can select.
/// </summary>
public ReadOnlyCollection<OptionViewModel<DrinkSize>> AvailableDrinkSizes
{
    get
    {
        if (_availableDrinkSizes == null)
            this.CreateAvailableDrinkSizes();
        return _availableDrinkSizes;
    }
}

void CreateAvailableDrinkSizes()
{
    var list = new List<OptionViewModel<DrinkSize>>();

    list.Add(new OptionViewModel<DrinkSize>(
        Strings.DrinkSize_Small, DrinkSize.Small, 0));
    list.Add(new OptionViewModel<DrinkSize>(
        Strings.DrinkSize_Medium, DrinkSize.Medium, 1));
    list.Add(new OptionViewModel<DrinkSize>(
        Strings.DrinkSize_Large, DrinkSize.Large, 2));

    foreach (OptionViewModel<DrinkSize> option in list)
        option.PropertyChanged += this.OnDrinkSizeOptionPropertyChanged;

    list.Sort();

    _availableDrinkSizes = 
        new ReadOnlyCollection<OptionViewModel<DrinkSize>>(list);
}

void OnDrinkSizeOptionPropertyChanged(
    object sender, PropertyChangedEventArgs e)
{
    var option = sender as OptionViewModel<DrinkSize>;
    if (option.IsSelected)
        this.CupOfCoffee.DrinkSize = option.GetValue();
}

VB.NET

Public ReadOnly Property AvailableDrinkSizes() As _
    ReadOnlyCollection(Of OptionViewModel(Of DrinkSize))
    Get
        If _objAvailableDrinkSizes Is Nothing Then
            CreateAvailableDrinkSizes()
        End If
        Return _objAvailableDrinkSizes
    End Get
End Property

Private Sub CreateAvailableDrinkSizes()
    Dim obj As New List(Of OptionViewModel(Of DrinkSize))
    With obj
        .Add(New OptionViewModel(Of DrinkSize) _
             (My.Resources.Strings.DrinkSize_Small, _
              DrinkSize.Small, 0))
        
        .Add(New OptionViewModel(Of DrinkSize) _
             (My.Resources.Strings.DrinkSize_Medium, _
              DrinkSize.Medium, 1))
        .Add(New OptionViewModel(Of DrinkSize) _
             (My.Resources.Strings.DrinkSize_Large, _
              DrinkSize.Large, 2))
    End With

    For Each objOption As OptionViewModel(Of DrinkSize) In obj
        AddHandler objOption.PropertyChanged, _
            AddressOf OnDrinkSizeOptionPropertyChanged
    Next

    obj.Sort()

    _objAvailableDrinkSizes = _
      New ReadOnlyCollection(Of OptionViewModel(Of DrinkSize))(obj)
End Sub

Private Sub OnDrinkSizeOptionPropertyChanged( _
    ByVal sender As Object, ByVal e As PropertyChangedEventArgs)
    Dim obj As OptionViewModel(Of DrinkSize) = _
        CType(sender, OptionViewModel(Of DrinkSize))
    If obj.IsSelected Then
        Me.CupOfCoffee.DrinkSize = obj.GetValue
    End If
End Sub

The value contained in an OptionViewModel<TValue> is accessible by calling its GetValue method. We decided to expose this as a method, instead of a property, so that the UI cannot easily bind to it. Elements in a View should have their properties bound to the DisplayName property, to ensure that the localized string is shown. The following XAML from CoffeeTypeSizePageView shows how this binding is created:

<ItemsControl ItemsSource="{Binding Path=AvailableDrinkSizes}">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <RadioButton
        Content="{Binding Path=DisplayName}"
        IsChecked="{Binding Path=IsSelected}"
        GroupName="DrinkSize"
        Margin="2,3.5"
        />
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

The list of RadioButtons is implemented as an ItemsControl with an ItemTemplate that generates a RadioButton for each item. This approach is more desirable, in most cases, than having the RadioButtons statically be declared in XAML, because it eliminates the need for redundant markup. In a more dynamic scenario, where the list of options might come from a database or Web Service, this approach is invaluable because it places no constraints on how many options can be displayed.

Internationalizing the Wizard

As mentioned in the Background section of this article, to globalize an application’s user interface is to make it ready and able to work properly in various cultures and languages. There have been many suggested approaches in the WPF community to accomplish this. The simple technique shown in this article has been used in real production WPF applications, and is known to be quick and reliable.

In this article, we only work with languages that read from left to right, and do not have locale-specific business logic. Those topics, while interesting and important, fall outside the scope of this article.

The Wizard is fully globalized, so that it can display its text in any language for which localized strings are available. The process of globalizing the application’s UI can be broken into two pieces: putting all of the default display text into resource files, and consuming those strings from the application. Localizing the application consists of having translators create language-specific resource files that contain translations of the original resource strings.

A resource file is commonly referred to as a “RESX” because that is the file extension used for resource files in Visual Studio. RESX’s have been around for years in the .NET world, and are the foundation of creating internationalized applications in Windows Forms and ASP.NET. Visual Studio has a RESX editor that allows you to work with many different kinds of resources. The RESX editor looks something like this:

ResxEditor.jpg

Every time you edit a RESX file in your project, Visual Studio updates an auto-generated class that allows you to easily access the resources it contains. The name of the class matches the name of the RESX file. When you access the static (Shared in VB.NET) members of that class, it retrieves the localized value for that property, based on the current culture of the thread in which it runs. For instance, if Thread.CurrentThread.CurrentCulture references a CultureInfo object that represents French as spoken in Switzerland (culture code: “fr-CH”) and you have a resource file with resources for that culture, then the static property will return the localized resource value for Swiss French.

Accessing the localized strings from code is very easy. Here is the DisplayName property from the CoffeeSummaryPageViewModel class, which is used to show a header above the Summary page when it is in view.

C#

public override string DisplayName
{
    get { return Strings.PageDisplayName_Summary; }
}

VB.NET

Public Overrides ReadOnly Property DisplayName() As String
    Get
        Return My.Resources.Strings.PageDisplayName_Summary
    End Get
End Property

In the demo application, the RESX file base name is Strings.resx, which is why the auto-generated class is named Strings. When I mention that the RESX file base name is Strings.resx, that means that all localized RESX files include their culture code in the file name, such as Strings.fr-CH.resx for the Swiss French version of the file. This is a required naming convention for the resource system to properly locate and load the localized resources at run time.

Accessing the localized strings from XAML is easy, too. However, it is critically important that you perform one step on the project’s default RESX files before you try to access their auto-generated classes from markup. By default, the auto-generated class for a RESX file is marked as internal (Friend in VB.NET), but we need it to be public instead. In order to have the class be created as public, you must select the RESX file in Solution Explorer and view its properties in the Properties window. You must change the “Custom Tool” property from “ResXFileCodeGenerator” to “PublicResXFileCodeGenerator”. This critical step is shown below:

ResxCustomToolConfiguration.jpg

Once that step is complete, it’s easy to consume the resource strings from XAML. First, add an XML namespace alias for the namespace that contains the auto-generated resource class. Then, simply use the {x:Static} markup extension as the value of a property setting. The following XAML snippet shows how the ApplicationMainWindow’s Title is set to a localized string:

<Window 
  x:Class="GlobalizedWizard.ApplicationMainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:res="clr-namespace:GlobalizedWizard.Resources"
  Title="{x:Static res:Strings.ApplicationMainWindow_Title}"
  >

There is one more trick to be aware of that makes internationalization possible in WPF apps. For whatever reason, WPF does not make use of the current culture in Bindings. In order for the current culture to be used by Bindings, such as getting the correct currency symbol when setting a Binding’s StringFormat property to “c” (the currency format string), you must run this line of code as the application first loads up:

C#

// Ensure the current culture passed into bindings 
// is the OS culture. By default, WPF uses en-US 
// as the culture, regardless of the system settings.
FrameworkElement.LanguageProperty.OverrideMetadata(
  typeof(FrameworkElement),
  new FrameworkPropertyMetadata(
      XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));

VB.NET

FrameworkElement.LanguageProperty.OverrideMetadata( _
    GetType(FrameworkElement), _
    New FrameworkPropertyMetadata( _
    System.Windows.Markup.XmlLanguage.GetLanguage( _
    CultureInfo.CurrentCulture.IetfLanguageTag)))

Testing the Wizard in Different Cultures

If you would like to see what the demo application looks like when running in other cultures, open the App.xaml.cs file (or the Application.xaml.vb file) and uncomment one of the lines of code that creates a CultureInfo object. This ensures that the current thread uses a culture for which localized resource strings are available.

References

Revision History

  • December 17, 2008 - Published the article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here