Using RIA Services with ComboBoxes and Enums

The other day someone asked me how to get a ComboBox working with enums and validation. It turned out to be more tricky than I had anticipated (there’s a common theme here…) so I figured I’d put together a quick post on it.

Stumbling Points

This section describes two specific traps I fell into. It’s not necessarily important for you to understand the details, but I’ll include them just in case you’re interested. Feel free to skip straight to the solution.

My first error was related to sharing enums in RIA. Due to the way enums appear in assembly metadata, the RIA code generator can’t determine whether they exist on the client or not. If you put an enum in a .shared.cs or .shared.vb file, it will quickly result in a compile error. The solution to this issue is simple. Instead of sharing the enum between tiers, just define it on your server and let codegen take care of the rest.

The second error I ran into was using client-side projection properties. In the past I’ve preferred these to using an IValueConverter in my bindings, but this investigation (finally) helped me to realize this approach has adverse effects on RIA validation. When the property being validated is named something other than the property being set (for example, the MyString property would write through to the MyEnum property where validation would occur) the visual control state will not be updated to reflect the error (no fancy red adornment).

The Solution

I eventually ended up using an IValueConverter-based solution. I put a little extra work into it to make it DisplayAttribute-aware since metadata is a commonly-used RIA feature. I wrote a utility to encapsulate most of this, but I’ll go over that piece at the end once you’ve got a grip on how it all fits together. (Similar variants of this solution are available in a number of places, but I wanted to fill in some of the corners that often get ignored)

I applied these changes to the sample I used in this post (https://blogs.msdn.com/b/kylemc/archive/2010/11/12/silverlight-tv-52-ria-services-q-a.aspx) if you want a place to try it out. These steps should work in any scenario, though.

image

To create the look above, I defined a simple enum with DisplayAttributes on each of the values.

   public enum MyEnum
  {
    [Display(Name = "(please pick a value)")]
    None = 0,
    [Display(Name = "Value 1")]
    Value1 = 1,
    [Display(Name = "Value 2")]
    Value2 = 2,
    [Display(Name = "Value 3")]
    Value3 = 3,
  }

Then I added a property to my entity that used custom validation to ensure a non-zero value is selected.

   [CustomValidation(typeof(Validation), "ValidateMyEnum")]
  [DataMember]
  public MyEnum MyEnum { get; set; }

(My validation is in a shared file in the web project, Validation.shared.cs)

   public static class Validation
  {
    public static ValidationResult ValidateMyEnum(
      MyEnum myEnum, ValidationContext context)
    {
      if (myEnum == MyEnum.None)
      {
        return new ValidationResult(
          "You must pick a value.", new[] { context.MemberName });
      }
      return ValidationResult.Success;
    }
  }

My next step was to define the ComboBox on the client.

   <ComboBox ItemsSource="{StaticResource myEnumNames}"
            SelectedItem="{Binding Path=MyEnum,
                                   Mode=TwoWay,
                                   Converter={StaticResource enumToString}}"
  />

The SelectedItem binding references a converter I instantiate in my page resources.

   <app:EnumToStringValueConverter x:Key="enumToString" />

The code for the converter is simple and leverages the EnumUtility I’ll describe a little later. It is also generic so a single instance can be shared between multiple types of enums.

   public class EnumToStringValueConverter : IValueConverter
  {
    public object Convert(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
      return EnumUtility.GetName(value.GetType(), value);
    }

    public object ConvertBack(object value, Type targetType,
      object parameter, CultureInfo culture)
    {
      return EnumUtility.GetValue(targetType, value.ToString());
    }
  }

The ItemsSource in the ComboBox above uses a static resource I add in my page constructor before parsing the xaml (in InitializeComponent). I’ve used the EnumUtility again to get the names for my enum.

   this.Resources.Add("myEnumNames", EnumUtility.GetNames(typeof(MyEnum)));
  InitializeComponent();

With all the pieces in place (an enum, a property to bind to, a converter, a list of names, and a ComboBox), I now have a ComboBox bound to an enum and validating input.

An Enum Utility

I wrote a utility class to handle most of the heavy lifting here. For each enum type, it iterates through all the values and figures out the names they should be mapped to. The GetName and GetValue methods map back and forth between the two. Here’s the source in case you’re interested.

   public static class EnumUtility
  {
    private static readonly
      IDictionary<Type, IEnumerable<string>> names =
        new Dictionary<Type, IEnumerable<string>>();
    private static readonly
      IDictionary<Type, IEnumerable<object>> values =
        new Dictionary<Type, IEnumerable<object>>();
    private static readonly
      IDictionary<Type, IDictionary<string, object>> namesToValues =
        new Dictionary<Type, IDictionary<string, object>>();
    private static readonly
      IDictionary<Type, IDictionary<object, string>> valuesToNames =
        new Dictionary<Type, IDictionary<object, string>>();

    public static void Initialize(Type type)
    {
      Initialize(type, GetDisplayAttributeName);
    }

    public static void Initialize(
      Type type, Func<FieldInfo, string> getName)
    {
      if (type == null)
      {
        throw new ArgumentNullException("type");
      }

      if (!type.IsEnum)
      {
        throw new ArgumentException("Type must be an enum.", "type");
      }

      if (getName == null)
      {
        throw new ArgumentNullException("getName");
      }

      if (names.ContainsKey(type))
      {
        return;
      }
        
      List<string> tempNames = new List<string>();
      List<object> tempValues = new List<object>();
      Dictionary<string, object> tempNamesToValues =
        new Dictionary<string, object>();
      Dictionary<object, string> tempValuesToNames =
        new Dictionary<object, string>();

      foreach (FieldInfo fi in
        type.GetFields(BindingFlags.Public | BindingFlags.Static))
      {
        string name = getName(fi);
        object value = fi.GetValue(null);

        tempNames.Add(name);
        tempValues.Add(value);
        tempNamesToValues[name] = value;
        tempValuesToNames[value] = name;
      }

      names[type] = tempNames;
      values[type] = tempValues;
      namesToValues[type] = tempNamesToValues;
      valuesToNames[type] = tempValuesToNames;
    }

    private static string GetDisplayAttributeName(FieldInfo fi)
    {
      DisplayAttribute displayAttribute = 
       (DisplayAttribute)fi.GetCustomAttributes(
         typeof(DisplayAttribute), false).FirstOrDefault();

      return (displayAttribute == null) ? fi.Name : displayAttribute.Name;
    }

    public static IEnumerable<string> GetNames(Type type)
    {
      Initialize(type);
      return names[type];
    }

    public static IEnumerable<object> GetValues(Type type)
    {
      Initialize(type);
      return values[type];
    }

    public static string GetName(Type type, object value)
    {
      Initialize(type);
      return valuesToNames[type][value];
    }

    public static object GetValue(Type type, string name)
    {
      Initialize(type);
      return namesToValues[type][name];
    }
  }