Monday 18 February 2013

Enum ComboBox using MarkupExtension

         Quite often I find myself in need of using ComboBoxes together with Enum types instead of an ordinary items collection. More over those Enum values have to be translated using a .resx file to the current language. A complex and simple to use solution was needed. In this example I'll show how to create one using MarkupExtension.
First of all, we create MarkupExtension by deriving from MarkupExtension class. MarkupExtensions are quite useful, especially in situations where we have static data (such as resource file, or enum) and we want to do something with the data and return the outcome. All the heavy lifting will be happening in the MarkupExtension's ProvideValue method which we need to override.

Code Snippet
  1. public class EnumValuesExtension : MarkupExtension
  2. {
  3.     private Type _enumType;
  4.     private String _resourceName;    
  5.     private bool _addEmptyValue = false;
  6.     // Enumeration type
  7.     public Type EnumType
  8.     {
  9.         set { _enumType = value; }
  10.     }
  11.     // Name of the class of the .resx file        
  12.     public String ResourceName
  13.     {
  14.         set { _resourceName = value; }
  15.     }    
  16.     // Add empty value flag        
  17.     public Boolean AddEmptyValue
  18.     {
  19.         set { _addEmptyValue = value; }
  20.     }
  21.     public EnumValuesExtension()
  22.     {
  23.     }

       Next, I've added these 3 properties, which are pretty self explanatory and decorated with comments, they provide the basis for the data that goes into the extension, and are settable from XAML. It is possible to use the constructor straight in XAML, but it's much more comfortable to do it using these properties, hence the constructor remains parameterless.

Code Snippet
  1. public override object ProvideValue(IServiceProvider serviceProvider)
  2. {
  3.     // Enumeration type not passed through XAML
  4.     if (_enumType == null)                        
  5.         throw new ArgumentNullException("EnumType (Property not set)");
  6.     if (!_enumType.IsEnum)                                  
  7.         throw new ArgumentNullException("Property EnumType must be an enum");
  8.     // Bindable properties list
  9.     List<dynamic> list = new List<dynamic>();        
  10.  
  11.     if (!String.IsNullOrEmpty(_resourceName))
  12.     {
  13.         // Name of the resource class
  14.         Type type = Type.GetType(_resourceName);  
  15.         if (type == null)                
  16.             throw new ArgumentException(
  17.                 "Resource name should be a fully qualified name");
  18.         // We iterate through the Enum values
  19.         foreach (var enumName in Enum.GetNames(_enumType))  
  20.         {
  21.             string translation = string.Empty;
  22.             var property = type.GetProperty(enumName,
  23.                 BindingFlags.Static |
  24.                 BindingFlags.Public |
  25.                 BindingFlags.NonPublic);
  26.             // If there's not translation for specific Enum value,
  27.             // there'll be a message shown instead
  28.             if (property == null)                 
  29.                 translation = String.Format(
  30.                     "Field {0} not found in the resource file {1}",
  31.                     enumName,
  32.                     _resourceName);
  33.             else
  34.                 translation = property.GetValue(null, null).ToString();
  35.                                                  
  36.             list.Add(GetNamed(translation, enumName));                    
  37.         }
  38.         // Adding empty row
  39.         if (_addEmptyValue)                       
  40.             list.Add(GetEmpty());
  41.         return list;
  42.     }
  43.     // If there's no resource provided Enum values will be used
  44.     // without translation
  45.     foreach (var enumName in Enum.GetNames(_enumType))  
  46.         list.Add(GetNamed(enumName, enumName));               
  47.  
  48.     if (_addEmptyValue)                                                  
  49.         list.Add(GetEmpty());
  50.  
  51.     return list;
  52. }

       Above code is mostly using reflection to shuffle properties around - get values of the enumeration and their translations. Also using dynamic objects to make a bindable context for our ViewModel to bind to. For that we need last 2 methods.

Code Snippet
  1. // Create one item which will fill our ComboBox ItemSource list
  2. private dynamic GetNamed(string translation, string enumName)
  3. {
  4.     // We create a bindable context
  5.     dynamic bindableResult = new ExpandoObject();    
  6.     // This dynamically created property will be
  7.     // bindable from XAML (through DisplayMemberPath or wherever)
  8.     bindableResult.Translation = translation;
  9.     // We're setting the value, which will be passed to SelectedItem
  10.     // of the ComboBox
  11.     bindableResult.Enum = enumName;                  
  12.     return bindableResult;
  13. }
  14.  
  15. // Create one empty item which will fill our ComboBox ItemSource list
  16. private dynamic GetEmpty()
  17. {
  18.     dynamic bindableResult = new ExpandoObject();
  19.     bindableResult.Translation = String.Empty;
  20.     bindableResult.Enum = null;
  21.     return bindableResult;
  22. }

       Finally, we can call this code from XAML, it is worth noting that due to how GetType method works we will need a fully qualified path to the resource class. In parameters below we can see properties being set, EnumType to the Type of the enum which is going to fill the Combo, and ResourceName to the fully qualified name of the class (the name of the project/module, in this case "Mod.Ule" is additionally added after the comma). SelectedType is a property in the ViewModel of the same type as the enum passed into the extension, and it is all we need in our VM to make this work. This mechanisms works with DataTemplates as well, all you'd need to do is bind to Translation instead of using DisplayMemberPath.

Code Snippet
  1. <ComboBox Height="30" Width="100" VerticalAlignment="Top"
  2. SelectedValuePath="Enum" DisplayMemberPath="Translation"
  3. ItemsSource="{Binding Source={Extensions:ValuesEnum
  4. EnumType=Extensions:Types,
  5. ResourceName='Mod.Ule.Resources.Strings.Enums.Resource1,Mod.Ule'}}"
  6. SelectedValue="{Binding SelectedType}"/>

No comments:

Post a Comment