Running with Code Like with scissors, only more dangerous

21Apr/080

The Difficulties of Using a Multicast Event-based API

Posted by Rob

I've made allusions to my current project several times, and while I can't discuss it with specifics, but I'm working with hardware; part of the device is an armature that extends to receive a piece of equipment from the user, and then it will once again extend to re-dispense the object at the conclusion of its task.  The armature controller hardware is strictly asynchronous, and as such, I decided initially to write it with asynchronous callbacks.  It's fairly straightfoward to have a method called "Extend" and an event called "Extended."  Once I was able to establish this API, I thought it would be fairly straightforward for the rest of the development team to move forward.

I thought incorrectly.

As .NET developers, I think we're conditioned from the time that we open the IDE to make sure that event handlers are wired up throughout the lifetime of the application.  In fact, both C# and Visual Basic make it so easy for multiple methods to handle events, that it's sometimes silly not to.  C# and Visual Basic event handling syntax is syntactical sugar of the Observer pattern.  And why shouldn't it be?  For probably 99.9% of the applications out there, it's great.

Still, the current project has me a bit miffed.

Consider that there are two instances in which the arm needs to be extended: one to accept the user's item, and the other to re-dispense it.  Dispensing the item happens automatically when the arm is flipped upside-down and then extended.  So, I can track state:

private void Arm_Extended(object sender, EventArgs e)
{
    if (receivingItem)
    {
        // wait for button press
    }
    else // if dispensing item
    {
        Arm.Retract();
    }
}

Doesn't this seem kludgy to you?  If not here, then consider that within a single class I potentially have six objects reporting asynchronous task results that may need to be handled differently depending on the state of the machine.

Unfortunately, the alternative method seems just as kludgy:

private void ArmExtendedForUserPrompt(object sender, EventArgs e)
{
    Arm.Extended -= handlerForArmExtendingForPrompt;
    Controller.PromptUserForItem();
}

private void ArmExtendedForDispensing(object sender, EventArgs e)
{
    Arm.Retracted += handlerForArmRetractAfterDispenseCompleted;
    Arm.Extended -= handlerForArmExtendedForDispensing;
    Arm.Retract();
}

The primary reason I've chosen the latter approach is that stack traces have a bit more meaning to me.  That's it.  I guess it's arguable that I don't need to manage umpteen state flags.  ( As a side note, I've just learned that the spell checker in Windows Live Writer considers "umpteen" to not be a spelling error ).  But the truth is that, state flags or function pointers, I'm still managing state.

It might have been more appropriate to handle asynchronous callbacks as parameters to functions:

public void Extend(object state, AsyncCallback callback) { ...

This is tricky, too; it means needing to manage an additional two variables per asynchronous call.  For an object that might support multiple concurrent asynchronous operations, that can become a nightmare of complexity management.

I don't have the right answer for this.  But it's definitely something to watch out for in the future.

Tagged as: , , No Comments
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!