0

I have several ComboBox controls, with DropDownStyle set to DropDownList. The items that are selected are enum values, but I want "friendly" descriptions of these to be displayed, i.e. with spaces and not camel case.

I want the combo box to be "two-way" bound to the data object: if the data object changes its property it alters the combo box, and vice versa.

I can do this easily with strings but the problem is I am binding the controls to enum properties in the objects, so I want the Items collection of the Combo Box to contain actual enums. This obviously isn't going to work.

Solution 1: textual properties

I can create extra properties in my object which are textual, and map those to the enum values. This is a bit messy though as I am including things in my business logic layer which really belong in the UI. In that business logic layer they should be enums and not strings.

Solution 2: event handlers

Another alternative is to use event handlers so that when the user changes the option it gets the selected item text and finds the appropriate enum value, then sets this in the object. This is only one way binding though.

Attempted Solution 3

public class BusinessObject
{
    private NumberCategory category;

    public NumberCategory Category
    {
        get
        {
            return category;
        }
        set
        {
            category = value;
        }
    }
}

public enum NumberCategory
{
    [Description("Negative Number")]
    NegativeNumber,

    [Description("Zero")]
    Zero,

    [Description("One")]
    One,

    [Description("Prime Number")]
    PrimeNumber,

    [Description("Composite Number")]
    CompositeNumber,
}

public class EnumDescriptionAdapter
{
    private readonly BusinessObject businessObject;

    public EnumDescriptionAdapter(BusinessObject businessObject)
    {
        this.businessObject = businessObject;
    }

    public string CategoryValue
    {
        get
        {
           //get the enum from businessObject and convert to a string
            return EnumUtils.GetDescription(businessObject.Category);
        }

        set
        {
            //get the string, convert to an enum and set it in BusinessObject
            businessObject.Category = EnumUtils.GetValueFromDescription<NumberCategory>(value); 
        }
    }
}

public static class EnumUtils 
{

    public static T GetValueFromDescription<T>(string description)
    {
        var type = typeof(T);
        if (!type.IsEnum) throw new InvalidOperationException();
        foreach (var field in type.GetFields())
        {
            var attribute = Attribute.GetCustomAttribute(field,
                typeof(DescriptionAttribute)) as DescriptionAttribute;
            if (attribute != null)
            {
                if (attribute.Description == description)
                    return (T)field.GetValue(null);
            }
            else
            {
                if (field.Name == description)
                    return (T)field.GetValue(null);
            }
        }
        throw new ArgumentException("Not found.", "description");
        // or return default(T);
    }

    public static string GetDescription(Enum value)
    {
        Type type = value.GetType();
        string name = Enum.GetName(type, value);
        if (name != null)
        {
            FieldInfo field = type.GetField(name);
            if (field != null)
            {
                DescriptionAttribute attr =
                       Attribute.GetCustomAttribute(field,
                         typeof(DescriptionAttribute)) as DescriptionAttribute;
                if (attr != null)
                {
                    return attr.Description;
                }
            }
        }
        return null;
    }
}

public partial class Form1 : Form
{
    public BusinessObject businessObject;

    public Form1()
    {
        InitializeComponent();

        string[] descriptions = Enum.GetValues(typeof(NumberCategory)).Cast<NumberCategory>().Select(e => EnumUtils.GetDescription(e)).ToArray();

        comboBox1.DataSource = descriptions;

        businessObject = new BusinessObject();
        EnumDescriptionAdapter adapter = new EnumDescriptionAdapter(businessObject);

        comboBox1.DataBindings.Add(new Binding("SelectedItem", adapter, "CategoryValue"));
    }

    private void button1_Click(object sender, EventArgs e)
    {
        businessObject.Category = NumberCategory.PrimeNumber;
    }
}

I put a button (button1) and a combo box (comboBox1) on my form. When I change the combo box selected item, it does fire the setter in EnumDescriptionAdapter.CategoryValue and change the businessObject. However, the reverse is not true: if I press the button it changes the businessObject but doesn't alter the selected item in comboBox1.

Paul Richards
  • 1,181
  • 1
  • 10
  • 29
  • What I used to do is build a function that I could pass any value of a specific enum to, and then with that I would get the string-representation of the enum, and parse the string manually. (On each upper-case letter add a space before, etc.) – Der Kommissar May 13 '15 at 15:01

1 Answers1

3

I don't know if it is more elegant but you can create a class with 2 properties, one for showing and one as value. You then populate a list of such created, one way or another, from your enums.

You get a star for recognising that decorating business layer stuff with presentation layer stuff is not considered good practice.

LosManos
  • 7,195
  • 6
  • 56
  • 107
  • 1
    The approach suggested in this answer is reinforced by the `DisplayMember` and `ValueMember` fields available in the `ComboBox` class. They respectively allow you to say which property of your custom class will be used to display information in the UI and which property will be used to store the actual values. By doing that, you can set the combo `DataSource` with a collection of your custom class with no need of additional logic. – Thiago Sá May 13 '15 at 18:37
  • @ThiagoSá I think you are along the right track but I still think it is flawed. I don't want to have to create a wrapper around my enum because then I'd have to return that from the business objects. The business objects should just return an enum. How the user interface deals with that is its own responsibility. – Paul Richards May 13 '15 at 19:24