23
\$\begingroup\$

I'm wanting to create a Reusable class to work on Enumerations so that I can decorate various Enums with different attributes and I can use this class to get a particular attribute property to display.

An UnitTest example of how I would use the class is:

    internal enum TestEnum
    {
        [System.ComponentModel.Description("Description Attribute")]
        [System.Xml.Serialization.XmlAttribute("XML Attribute")]    
        EnumWithDescription,

        [System.ComponentModel.Category("Display name attribute")]
        [System.ComponentModel.Description("Description Attribute over category attribute")]            
        EnumWithXMLAttribute,

        EnumNoAttributes,
    }

    protected EnumAttributeDescriptor CreateEnumAttributeDescriptor()
    {
        EnumAttributeDescriptor descriptor = new EnumAttributeDescriptor();

        descriptor.AddAttributePriority<System.ComponentModel.DescriptionAttribute>("Description");
        descriptor.AddAttributePriority(new PropertyDecoration("Category", typeof(System.ComponentModel.CategoryAttribute)));            

        return descriptor;
    }

    internal class IntAttributeDescriptor : PropertyAttributeDescriptor
    {
        public IntAttributeDescriptor()
            : base() 
        {
            AddAttributePriority<System.ComponentModel.DescriptionAttribute>("Description");
        }
    }

    internal class EnumSwitchableDescriptor : IObjectDescriptor
    {
        public string GetDescription(object record)
        {
            TestEnum item = (TestEnum)record;

            switch (item)
            {
                case TestEnum.EnumWithDescription:
                    return "This is a test attribute";
                case TestEnum.EnumWithXMLAttribute:
                    return "How did you sneak in here";
                default:
                    throw new NotSupportedException(string.Format("Enumeration not supported {0}", item));
            }  
        }
    }

    internal class TestObject
    {
        [System.ComponentModel.Description("Testable integer")]
        public int TestInteger
        {
            get;
            set;
        }
    }

    [TestMethod]
    public void EnumDescriptionHardCodedTest()
    {
        ObjectDescription descriptions = new ObjectDescription(new EnumSwitchableDescriptor());
        string name = descriptions.GetDisplayName(TestEnum.EnumWithDescription);

        Assert.AreEqual("This is a test attribute", name);
    }

    [TestMethod]
    public void EnumDescriptionMissingAttributeTest()
    {
        ObjectDescription descriptions = new ObjectDescription(CreateEnumAttributeDescriptor());

        string name = descriptions.GetDisplayName(TestEnum.EnumNoAttributes);

        Assert.IsNull(name);
    }

    [TestMethod]
    public void EnumDescriptionDefaultAttributeTest()
    {
        ObjectDescription descriptions = new ObjectDefaultableDescription((obj) => { return null; });

        string name = descriptions.GetDisplayName(TestEnum.EnumWithDescription);

        Assert.AreEqual("EnumWithDescription", name);
    }

    [TestMethod]
    public void EnumDescriptionFromSingleAttributeTest()
    {
        ObjectDescription descriptions = new ObjectDescription(CreateEnumAttributeDescriptor());

        string name = descriptions.GetDisplayName(TestEnum.EnumWithDescription);

        Assert.AreEqual("Description Attribute", name);
    }

    [TestMethod]
    public void EnumDescriptionFromObjectTest()
    {
        ObjectDescription descriptions = new ObjectDescription(CreateEnumAttributeDescriptor());

        TestEnum testable = TestEnum.EnumWithXMLAttribute;
        string name = descriptions.GetDisplayName(testable);

        Assert.AreEqual("Description Attribute over category attribute", name);
    }

    [TestMethod]
    public void IntDescriptionFromObjectTest()
    {
        TestObject testable = new TestObject();
        ObjectDescription descriptor = new ObjectDescription(new IntAttributeDescriptor());

        string description = descriptor.GetDisplayName(testable);

        Assert.AreEqual("Testable integer", description);
    }

My ObjectInspector class responsible for getting a property :

public class ObjectInspector
{
    public string GetProperty(object record, string propertyName)
    {
        Type typeOf = record.GetType();

        object value = typeOf.GetProperty(propertyName).GetValue(record, null);

        if (value == null)
            return null;
        else
            return value.ToString();
    }

    public bool IsOfType(object record, Type typeOf)
    {
        Type typeOfRecord = record.GetType();            
        return typeOfRecord.Equals(typeOf) || typeOfRecord.IsSubclassOf(typeOf);            
    }

    public bool IsOfType<T>(object record)
    {
        Type typeOf = typeof(T); 
        return IsOfType(record, typeOf);
    }

}

My Descriptor class and interface:

public interface IObjectDescriptor
{
    string GetDescription(object record);
}

public abstract class AttributeDescriptor : IObjectDescriptor
{
    private readonly List<PropertyDecoration> _attributes;

    public AttributeDescriptor() 
    {
        _attributes = new List<PropertyDecoration>();
    }

    public string GetDescription(object record)
    {
        var priorities = GetAttributes();

        if (priorities == null || record == null)
            return null;

        var attributes = GetAttributes(record);

        if (attributes != null)
            return GetAttributeDescription(attributes);
        else
            return null;
    }

    public virtual void AddAttributePriority(PropertyDecoration attr)
    {
        _attributes.Add(attr);
    }

    public virtual void AddAttributePriority<T>(string propertyName)
    {
        PropertyDecoration lookup = new PropertyDecoration(propertyName, typeof(T));
        AddAttributePriority(lookup);
    }

    public virtual IEnumerable<PropertyDecoration> GetAttributes()
    {
        return _attributes;
    }

    protected abstract object[] GetAttributes(object record);

    protected virtual string GetAttributeDescription(object[] attributes)
    {
        ObjectInspector inspector = new ObjectInspector();
        object attribute = null;

        // looping from highest precedence
        foreach (var attr in GetAttributes())
        {
            // find a matching attribute on our object
            attribute = attributes.FirstOrDefault(p => inspector.IsOfType(p, attr.GetPropertyType()));

            if (attribute != null)
            {
                //return attribute;
                return attr.GetDescription(attribute);
            }
        }

        return null;
    }
}

public class EnumAttributeDescriptor : AttributeDescriptor
{
    protected override object[] GetAttributes(object record)
    {
        var typeOf = record.GetType();
        var info = typeOf.GetMember(record.ToString());
        return info[0].GetCustomAttributes(false);
    }
}

And finally the Object description classes to actually get the description:

public class ObjectDescription
{
    private IObjectDescriptor _objectDescriptor;

    public ObjectDescription(IObjectDescriptor attributePriorities)
    {
        _objectDescriptor = attributePriorities;
    }

    public string GetDisplayName(object record)
    {
        return GetDescription(record, _objectDescriptor);
    }

    protected virtual string GetDescription(object record, IObjectDescriptor descriptor)
    {
        return descriptor.GetDescription(record);
    }
}

public class ObjectDefaultableDescription : ObjectDescription
{
    private Func<object, string> _defaultIfEmpty;

    public ObjectDefaultableDescription(Func<object, string> defaultIfEmpty)
        : base(null)
    {
        _defaultIfEmpty = defaultIfEmpty;
    }

    protected override string GetDescription(object record, IObjectDescriptor descriptor)
    {         
        if (record == null)
            return _defaultIfEmpty(record);
        else
            return record.ToString();
    }

    private string GetDefaultIfEmpty(object record)
    {
        if (_defaultIfEmpty != null)
            return _defaultIfEmpty(record);
        else
            return null;
    }
}

I want to keep it flexible so that I can decorate Enums with different Attributes as the requirements may vary i.e. using System.Xml.Serialization.XmlEnumAttribute in conjunction with System.ComponentModel.DescriptionAttribute so a UI can use the Description attribute to display a nice name while an XML serialiser can use the XmlEnumAttribute etc

I would expect to either provide a wrapper class for this on my various projects so that I wouldn't have to create the delegate method everywhere however unsure of this approach.

EDIT: After re-looking I thought the GetProperty() method is pretty generic and doesn't really necessary only belong to an Enum so I moved that out into it's own ObjectInspector class.

EDIT: After some further thought into this I decided that actually it's not directly related to Enum's but rather the ability to get descriptions from properties in general. I've made some tweaks to the classes to try and separate this out. Any comments would be most appreciated.

\$\endgroup\$
  • \$\begingroup\$ Not having access to VS2010 at the moment, why not steal code from weblogs.asp.net/grantbarrington/archive/2009/01/19/… but rework the signature to be public static string GetDescription<T>(Enum en) where T : something instead? Instead of return en.ToString(); you can do return null; as a way to indicate that this particular attribute was missing. Once this works, you can add wrapper methods which provide even more convenient ways of getting the info you want. \$\endgroup\$ – Leonid Feb 12 '12 at 3:08
  • \$\begingroup\$ @Leonid. hmmm, doesn't seem to provide enough flexibility. How could I use a category attribute easily, or perhaps an Xml AttributeAttribute. These don't have the Description property so I couldn't cast accordingly? \$\endgroup\$ – dreza Feb 12 '12 at 9:41
  • \$\begingroup\$ Right ... I am not in front of VS2010. What if the main worker method could get you anything you ask for and helper methods making it easier. The usage: public static GetCategory(enum) { return GetCustomAttributevalue<CategoryAttribute>(enum, "Category");} and so on. I do not like GetEnumAttribute - it decides what you want for you. I want to be able to ask for a specific attribute and get a null if it is not there. Then I can add a method on top of this that will look for the first non-null attribute string in some prioritized order. \$\endgroup\$ – Leonid Feb 12 '12 at 15:27
  • 2
    \$\begingroup\$ Related: codereview.stackexchange.com/a/5354/3902 \$\endgroup\$ – finnw Feb 13 '12 at 1:21
  • 1
    \$\begingroup\$ @finnw Sort of but not quite. I'm wanting to decorate my Enum's etc with a number of attributes not just the one. \$\endgroup\$ – dreza Feb 13 '12 at 1:46
2
\$\begingroup\$

I don't really see the point of XmlAttribute on a specific enumeration constant. I think that would go as a TypeConverter on the enum itself or on a property of the enum type. I can see some uses for DisplayName and Description. Unfortunately, DisplayName is not supported on Enums. The DynamicObject didn't actually end being that useful, but maybe it will give you some ideas. You could add some additional properties to the class like "enum name"Desc. I was thinking that might be useful for WPF binding, but only if you structured the helper class to have a current value. (And if you have the current value, then I don't see what you're gaining over a non-dynamic wrapper.) The ICustomTypeConverter also provided some interesting functionality. Here's a pass with DynamicObject:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;

namespace TestEnumDO
{
    sealed class EnumHelper<T> : DynamicObject, ICustomTypeDescriptor where T: struct // ,System.Enum -- you wished
    {
        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return Enum.GetNames(typeof(T));
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            T t;
            if(!Enum.TryParse<T>(binder.Name, out t))
                return base.TryGetMember(binder, out result);
            result = t;
            return true;
        }

        public AttributeCollection GetAttributes()
        {
            return TypeDescriptor.GetAttributes(typeof(T));
        }

        public string GetClassName()
        {
            return TypeDescriptor.GetClassName(typeof(T));
        }

        public string GetComponentName()
        {
            return TypeDescriptor.GetComponentName(this, true);
        }

        public TypeConverter GetConverter()
        {
            return TypeDescriptor.GetConverter(typeof(T));
        }

        public EventDescriptor GetDefaultEvent()
        {
            return TypeDescriptor.GetDefaultEvent(typeof(T));
        }

        public PropertyDescriptor GetDefaultProperty()
        {
            return TypeDescriptor.GetDefaultProperty(typeof(T));
        }

        public object GetEditor(Type editorBaseType)
        {
            return TypeDescriptor.GetEditor(typeof(T), editorBaseType);
        }

        public EventDescriptorCollection GetEvents(Attribute[] attributes)
        {
            return TypeDescriptor.GetEvents(typeof(T), attributes);
        }

        public EventDescriptorCollection GetEvents()
        {
            return TypeDescriptor.GetEvents(typeof(T));
        }

        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            var values = (T[])Enum.GetValues(typeof(T));
            var props = new PropertyDescriptor[values.Length];
            for (int i = 0; i < values.Length; i++)
            {
                props[i] = new EnumPropertyDescriptor(values[i], attributes);
            }
            return new PropertyDescriptorCollection(props, true);
        }

        public PropertyDescriptorCollection GetProperties()
        {
            return GetProperties(null);
        }

        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return this;
        }

        private class EnumPropertyDescriptor : PropertyDescriptor
        {
            private readonly T _t;
            public EnumPropertyDescriptor(T value, Attribute[] atts):base(Enum.GetName(typeof(T), value), atts)
            {
                _t = value;
            }

            public override AttributeCollection Attributes
            {
                get
                {
                    var attributeDefs = typeof(T).GetMember(Name).Single().CustomAttributes;
                    var attributes = new List<Attribute>();
                    foreach(var ad in attributeDefs){
                        var constructorArgs = ad.ConstructorArguments.Select(ca => ca.Value).ToArray<object>();
                        var attribute = (Attribute)ad.Constructor.Invoke(constructorArgs);
                        foreach (var na in ad.NamedArguments)
                        {
                            if (na.MemberInfo is PropertyInfo)
                                ((PropertyInfo)na.MemberInfo).SetValue(attribute, na.TypedValue.Value);
                            else if (na.MemberInfo is FieldInfo)
                                ((FieldInfo)na.MemberInfo).SetValue(attribute, na.TypedValue.Value);
                            else throw new NotImplementedException();
                        }
                        attributes.Add(attribute);
                    }
                    return new AttributeCollection(attributes.ToArray());
                }
            }

            public override bool CanResetValue(object component)
            {
                return false;
            }

            public override Type ComponentType
            {
                get { return typeof(T); }
            }

            public override object GetValue(object component)
            {
                return _t;
            }

            public override bool IsReadOnly
            {
                get { return true; }
            }

            public override Type PropertyType
            {
                get { return typeof(T); }
            }

            public override void ResetValue(object component)
            {
            }

            public override void SetValue(object component, object value)
            {
                throw new NotSupportedException();
            }

            public override bool ShouldSerializeValue(object component)
            {
                return true;
            }
        }
    }

    class Program
    {
        enum TestEnum
        {
            [Description("Go One")]
            Go1,
            [Description("Go Two")]
            Go2,
            Go3
        }

        static void Main(string[] args)
        {
            dynamic helper = new EnumHelper<TestEnum>();
            Console.WriteLine("Names:");
            foreach (var name in helper.GetDynamicMemberNames())
                Console.WriteLine("  " + name);

            Console.WriteLine();
            Console.WriteLine("Display Names:");

            var pdc = TypeDescriptor.GetProperties(helper) as PropertyDescriptorCollection;
            var desc = pdc.OfType<PropertyDescriptor>().Single(pd => pd.Name == "Go1").Description;
            Console.WriteLine("Go1 Value = {0}, Description = {1}", (int)helper.Go1, desc);

            if (Debugger.IsAttached)
            {
                Console.WriteLine();
                Console.WriteLine("Press any key...");
                Console.ReadKey();
            }
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Cheers Brannon. The xmlAttribute is used for when the enum is to be used for serialisation and you want to give it a different xml value. We had to use both that attribute and the description to give it a different display value to the user. \$\endgroup\$ – dreza Oct 30 '12 at 19:03
  • \$\begingroup\$ I still think a TypeConverter on the enum declaration would be more appropriate than an XmlAttribute, but I'm not sure what deserializer you were using and if it supported TypeConverters. \$\endgroup\$ – Brannon Oct 31 '12 at 3:22
  • \$\begingroup\$ System.Xml.Serialization.XmlSerializer \$\endgroup\$ – dreza Oct 31 '12 at 5:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.