Running with Code Like with scissors, only more dangerous

3Apr/080

Localizing a WPF Application with ResX Files

Posted by Rob

As I mentioned during my rant earlier this week, localization is fairly difficult with WPF.  As it stands right now, the only real tool support Microsoft offers for localization is a tool called LocBaml.exe, which they clearly mark as "not production-ready."  Samples available online describe how to utilize it, but there are significant drawbacks to its current implementation.  First, since LocBaml generates CSV files, it is difficult or impossible to localize most anything other than simple strings; images, for instance, are impossible to embed into a CSV file.  ResX, an XML-based file format, has not only been incredibly strong on this front, but in fact is an excellent choice because of its rich tool support available in the Visual Studio IDE.

I won't go into the depths of how LocBaml is used for localizing a WPF application; the linked sample on CodeProject and the MSDN site should give you a fairly strong idea of how it's implemented.  But here is a bulleted list of its major drawbacks.

Drawbacks to using LocBaml for WPF Localization:

  • Localization is done per-XAML-file and not per resource such as string or image.
  • Localized resources are not strongly-typed; rather, they are stored as MemoryStreams:
    A resource assembly loaded into Reflector
  • Only string resources can be easily localized.
  • Localization takes place out of the IDE and away from the localized task; generating the CSV file is done after the initial assembly is compiled and then the satellite assemblies are generated later.
  • Assemblies with strong name signatures may need to be delay-signed, waiting until after the resource modules have been generated.  (I'm not 100% certain about this).

Bending XAML to Your Localization Will

It turns out that XAML already has the perfect tool set for your application: Attached Properties.  By setting attached properties with specific IDs, we can generate markup that cleanly pulls in localization resources.  Here's a sample XAML file that demonstrates this approach:

   1:  <Window x:Class="LocalizationTestTake3.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      xmlns:loc="clr-namespace:LocalizationTestTake3"
   5:      Title="Window1" Height="300" Width="300">
   6:      <Grid>
   7:          <Button Height="45" Margin="55,82,75,0" loc:LocalizationProvider.ContentID="HelloWorld" Name="button1" VerticalAlignment="Top"></Button>
   8:          <Button Height="23" Margin="94,178,109,0" loc:LocalizationProvider.ContentID="English" Name="button2" VerticalAlignment="Top" Click="button2_Click"></Button>
   9:          <Button Height="23" Margin="94,0,109,24" loc:LocalizationProvider.ContentID="Spanish" Name="button3" VerticalAlignment="Bottom" Click="button3_Click"></Button>
  10:          <TextBox Height="23" Margin="55,14,59,0" loc:LocalizationProvider.TextID="HowAreYou" Name="textBox1" VerticalAlignment="Top" />
  11:      </Grid>
  12:  </Window>

In this example, you can see the attached properties defined by the LocalizationProvider class.  The IDs specified in the ContentID and TextID properties are actually defined within my .resx files.  The neat thing about this approach is that it includes full designer support, in both Visual Studio 2008 and Blend 2.5:

image

The application is able to switch at runtime by changing the Thread.CurrentThread.CurrentCulture property and then calling LocalizationProvider.UpdateAllControls().  Here's part of the class, and then I'll discuss it:

   1:      public static class LocalizationProvider 
   2:      {
   3:          private static List<DependencyObject> _contentObjects;
   4:   
   5:          static LocalizationProvider()
   6:          {
   7:              _contentObjects = new List<DependencyObject>();
   8:          }
   9:   
  10:          public static void UpdateAllObjects()
  11:          {
  12:              foreach (DependencyObject obj in _contentObjects)
  13:              {
  14:                  ResourceManager res = Resources.ResourceManager;
  15:                  ContentControl cctrl = obj as ContentControl;
  16:                  if (cctrl == null)
  17:                      throw new InvalidCastException(string.Format("Type '{0}' does not derive from type 'ContentControl'.", obj.GetType().FullName));
  18:   
  19:                  string key = obj.GetValue(ContentIDProperty) as string;
  20:   
  21:                  string resourceValue = res.GetString(key, CultureInfo.CurrentCulture);
  22:   
  23:                  cctrl.SetValue(ContentControl.ContentProperty, resourceValue);
  24:              }
  25:          }
  26:   
  27:          #region ContentID property
  28:          public static string GetContentID(DependencyObject obj)
  29:          {
  30:              return (string)obj.GetValue(ContentIDProperty);
  31:          }
  32:   
  33:          public static void SetContentID(DependencyObject obj, string value)
  34:          {
  35:              obj.SetValue(ContentIDProperty, value);
  36:          }
  37:   
  38:          private static void OnContentIDChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  39:          {
  40:              if (obj == null)
  41:                  throw new ArgumentNullException("obj");
  42:   
  43:              ResourceManager res = Resources.ResourceManager;
  44:              ContentControl cctrl = obj as ContentControl;
  45:              if (cctrl == null)
  46:                  throw new InvalidCastException(string.Format("Type '{0}' does not derive from type 'ContentControl'.", obj.GetType().FullName));
  47:   
  48:   
  49:              string resourceValue = null;
  50:              try
  51:              {
  52:                  resourceValue = res.GetString(e.NewValue as string, CultureInfo.CurrentCulture);
  53:              }
  54:              catch (Exception ex) { }
  55:   
  56:              if (resourceValue != null)
  57:              {
  58:                  cctrl.SetValue(ContentControl.ContentProperty, resourceValue);
  59:                  if (!_contentObjects.Contains(obj))
  60:                      _contentObjects.Add(obj);
  61:              }
  62:   
  63:          }
  64:   
  65:          public static DependencyProperty ContentIDProperty =
  66:              DependencyProperty.RegisterAttached("ContentID", typeof(string), typeof(LocalizationProvider),
  67:              new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(OnContentIDChanged)));
  68:          #endregion
  69:      }

This part of the class tracks objects that use the ContentID attached property; this property is set on controls that use the Content property (any controls that inherit from ContentControl).  I'll explain why this is important in a little while.

First, we declare the ContentIDProperty as a DependencyProperty.  We tell the framework that it affects the arrangement of controls to which it is attached, and that it should call the method OnContentIDChanged when it is changed for a control.  That's down at the bottom.  We also then provide the methods for GetContentID and SetContentID - these are like extension methods for the specific dependency property we're creating.  (For more information about how a dependency property is created, see the related MSDN article).  The meat and potatoes of the class is in the OnContentIDChanged method - it looks at the new value, searches for a resource with the given ID, and then sets the control's Content dependency property to the value of the resource.  (Note: the current implementation also only works with strings.  However, changing the type to Object and then pulling the value out would work for other types of resources as well).  It then saves the DependencyObject to a list of controls that have been localized.

The UpdateAllControls iterates over each localized control and rebinds its value based on its type, using much the same logic as the initial method.

Caveats

There are a few difficulties introduced by using this method:

  • If you set the Content property of a control (by specifying internal text or by using the attribute markup syntax), you run the risk of overriding the attached property that we defined here.  For example, this markup:

    <Button Height="45" Margin="55,82,75,0" loc:LocalizationProvider.ContentID="HelloWorld" Name="button1" VerticalAlignment="Top">Hey there, world!</Button>

    would override the localized text.  This is because of the order the XAML parser loads and binds dependency properties -- remember that the Content property is also a dependency property.  I've tried a lot of different techniques for fixing this, but thus far can't find any.

  • It requires the xmlns: namespace declaration in each XAML file in which it's used.
  • Different controls and different dependency properties require different localization ID properties.  For example, TextBlocks and Runs, which have a Text property but not a Content property, need to use a different localization handler.  That involves changing the UpdateAllControls method to account for new dependency property callback types as well.
  • For very large applications, updating all controls may take longer.
  • The Text property, which is not defined on a common base class, uses reflection to set the dependency property.  The way that the set method is invoked is slower than calling the method or property setter directly.
  • Because ResX files are bound per-assembly, it can be difficult to centralize the LocalizationProvider class.  However, if it is included as an internal class in each assembly in which it is used, it can be worked around.  Another option is to use base classes, although since it's a static class, this could create a more difficult implementation.

Conclusion

For all of the potential difficulties, we like this approach better for a number of reasons:

  • It provides designer support.  You can even set the dependency properties in Blend.
  • It has richer tool support and an established file format.
    • Existing localization investments don't go away.
  • It doesn't rely on binding syntax.
    • Binding syntax is fine, and is described in the CodeProject article linked above as a convenient way to use ResX files.  However, they require editing in XAML directly; this does not.
  • It allows the runtime, dynamic modification of the UI culture.

I'll be posting a more complete sample project soon, so if this interests you, stay tuned!

28Mar/080

Why doesn’t Dispatcher implement ISynchronizeInvoke?

Posted by Rob

This is a rant.  I don't have the answer to the question.

So, the latest project I'm working on is a kiosk app in WPF that has to interact with hardware.  The hardware has various needs; some of it I need to poll, and others I check on status every given interval.  Every class I've created for interacting with the hardware has a SynchronizingObject property, just like the System.Timers.Timer class, and I was pretty happy with myself when I figured out that my event raising implementation was the same as that class's.

The SynchronizingObject property looks like this:

   1:  public ISynchronizeInvoke SynchronizingObject
   2:  {
   3:      get { return m_syncObj; }
   4:      set { m_syncObj = value; } 
   5:  }

Pretty straightforward.  To call an event it might be something like this:

   1:  protected virtual void OnStatusChanged(EventArgs e)
   2:  {
   3:      if (StatusChanged != null)
   4:      {
   5:          if (m_syncObj == null || !m_syncObj.InvokeRequired)
   6:              StatusChanged(this, e);
   7:          else
   8:              m_syncObj.BeginInvoke(StatusChanged, new object[] { this, e });
   9:      }
  10:  }

Easy?  Good.

Well, like apparently everything straightforward about Windows Forms programming, it's been changed for Windows Presentation Foundation.  Each Visual element has an associated Dispatcher; Dispatchers are created per-thread, and like in Windows Forms, you can't update a Visual or its descendent tree from another thread.  And the Dispatcher class almost looks like it would be interface-compatible with ISynchronizeInvoke with a couple exceptions.  I thought, "great!  I'll be able to just create a simple Adapter class!"  Nope.  Well, it wasn't simple, that's for sure.

Let's take a look at the overload list for Invoke and you might see why.  Invoked delegates either take one argument or one argument plus a params list of arguments.  Then you think.... what?

There are some issues with params lists.  For example, let's say that you're passing an argument that is an object[].  Should that get expanded?  Consider this code:

   1:  void DoStuff(params object[] args) { ... }
   2:   
   3:  // now within a function
   4:  object[] values = new object[] { 1, "stuff", 25.0 };
   5:  DoStuff(values, "something else?");

The kind of difficulty we run into is -- should "values" be expanded out or should it stay as an array?  In other words, should args be { 1, "stuff", 25.0, "something else?" }, or should it be { { 1, "stuff", 25.0 }, "something else?" }?  What about when it's the only argument passed - what if DoStuff(values) is the call?  Then should args be {1, "stuff", 25.0} or { {1, "stuff", 25.0} }?  For the purposes of this application, I assumed that, in the first case, args would be like the latter; and in the second case, args would be like the former.

So here's my test app -- it's a basic WPF application.  Here's Window1.xaml:

   1:  <Window x:Class="DispatcherTest.Window1"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:      Title="Window1" Height="300" Width="300">
   5:      <Grid>
   6:          <Button Height="23" Margin="102,87,100,0" Name="button1" VerticalAlignment="Top" Click="button1_Click">Button</Button>
   7:      </Grid>
   8:  </Window>

and Window1.xaml.cs:

   1:  public partial class Window1 : Window
   2:  {
   3:      private ISynchronizeInvoke m_invoker;
   4:   
   5:      public Window1()
   6:      {
   7:          InitializeComponent();
   8:          m_invoker = new DispatcherWinFormsCompatAdapter(this.Dispatcher));
   9:      }
  10:   
  11:      private void button1_Click(object sender, RoutedEventArgs e)
  12:      {
  13:          ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeLongRunningWork), null);
  14:      }
  15:   
  16:      private void DoSomeLongRunningWork(object state)
  17:      {
  18:          Thread.Sleep(2000);
  19:          EventHandler updateButton = delegate(object sender, EventArgs e)
  20:          {
  21:              button1.Content = "Clicked!";
  22:          };
  23:   
  24:          if (m_invoker.InvokeRequired)
  25:          {
  26:              m_invoker.BeginInvoke(updateButton, new object[] { this, EventArgs.Empty });
  27:          }
  28:          else
  29:          {
  30:              updateButton(this, EventArgs.Empty);
  31:          }
  32:      }
  33:  }

Finally, here's my skeleton adapter class.  Note that EndInvoke and Invoke are not used so I don't implement them yet:

   1:  internal class DispatcherWinFormsCompatAdapter : ISynchronizeInvoke
   2:  {
   3:      #region IAsyncResult implementation
   4:      private class DispatcherAsyncResultAdapter : IAsyncResult
   5:      {
   6:          private DispatcherOperation m_op;
   7:          private object m_state;
   8:   
   9:          public DispatcherAsyncResultAdapter(DispatcherOperation operation)
  10:          {
  11:              m_op = operation;
  12:          }
  13:   
  14:          public DispatcherAsyncResultAdapter(DispatcherOperation operation, object state)
  15:              : this(operation)
  16:          {
  17:              m_state = state;
  18:          }
  19:   
  20:          public DispatcherOperation Operation
  21:          {
  22:              get { return m_op; }
  23:          }
  24:   
  25:          #region IAsyncResult Members
  26:   
  27:          public object AsyncState
  28:          {
  29:              get { return m_state; }
  30:          }
  31:   
  32:          public WaitHandle AsyncWaitHandle
  33:          {
  34:              get { return null; }
  35:          }
  36:   
  37:          public bool CompletedSynchronously
  38:          {
  39:              get { return false; }
  40:          }
  41:   
  42:          public bool IsCompleted
  43:          {
  44:              get { return m_op.Status == DispatcherOperationStatus.Completed; }
  45:          }
  46:   
  47:          #endregion
  48:      }
  49:      #endregion
  50:      private Dispatcher m_disp;
  51:      public DispatcherWinFormsCompatAdapter(Dispatcher dispatcher)
  52:      {
  53:          m_disp = dispatcher;
  54:      }
  55:      #region ISynchronizeInvoke Members
  56:   
  57:      public IAsyncResult BeginInvoke(Delegate method, object[] args)
  58:      {
  59:          if (args != null && args.Length > 1)
  60:          {
  61:              object[] argsSansFirst = GetArgsAfterFirst(args);
  62:              DispatcherOperation op = m_disp.BeginInvoke(DispatcherPriority.Normal, method, args[0], argsSansFirst);
  63:              return new DispatcherAsyncResultAdapter(op);
  64:          }
  65:          else
  66:          {
  67:              if (args != null)
  68:              {
  69:                  return new DispatcherAsyncResultAdapter(m_disp.BeginInvoke(DispatcherPriority.Normal, method, args[0]));
  70:              }
  71:              else
  72:              {
  73:                  return new DispatcherAsyncResultAdapter(m_disp.BeginInvoke(DispatcherPriority.Normal, method));
  74:              }
  75:          }
  76:      }
  77:   
  78:      private static object[] GetArgsAfterFirst(object[] args)
  79:      {
  80:          object[] result = new object[args.Length - 1];
  81:          Array.Copy(args, 1, result, 0, args.Length - 1);
  82:          return result;
  83:      }
  84:   
  85:      public object EndInvoke(IAsyncResult result)
  86:      {
  87:          DispatcherAsyncResultAdapter res = result as DispatcherAsyncResultAdapter;
  88:          if (res == null)
  89:              throw new InvalidCastException();
  90:   
  91:          while (res.Operation.Status != DispatcherOperationStatus.Completed || res.Operation.Status == DispatcherOperationStatus.Aborted)
  92:          {
  93:              Thread.Sleep(50);
  94:          }
  95:   
  96:          return res.Operation.Result;
  97:      }
  98:   
  99:      public object Invoke(Delegate method, object[] args)
 100:      {
 101:          if (args != null && args.Length > 1)
 102:          {
 103:              object[] argsSansFirst = GetArgsAfterFirst(args);
 104:              return m_disp.Invoke(DispatcherPriority.Normal, method, args[0], argsSansFirst);
 105:          }
 106:          else
 107:          {
 108:              if (args != null)
 109:              {
 110:                  return m_disp.Invoke(DispatcherPriority.Normal, method, args[0]);
 111:              }
 112:              else
 113:              {
 114:                  return m_disp.Invoke(DispatcherPriority.Normal, method);
 115:              }
 116:          }
 117:      }
 118:   
 119:      public bool InvokeRequired
 120:      {
 121:          get { return m_disp.Thread != Thread.CurrentThread; }
 122:      }
 123:   
 124:      #endregion
 125:  }

All told, I'm still not sure that this will work with delegates with more than two parameters.  I'm still concerned about params argument binding -- but what can I do?  Reflection.Emit custom function calls for n parameters?  No thanks.

Microsoft guy who made System.Windows.Threading - for .NET 4.0, how about making Dispatcher implement ISynchronizeInvoke, hm?  Thanks!

Tagged as: , No Comments