tags:

views:

8874

answers:

14

In the post Enum ToString, a method is described to use the custom attribute DescriptionAttribute like this:

Enum HowNice {
  [Description("Really Nice")]
  ReallyNice,
  [Description("Kinda Nice")]
  SortOfNice,
  [Description("Not Nice At All")]
  NotNice
}

And then, you call a function GetDescription, using syntax like:

GetDescription<HowNice>(NotNice); // Returns "Not Nice At All"

But that doesn't really help me when I want to simply populate a ComboBox with the values of an enum, since I cannot force the ComboBox to call GetDescription.

What I want has the following requirements:

  • Reading (HowNice)myComboBox.selectedItem will return the selected value as the enum value.
  • The user should see the user-friendly display strings, and not just the name of the enumeration values. So instead of seeing "NotNice", the user would see "Not Nice At All".
  • Hopefully, the solution will require minimal code changes to existing enumerations.

Obviously, I could implement a new class for each enum that I create, and override its ToString(), but that's a lot of work for each enum, and I'd rather avoid that.

Any ideas?

Heck, I'll even throw in a hug as a bounty :-)

+2  A: 

The best way to do this is to make a class.

class EnumWithToString {
    private String description;
    internal EnumWithToString(String desc){
        description = desc;
    }
    public override ToString(){
        return description;
    }
}

class HowNice : EnumWithToString {

    private HowNice(String desc) : base(desc){}

    public static readonly ReallyNice = new HowNice("Really Nice");
    public static readonly KindaNice = new HowNice("Kinda Nice");
    public static readonly NotVeryNice = new HowNice("Really Mean!");
}

I believe that is the best way to do it.

When stuffed in comboboxes the pretty ToString will be shown, and the fact that no one can make any more instances of your class essentially makes it an enum.

p.s. there may need to be some slight syntax fixes, I'm not super good with C#. (Java guy)

jjnguy
How does this help with the combobox problem?
peSHIr
Well, now, when the new object is put in a combobox, its ToString will be properly displayed, and the class still acts like an enum.
jjnguy
Would have been my answer as well.
Mikko Rantanen
Also, it requires minimal code changes.
jjnguy
And seeing how the original poster explicitly did not want a class. I dont't think a class is that much more work. You can abstract the description and ToString override away to a parent class for all enums. After this all you need is a constructor `private HowNice(String desc) : base(desc) { }` and the static fields.
Mikko Rantanen
I was hoping to avoid this, since it means that each and every enumeration I make will require its own class. Ugh.
scraimer
Well, each enum would require its own enum type. And, like Mikko said, you can make one base EnumToString class, and have that do all of the work by simply extending it.
jjnguy
What's the real difference between requiring own enum and own class? Or you're already thinking from performance/memory point of view? It's not like C# wants classes being specified in their own code files like Java.
Mikko Rantanen
enums have a lot of syntactic sugar associated with them, as well as some matching extra compile-time checks that I'd rather use. But what bothers me the most is that enums are much easier to read (in the code) than an equivalent class.
scraimer
Oh. And for reference, you cannot initialize a non-string reference const so the C# Syntax hit jjnguy. The correct definition is `public readonly static` instead of `public const`.
Mikko Rantanen
A: 

Not possible to override the ToString() of enums in C#. However, you can use extension methods;

public static string ToString(this HowNice self, int neverUsed)
{
    switch (self)
    {
        case HowNice.ReallyNice:
            return "Rilly, rilly nice";
            break;
    ...

Of course you will have to make an explicit call to the method, i.e;

HowNice.ReallyNice.ToString(0)

This is not a nice solution, with a switch statement and all - but it should work and hopefully whitout to many rewrites...

Björn
Be aware that the control that binds to your enum would not call this extension method, it would call the default implementation.
Richard Szalay
Right. So this is a viable option if you need a description somewhere, it does not help with the posed combobox problem.
peSHIr
A bigger problem is that this will *never* get called (as an extension method) - instance methods that already exist always take priority.
Marc Gravell
Of course Marc is right (as always?). My .NET experience is minimal, but providing a dummy parameter to the method should do the trick. Edited answer.
Björn
+3  A: 

Given that you'd rather not create a class for each enum, I'd recommend creating a dictionary of the enum value/display text and binding that instead.

Note that this has a dependency on the GetDescription method methods in the original post.

public static IDictionary<T, string> GetDescriptions<T>()
 where T : struct
{
 IDictionary<T, string> values = new Dictionary<T, string>();

 Type type = enumerationValue.GetType();
 if (!type.IsEnum)
 {
  throw new ArgumentException("T must be of Enum type", "enumerationValue");
 }

 //Tries to find a DescriptionAttribute for a potential friendly name
 //for the enum
 foreach (T value in Enum.GetValues(typeof(T)))
 {
  string text = value.GetDescription();

  values.Add(value, text);
 }

 return values;
}
Richard Szalay
Nice idea. But how would I use this with a combobox? Once the user selects an items from the combobox, how do I know which of the items he selected? Search by the Description string? That makes my skin itch... (there might be a string "collision" between Description strings)
scraimer
The selected item's Key will be the actual enum value. Also, don't collide the description strings - how will the user tell the difference?
Richard Szalay
<cont> if you have description strings that collide, then you shouldn't be binding the values of the enum to a combobox directly anyway.
Richard Szalay
hmmm... Well, could you give me example code on how you would add items to the combobox? All I can think of is "foreach (string s in descriptionsDict.Values) { this.combobox.Items.Add(s); }"
scraimer
ComboBox.DataSource = dictionary;
Richard Szalay
You can do that? Damn! Awesome!
scraimer
+1  A: 

Create a collection that contains what you need (like simple objects containing a Value property containing the HowNice enum value and a Description property containing GetDescription<HowNice>(Value) and databind the combo to that collection.

Bit like this:

Combo.DataSource = new EnumeratedValueCollection<HowNice>();
Combo.ValueMember = "Value";
Combo.DisplayMember = "Description";

when you have a collection class like this:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Whatever.Tickles.Your.Fancy
{
    public class EnumeratedValueCollection<T> : ReadOnlyCollection<EnumeratedValue<T>>
    {
        public EnumeratedValueCollection()
            : base(ListConstructor()) { }
        public EnumeratedValueCollection(Func<T, bool> selection)
            : base(ListConstructor(selection)) { }
        public EnumeratedValueCollection(Func<T, string> format)
            : base(ListConstructor(format)) { }
        public EnumeratedValueCollection(Func<T, bool> selection, Func<T, string> format)
            : base(ListConstructor(selection, format)) { }
        internal EnumeratedValueCollection(IList<EnumeratedValue<T>> data)
            : base(data) { }

        internal static List<EnumeratedValue<T>> ListConstructor()
        {
            return ListConstructor(null, null);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, string> format)
        {
            return ListConstructor(null, format);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, bool> selection)
        {
            return ListConstructor(selection, null);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, bool> selection, Func<T, string> format)
        {
            if (null == selection) selection = (x => true);
            if (null == format) format = (x => GetDescription<T>(x));
            var result = new List<EnumeratedValue<T>>();
            foreach (T value in System.Enum.GetValues(typeof(T)))
            {
                if (selection(value))
                {
                    string description = format(value);
                    result.Add(new EnumeratedValue<T>(value, description));
                }
            }
            return result;
        }

        public bool Contains(T value)
        {
            return (Items.FirstOrDefault(item => item.Value.Equals(value)) != null);
        }

        public EnumeratedValue<T> this[T value]
        {
            get
            {
                return Items.First(item => item.Value.Equals(value));
            }
        }

        public string Describe(T value)
        {
            return this[value].Description;
        }
    }

    [System.Diagnostics.DebuggerDisplay("{Value} ({Description})")]
    public class EnumeratedValue<T>
    {
        private T value;
        private string description;
        internal EnumeratedValue(T value, string description) {
            this.value = value;
            this.description = description;
        }
        public T Value { get { return this.value; } }
        public string Description { get { return this.description; } }
    }

}

As you can see, this collection is easily customizable with lambda's to select a subset of your enumerator and/or implement a custom formatting to string instead of using the GetDescription<T>(x) function you mention.

peSHIr
Excellent, but I'm looking for something that requires even less work in the code.
scraimer
Even if you can use the same generic collection for this kind of thing for all your enumerators? I wasn't suggesting custom writing such a collection for each enum, of course.
peSHIr
+14  A: 

Don't! Enums are primitives and not UI objects - making them serve the UI in .ToString() would be quite bad from a design standpoint. You are trying to solve the wrong problem here: the real issue is that you do not want Enum.ToString() to show up in the combo box!

Now this is a very solveable problem indeed! You create a UI object to represent your combo box items:

sealed class NicenessComboBoxItem
{
    public string Description { get { return ...; } }
    public HowNice Value { get; private set; }

    public NicenessComboBoxItem(HowNice howNice) { Value = howNice; }
}

And then just add instances of this class to your combo box's Items collection and set these properties:

comboBox.ValueMember = "Value";
comboBox.DisplayMember = "Description";
Sander
I agree wholeheartedly. Neither should you expose the result of ToString() to the UI. And, you don't get localization.
Øyvind Skaar
+30  A: 

ComboBox has everything you need: the FormattingEnabled property, which you should set to true, and Format event, where you'll need to place desired formatting logic. Thus,

myComboBox.FormattingEnabled = true;
myComboBox.Format += delegate(object sender, ListControlConvertEventArgs e)
    {
        e.Value = GetDescription<HowNice>((HowNice)e.Value);
    }
Anton Gogolev
+1 Excellent!
scraimer
Very cool ... .
majkinetor
+4  A: 

I don't think you can do it without simply binding to a different type - at least, not conveniently. Normally, even if you can't control ToString(), you can use a TypeConverter to do custom formatting - but IIRC the System.ComponentModel stuff doesn't respect this for enums.

You could bind to a string[] of the descriptions, or a something essentially like a key/value pair? (desription/value) - something like:

class EnumWrapper<T> where T : struct
{
    private readonly T value;
    public T Value { get { return value; } }
    public EnumWrapper(T value) { this.value = value; }
    public string Description { get { return GetDescription<T>(value); } }
    public override string ToString() { return Description; }

    public static EnumWrapper<T>[] GetValues()
    {
        T[] vals = (T[])Enum.GetValues(typeof(T));
        return Array.ConvertAll(vals, v => new EnumWrapper<T>(v));
    }
}

And then bind to EnumWrapper<HowNice>.GetValues()

Marc Gravell
+11  A: 

You could write an TypeConverter that reads specified attributes to look them up in your resources. Thus you would get multi-language support for display names without much hastle.

Look into the TypeConverter's ConvertFrom/ConvertTo methods, and use reflection to read attributes on your enum fields.

Simon Svensson
+1 whoa. This might be what I need.
scraimer
OK, I wrote some code (see my answer to this question) - do you think that's enough, am I missing something?
scraimer
Nice one. Better overal, but might be overkill for your run-of-the-mill never-will-be-globalized-in-any-way piece of software. (I know, that assumption will turn out to be false later. ;-))
peSHIr
+1  A: 

I would write a generic class for use with any type. I've used something like this in the past:

public class ComboBoxItem<T>
{
 /// The text to display.
 private string text = "";
 /// The associated tag.
 private T tag = default(T);

 public string Text
 {
  get
  {
   return text;
  }
 }

 public T Tag
 {
  get
  {
   return tag;
  }
 }

 public override string ToString()
 {
  return text;
 }

 // Add various constructors here to fit your needs
}

On top of this, you could add a static "factory method" to create a list of combobox items given an enum type (pretty much the same as the GetDescriptions method you have there). This would save you of having to implement one entity per each enum type, and also provide a nice/logical place for the "GetDescriptions" helper method (personally I would call it FromEnum(T obj) ...

Dan C.
+1  A: 

You could make a generic struct that you could use for all of your enums that has descriptions. With implicit conversions to and from the class, your variables still works like the enum except for the ToString method:

public struct Described<T> where T : struct {

 private T _value;

 public Described(T value) {
  _value = value;
 }

 public override string ToString() {
  string text = _value.ToString();
  object[] attr =
   typeof(T).GetField(text)
   .GetCustomAttributes(typeof(DescriptionAttribute), false);
  if (attr.Length == 1) {
   text = ((DescriptionAttribute)attr[0]).Description;
  }
  return text;
 }

 public static implicit operator Described<T>(T value) {
  return new Described<T>(value);
 }

 public static implicit operator T(Described<T> value) {
  return value._value;
 }

}

Usage example:

Described<HowNice> nice = HowNice.ReallyNice;

Console.WriteLine(nice == HowNice.ReallyNice); // writes "True"
Console.WriteLine(nice); // writes "Really Nice"
Guffa
+5  A: 

TypeConverter. I think this is what I was looking for. All hail Simon Svensson!

[TypeConverter(typeof(EnumToStringUsingDescription))]
Enum HowNice {
  [Description("Really Nice")]
  ReallyNice,
  [Description("Kinda Nice")]
  SortOfNice,
  [Description("Not Nice At All")]
  NotNice
}

All I need to change in my current enum is add this line before their declaration.

[TypeConverter(typeof(EnumToStringUsingDescription))]

Once I do that, any enum will get displayed using the DescriptionAttribute of its fields.

Oh, and the TypeConverter would be defined like this:

public class EnumToStringUsingDescription : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return (sourceType.Equals(typeof(Enum)));
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        return (destinationType.Equals(typeof(String)));
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (!destinationType.Equals(typeof(String)))
        {
            throw new ArgumentException("Can only convert to string.", "destinationType");
        }

        if (!value.GetType().BaseType.Equals(typeof(Enum)))
        {
            throw new ArgumentException("Can only convert an instance of enum.", "value");
        }

        string name = value.ToString();
        object[] attrs = 
            value.GetType().GetField(name).GetCustomAttributes(typeof(DescriptionAttribute), false);
        return (attrs.Length > 0) ? ((DescriptionAttribute)attrs[0]).Description : name;
    }
}

This helps me with my ComboBox case, but obviously doesn't actually override the ToString(). I guess I'll settle for this meanwhile...

scraimer
You're handling Enum -> String, but you will also need Enum > InstanceDescriptor, and String -> Enum if you want a complete implementation. But I guess displaying it is enough for your needs at the moment. ;)
Simon Svensson
This solution only works when your descriptions are static, unfortunately.
vanslly
Yup, but then again, that is all I wanted...
scraimer
By the way, the use of TypeConverter is not bound to static descritions, coverter can populate values from other sources than attributes.
Dmitry Tashkinov
A: 

You could use PostSharp to target Enum.ToString and add aditionall code you want. This doesn't require any code changes.

majkinetor
+1  A: 

What you need is to turn an enum into a ReadonlyCollection and bind the collection to the combobox (or any Key-Value Pair enabled control for that matter)

First off you need a class to contain the items of the list. Since all you need is the int/string pair I suggest using an interface and a base class combo so that you can implement the functionality in any object you want:

public interface IValueDescritionItem
{
    int Value { get; set;}
    string Description { get; set;}
}

public class MyItem : IValueDescritionItem
{
    HowNice _howNice;
    string _description;

    public MyItem()
    {

    }

    public MyItem(HowNice howNice, string howNice_descr)
    {
        _howNice = howNice;
        _description = howNice_descr;
    }

    public HowNice Niceness { get { return _howNice; } }
    public String NicenessDescription { get { return _description; } }


    #region IValueDescritionItem Members

    int IValueDescritionItem.Value
    {
        get { return (int)_howNice; }
        set { _howNice = (HowNice)value; }
    }

    string IValueDescritionItem.Description
    {
        get { return _description; }
        set { _description = value; }
    }

    #endregion
}

Here is the interface and a sample class that implements it.Notice that the class' Key is strongly typed to the Enum, and that the IValueDescritionItem proprties are implemented explicitely (so the class can have whatever properties and you can CHOOSE the ones that implement the Key/Value pair.

Now the EnumToReadOnlyCollection class:

public class EnumToReadOnlyCollection<T,TEnum> : ReadOnlyCollection<T> where T: IValueDescritionItem,new() where TEnum : struct
{
    Type _type;

    public EnumToReadOnlyCollection() : base(new List<T>())
    {
        _type = typeof(TEnum);
        if (_type.IsEnum)
        {
            FieldInfo[] fields = _type.GetFields();

            foreach (FieldInfo enum_item in fields)
            {
                if (!enum_item.IsSpecialName)
                {
                    T item = new T();
                    item.Value = (int)enum_item.GetValue(null);
                    item.Description = ((ItemDescription)enum_item.GetCustomAttributes(false)[0]).Description;
                    //above line should be replaced with proper code that gets the description attribute
                    Items.Add(item);
                }
            }
        }
        else
            throw new Exception("Only enum types are supported.");
    }

    public T this[TEnum key]
    {
        get 
        {
            return Items[Convert.ToInt32(key)];
        }
    }

}

So all you need in your code is :

private EnumToReadOnlyCollection<MyItem, HowNice> enumcol;
enumcol = new EnumToReadOnlyCollection<MyItem, HowNice>();
comboBox1.ValueMember = "Niceness";
comboBox1.DisplayMember = "NicenessDescription";
comboBox1.DataSource = enumcol;

Remember that your collection is typed with MyItem so the combobox value should return an enum value if you bind to the appropriate proprtie.

I added the T this[Enum t] property to make the collection even more usefull than a simple combo consumable, for example textBox1.Text = enumcol[HowNice.ReallyNice].NicenessDescription;

You can of course chose to turn MyItem into a Key/Value class used only for this puprose effectively skipping MyItem in the type arguments of EnumToReadnlyCollection altogether, but then you'd be forced to go with int for the key (meaning getting combobox1.SelectedValue would return int and not the enum type). You work around that if you create a KeyValueItem class to replace MyItem and so on and so forth...

A: 

Could you post an example of using postsharp to target the enum.tostring() method?

rickard