Freigeben über


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];
    }
  }

Comments

  • Anonymous
    January 11, 2011
    Kyle, thanks for posting this information.  Most real world scenarios will use ComboBoxes to restrict data entry.  It would also be very valuable to have a post, video, or tutorial on how to fill a ComboBox from a database lookup table, which is a very common business app scenario.  I have found various posts on this but all seemed to dead end in failure.  It seems that the major errors are how to bind the ComboBox with the lookup table, and to have the proper item in the ComboBox selected upon navigation to it. Any help with this would be greatly appreciated! Thanks, Charlie

  • Anonymous
    January 11, 2011
    @H2O It doesn't explicitly address pulling from a lookup table, but the sample from this post uses a list of values provided by the server (take a look at the 'A' ComboBoxes). All you need to do is follow the same pattern and return results from your lookup table. blogs.msdn.com/.../combobox-sample-for-ria-services.aspx

  • Anonymous
    January 12, 2011
    The comment has been removed

  • Anonymous
    January 12, 2011
    So if I understand correctly, in my SL4 RIA Business App solution, I need to include the ComboBoxExtensions project and follow this post and the post on the A Comboboxes.  I will give that a try. Thanks again for the help.

  • Anonymous
    January 12, 2011
    @SBark Hmm... a localization question. I'm not sure what others are doing, but you might consider recreating the page. I haven't seen any solutions using INotifyPropertyChanged for application resources. It doesn't mean it isn't possible, I'm just not sure how much work it would be. If you re-navigate or re-create the page the bindings will re-read the resources.

  • Anonymous
    January 12, 2011
    The comment has been removed

  • Anonymous
    January 21, 2011
    Localisation: perhaps locale might be specified as a value converter parameter?

  • Anonymous
    February 04, 2011
    @Peter I assumed you could implement localization using existing patterns for the DisplayAttribute and CustomValidationAttribute. Is there a reason you'd want to do it a different way?