Friday, August 17, 2012

WPF PropertyGrid - display partial enum values in ComboBox

In the previous post, I explained a way of getting a subset of enum values. This post shows an example of displaying a subset of enum onto WPF PropertyGrid. In WPF 4.0, there is no standard built-in PropertyGrid control but one can find a good PropertyGrid from Extended WPF ToolKit at codeplex website. As of now, its latest version is 1.6 but due to some compatibility issue, I happen to use v1.5. Since this is open source, you can also download source code from the website.

In WPF PropertyGrid 1.5, there is no way of doing enum filtering as we expect (of course). In order to facilitate enum filtering, I added an editor EnumFilterComboBoxEditor derived from ComboBoxEditor.

public class EnumFilterComboBoxEditor<T> : ComboBoxEditor where T : struct
{
    public EnumFilterComboBoxEditor()
    {
    }

    protected override void ResolveValueBinding(PropertyItem propertyItem)
    {
        EnumFilter<T> enumFilter = (EnumFilter<T>)propertyItem.Value;
        var _binding = new Binding("Value");
        _binding.Source = enumFilter;
        _binding.ValidatesOnExceptions = true;
        _binding.ValidatesOnDataErrors = true;
        _binding.Mode = propertyItem.IsWriteable ? BindingMode.TwoWay : BindingMode.OneWay;
        _binding.Converter = CreateValueConverter();
        BindingOperations.SetBinding(Editor, ValueProperty, _binding);
    }

    protected override IList<object> CreateItemsSource(PropertyItem propertyItem)
    {
        return GetValues(propertyItem.Value);
    }

    private static object[] GetValues(object enumObject)
    {
        List<object> values = new List<object>();

        EnumFilter<T> enumFilter = (EnumFilter<T>)enumObject;            
        var enums = enumFilter.GetEnums();
        foreach (var e in enums)
        {
            values.Add(e);
        }

        return values.ToArray();
    }
}

ResolveValueBinding() sets binding between source property EnumFilter<T>.Value and target Editor.ValueProperty. CreateItemsSource() sets all possible values of ComboBox items where we call GetEnums() method to get filtered enum values. And now to invoke this new type of editor, the following code added to PropertyGrid class (line 478 in propertygrid.cs).

else if (propertyItem.PropertyType.BaseType == typeof(Common.EnumFilter))
{
    Type tenum = propertyItem.Value.GetType().GetGenericArguments()[0];
    Type filterEditor = typeof(EnumFilterComboBoxEditor<>).MakeGenericType(tenum);
    editor = (ITypeEditor)Activator.CreateInstance(filterEditor, true);                        
}
else if (propertyItem.PropertyType.IsEnum) // all Enums
    editor = new EnumComboBoxEditor();

Basically what it does is check the PropertyType and sets different editor based on the type. For enum filtering case, propertyItem.Value is EnumFilter<T> object. And calling GetGenericArguments()[0] returns the first generic argument type (that is, T). For example, for EnumFilter<MartType>, GetGenericArguments()[0] will return MartType. Next line MakeGenericType() call returns concrete type from generics by using passed argument (tenum). So filterEditor will be EnumFilterComboBoxEditor<MartType> type. Please note C# generics cannot be a type unless generic argument T is specified. Once we got the concrete type, we can create an instance from it by using Activator.CreateInstance() method as seen in the next line. I think the sample code snippet is not optimized due to lack of time. Just wanted to summarize my thought...



Filter enum type - Show a subset of enum values

I happen to run into the case where I have to display a subset of enum type values instead of displaying the all elements - which is common case - of the enum type. .NET enum is a predefined type, should be defined at compile time and cannot be dynamically built at run time.

To illustrate the problem domain, let's say we have a enum called MartType as follows.

public enum MartType{
   Safeway,
   QFC,
   Alberson,
   HMart
}

If we display a enum type property in PropertyGrid (actually I used WPF property grid which is included in WPF extended toolkit), it will show a combox editor containing all enum values.




Now, what if we want to display a subset of enum values based on Area? For example, in case of another city (Bellevue), we only want to show 3 grocery stores.



In order to do this, one should restrict ItemsSource data for the ComboBox. And in order to accomplish this in WPF PropertyGrid control, one have to add a special editor to handle this special case (will write about this later).

For the example above, let's say we expose 2 properties like this.

public string Area { get; set; }
public EnumFilter<MartType> AreaMartType { get; set; }

AreaMartType public property is not simply MartType enum, but a EnumFilter of MartType. This AreaMartType is set by the following helper private method, which returns filtered enum based on areaNo.

private EnumFilter<MartType> FilterMarts(int areaNo, MartType selectedMart)
{
    List subset = new List();
    switch (areaNo)
    {
        case 1:
            subset.Add(MartType.QFC);
            subset.Add(MartType.Alberson);
            subset.Add(MartType.HMart);
            break;
        case 2:
            subset.Add(MartType.QFC);
            subset.Add(MartType.Alberson);
            subset.Add(MartType.Safeway);
            break;
        case 3:                    
            subset.Add(MartType.Alberson);
            subset.Add(MartType.Safeway);
            subset.Add(MartType.HMart);
            break;
        default:
            subset.Add(MartType.QFC);
            subset.Add(MartType.Alberson);
            subset.Add(MartType.Safeway);
            subset.Add(MartType.HMart);
            break;
    }
    EnumFilter<MartType> enumFilter = new EnumFilter<MartType>(subset);
    enumFilter.Value = selectedMart;
    return enumFilter;
}

So EnumFilter of T class takes enum value list which is a subset of enum values. And GetEnums() method actually do the filtering work based on the given subset. By IEnumerable and yield only filtered one, it can dynamically retrieve a subset of enum items.

public class EnumFilter
{        
}
public class EnumFilter<T> : EnumFilter, INotifyPropertyChanged
    where T : struct
{
    private List _elements;
    private T _value;

    public EnumFilter()
    {
        _elements = new List<T>();
    }

    public EnumFilter(List<T> elements)
    {
        _elements = elements;
    }

    public IEnumerable<T> GetEnums()
    {
        foreach (var field in typeof(T).GetFields(
            BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static))
        {
            T enumVal = (T)field.GetValue(null);
            if (_elements.Contains(enumVal))
            {
                yield return enumVal;
            }
        }
    }

    public T Value
    {
        get { return _value; }
        set
        {
            _value = value;
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs("Value"));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

How we display the subset of enums in WPF PropertyGrid is little different topic but related. Will write more. (Please see : http://dotnetbeyond.blogspot.com/2012/08/wpf-propertygrid-display-partial-enum.html )