in

Dé specialist in .NET trainingen en consultancy

Thomas Huijer

december 2008 - Posts

  • Easy custom localization in WPF

    One of our customers wanted the application that we're working on to be localized in different languages. Fortunately all that was required was localization of texts and not of positions, heights or sizes. But an important requirement was also that changes needed to be made easily by people without a degree in software-engineering, i.e. users who were able to use Office, but not much else. So we decided against the use of LocBaml and see if we could come up with a simpler approach. Here's what we came up with:

    1. we're using an Excel sheet as the basis for the translations. Each translatable text would be given a key and five translations for Dutch, English, Spanish, German and Italian.
    2. the Excel-sheet is put into our version control system TFS
    3. in our FinalBuilder script we're doing a Get Latest Version to make sure we're using the latest translations
    4. although the entire product was written in C#, we're using a VB.NET tool to convert the Excel-sheet to a serialized list of (custom) Translation objects
    5. in our XAML we're using a MarkupExtension to fetch the required translated text.

    Why did we use VB.NET to create the conversion from Excel to a serialized list?

    I personally have more affection for C# than for VB.NET, which is primarily driven by my strong Delphi background. I have nothing against VB: it certainly has a place in the developer community. The beauty of .NET is the freedom of choice: the choice for a specific language. The language you feel most comfortable in. If you ever catch me bashing on VB, it's on VB6 and earlier. VB.NET is a language that is as strong as C# is. It's just a matter of what you like best. I often compare it to cars. What's the better car: a BMW or a Mercedes? It depends on who you ask (if you ask me, I'd say a BMW, but that's an entirely different story)

    Some people might argue that it's bad to use both VB.NET and C# in the same solution, but I don't see an issue with that for this case. The import program is really small and any C# programmer can read (and write although a bit slower) VB.NET. Most often, it's a matter of taste: most C# developers just don't like it. Anyway, the real reason we chose VB.NET here is the support for default parameters. Something which is really useful when talking to a COM-based application. But soon, well, about a year from now probably, C# will also have named and default parameters in C# 4.0.

    So, the import program is pretty simple:
     

          For row As Integer = 2 To 2000

            Dim range As Excel.Range = xlws.Range(String.Format("A{0}:F{0}", row))

     

            Dim key As String = range.Value2(1, 1)

            Dim english As String = range.Value2(1, 2)

            Dim dutch As String = range.Value2(1, 3)

            Dim german As String = range.Value2(1, 4)

            Dim spanish As String = range.Value2(1, 5)

            Dim italian As String = range.Value2(1, 6)

     

            If Not String.IsNullOrEmpty(key) And key <> "-" Then

              If keys.Contains(key) Then

                doubles.AppendLine(key)

              Else

                Dim trans As New Translation(key, dutch, english, german, italian, spanish)

                list.Add(trans)

                keys.Add(key)

              End If

            End If

          Next

     

          Dim serializer As New Serialization.XmlSerializer(GetType(List(Of Translation)))

     

          File.Delete("translations.xml")

          Dim fs As New FileStream("translations.xml", FileMode.CreateNew, FileAccess.Write)

     

          serializer.Serialize(fs, list)

          fs.Close()

          xl.Quit()

     

    Really straightforward, he? Really quick and dirty. I'm really against Q&D, but in this case it's just a waste to spend more time on it then needed. It gets the job done and it's highly unlikely that we'll ever look at this code again. Sometimes we have to sacrifice maintainability to get more productive. The real trick is to know when to do this and when to go for the better solution.

    MarkupExtensions

    But the real trick was to use theses translations in XAML. To accomplish that we wrote a MarkupExtension as said before. MarkupExtension are what's in curly braces in XAML, like this:

    <Grid Width="500"
            Margin="5"
            DataContext="{Binding ElementName=entryListBox, Path=SelectedItem}" >

    <TextBlock Text="This is Active caption color" Background="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"/>

     

    MarkupExtension are, of course i'd say, classes. And you can derive from this abstract class. The MarkupExtension class itself is pretty simple:

    using System;

     

    namespace System.Windows.Markup

    {

      public abstract class MarkupExtension

      {

        protected MarkupExtension();

        public abstract object ProvideValue( IServiceProvider serviceProvider );

      }

    }

     

    The convention is that MarkupExtensions end their name with Extension, but this is not needed to specify in XAML. So the Binding MarkupExtension is really named BindingExtension and the StaticResource MarkupExtension is named StaticResourceExtension. All you have to do in the MarkupExtension is to implement the ProvideValue method. This is our implementation of our LanguageExtension

      [MarkupExtensionReturnType( typeof( string ) )]

      public class LanguageExtension : MarkupExtension

      {

        private object resourceKey;

        public object ResourceKey

        {

          get

          {

            return resourceKey;

          }

          set

          {

            resourceKey = value;

          }

        }

     

        public LanguageExtension( object resourceKey )

        {

          ResourceKey = resourceKey;

        }

     

        public override object ProvideValue( IServiceProvider serviceProvider )

        {

          return LanguageManager.Instance.LocalizedString( resourceKey.ToString() );

        }

      }

     
    The MarkupExtensionReturnType attribute powers the compiler with the knowledge that this MarkupExtension always returns strings, so that it can check if you're only using it at places where a string is expected. The LanguageManager is a singleton that returns the localized text of the given key based on the currently chosen language.

        public LanguageExtension( object resourceKey )

        {

          ResourceKey = resourceKey;

        }


    This constructor makes sure we're required to pass an argument when using the MarkupExtension like this: 

    <Window

      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

      xmlns:lm="clr-namespace:Oosterkamp.Samples"

      x:Class="Oosterkamp.Samples.MainWindow"

      x:Name="MainApp"

            <ToggleButton x:Name="LocalizedButton" Content="{lm:Language LocalizedButtonKey}" />

    </Window>


    We just add the CLR namespace 'lm' at the top of the XAML file with this line of code; xmlns:lm="clr-namespace:Oosterkamp.Samples" . Then anywhere where we want a localized text we use ="{lm:Language LocalizedButtonKey} to get the localized text for the given key.

    In this particular scenario, this was the best solution we could come up with. People are maintaning the translations in Excel and our automated build process in FinalBuilder makes sure that these translations are used in the next build when the Excel file is checked back into TFS.

  • Applications built for the 3.0 Framework with VS2005 might not compile in VS2005

    Recently we've encountered some problems when compiling a .NET 3.0 application in Visual Studio 2005. The weird thing was that we could compile and run the application on our local machines, but the build computer we used refused to compile the application due to the simple fact that the code wouldn't compile. It called methods that existed, but an overload was used that didn't exist.

    The local computer and the build computer had a 3.0 Framework or higher installed. Both had the same Visual Studio 2005 installed. Both had the same service packs installed. We just couldn't figure out what was different on those machines.

    It turned out that the local computer had only a 3.5 Framework installed. All references in the project were to the 3.0 Framework, so everything compiled just fine. But Intellisense reads the metadata from the actual assebly being referenced, which was on the build computer the 3.5 assemblies. So on the build computer we had these overloads of Dispatcher.Invoke, all coming from the 3.0 assemblies:

    Dispatcher.Invoke (DispatcherPriority, Delegate)
    Dispatcher.Invoke (DispatcherPriority, Delegate, Object)
    Dispatcher.Invoke (DispatcherPriority, TimeSpan, Delegate)
    Dispatcher.Invoke (DispatcherPriority, Delegate, Object, Object[])
    Dispatcher.Invoke (DispatcherPriority, TimeSpan, Delegate, Object)
    Dispatcher.Invoke (DispatcherPriority, TimeSpan, Delegate, Object, Object[])

     

    But on the local machine with only the 3.5 Framework we had these overloads:

    Invoke(DispatcherPriority, Delegate)
    Invoke(Delegate, array<Object>[]()[])
    Invoke(DispatcherPriority, Delegate, Object)
    Invoke(DispatcherPriority, TimeSpan, Delegate)
    Invoke(Delegate, TimeSpan, array<Object>[]()[])
    Invoke(Delegate, DispatcherPriority, array<Object>[]()[])
    Invoke(DispatcherPriority, Delegate, Object, array<Object>[]()[])
    Invoke(DispatcherPriority, TimeSpan, Delegate, Object)
    Invoke(Delegate, TimeSpan, DispatcherPriority, array<Object>[]()[])
    Invoke(DispatcherPriority, TimeSpan, Delegate, Object, array<Object>[]()[])

    So if we used any overload from the latter list that is not in the first list, it'd not compile on the build computer. So, programmer beware: an application built in VS2005 might not compile in VS2005.