Click here to Skip to main content
15,885,933 members
Articles / Desktop Programming / WPF

Displaying User Friendly Enum Values in WPF

Rate me:
Please Sign up or sign in to vote.
4.94/5 (25 votes)
12 Mar 2014CPOL13 min read 105.2K   1.7K   68  
Presents a helper class to easily display user-friendly enum value representations including customizable and localizable text, images, and arbitrary XAML content. This class can also be used as a general XAML switch statement for enum and non-enum classes.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Resources;
using System.Diagnostics;
using System.Drawing;

namespace EnumTools
{
    
    /// <summary>
    /// Enumerates the properties for EnumItem.
    /// </summary>
    public enum EnumItemsOrder
    {
        Custom,
        Value,
        DisplayValue,
        Text
    }

    /// <summary>
    /// Represents a list of enumeration values and corresponding values to be shown or used in the user interface.
    /// </summary>
    [Serializable()]
    [Localizability(LocalizationCategory.None,Modifiability=Modifiability.Unmodifiable)]
    public class EnumItemList : ObservableCollection<EnumItem>, IValueConverter
    {

    #region Constructors
        
        /// <summary>
        /// Creates a new empty EnumItemList.
        /// </summary>
        public EnumItemList()
        {
             SortBy = EnumItemsOrder.Text;
        }

        /// <summary>
        /// Creates a new EnumItemList for the given enumeration type.
        /// </summary>
        /// <param name="enumType">Type of enumeration values</param>
        public EnumItemList(Type enumType)
            : this()
        {
            EnumType = enumType;
        }
        
        /// <summary>
        /// Creates a new EnumItemList. 
        /// </summary>
        /// <param name="enumType">Type of enumeration values</param>
        /// <param name="items">Collection to copy enumeration items from.</param>
        public EnumItemList(Type enumType, IEnumerable<EnumItem> items)
            : base(items)
        {
            SortBy = EnumItemsOrder.Text;
            EnumType = enumType;
        }
		 
	#endregion

    #region Private members


        private Type m_enumType;

        private TypeConverter m_converter;

        private Type m_resourceType;

        private readonly bool m_isInDesignerMode = (bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue);
    
    #endregion       
        
    #region Public methods

        /// <summary>
        /// Converts the Value of an <see cref="EnumItem"/> item to the enumeration type of this EnumItemList.
        /// </summary>
        /// <param name="item"></param>
        public void ConvertValueToEnumType(EnumItem item)
        {
            if (m_enumType != null && m_converter != null && item.Value != null && 
                (m_enumType.IsAssignableFrom(item.Value.GetType()) == false) && m_converter.CanConvertFrom(item.Value.GetType()) )
            {
                item.Value = m_converter.ConvertFrom(item.Value);
            }
        }

        /// <summary>
        /// Finds the EnumItem corresponding to the given enumeration value
        /// </summary>
        /// <param name="enumValue">Enumeration value to search for</param>
        /// <returns>The corresponding EnumItem or <c>null</c> if no match was found.</returns>
        public EnumItem FindItemByValue(object enumValue)
        {
            int index = IndexOfValue(enumValue);
            return index >= 0 ? this[index] : null;
        }

        /// <summary>
        /// Finds the index of the given enumeration value
        /// </summary>
        /// <param name="enumValue">The enumeration value to search for.</param>
        /// <returns>The zero-based index of the value or -1 if not found.</returns>
        public int IndexOfValue(object enumValue)
        {
            for (int index = 0; index < Count; index++)
            {
                if (Object.Equals(enumValue, this[index].Value)) return index;
            }
            return -1;
        }

        /// <summary>
        /// Finds an EnumItem with the given DisplayValue.
        /// </summary>
        /// <param name="displayValue">DisplayValue to search for</param>
        /// <returns>The EnumItem with the given DisplayValue or <c>null</c> if value not found
        /// </returns>
        public EnumItem FindItemByDisplayValue(object displayValue)
        {
            for (int index = 0; index < Count; index++)
            {
                EnumItem item = this[index];
                if (Object.Equals(item.DisplayValue, displayValue)) return item;
            }
            return null;
        }
        
     #endregion

    #region Public properties

        /// <summary>
        /// Gets or sets the type of enumeration that is handled by this EnumItemList.
        /// </summary>
        /// <remarks>
        /// When setting this property all existing values will be converted to the specified type. If the type
        /// is an <c>enum</c> the list will be populated with all available enum values.
        /// This property can only be set once.
        /// </remarks>
        /// <exception cref="System.ArgumentNullException">If value is <c>null</c>.</exception>
        /// <exception cref="InvalidOperationException">If the property has already been set to another value.</exception>
        public Type EnumType
        {
            get { return m_enumType; }
            set
            {
                
                // Skip setting in designer mode
                if(  m_isInDesignerMode)
                {
                    return;
                } 
                
                if (value == null) throw new ArgumentNullException();

                if (m_enumType == value) return;

                if (m_enumType != null) throw new InvalidOperationException("The EnumType value cannot be changed once set.");

                m_enumType = value;

                // Cache TypeConverter to increase performance.
                m_converter = TypeDescriptor.GetConverter(m_enumType);
                if (m_converter == null)
                {
                    throw new ArgumentException("No converter found for type "+m_enumType.FullName);
                }

                // Convert any existing item values to type.
                foreach (EnumItem item in this)
                {
                    ConvertValueToEnumType(item);
                }

                if (DefaultItem != null)
                {
                    ConvertValueToEnumType(DefaultItem);
                }

                //For enum types, fill list with values and their names 
                if (m_enumType.IsEnum)
                {
                    // For all public static fields, i.e. all enum values
                    foreach (FieldInfo field in m_enumType.GetFields(BindingFlags.Public | BindingFlags.Static))
                    {
                        object fieldValue = field.GetValue(null);
      
                        // Look for DescriptionAttributes.
                        object[] descriptions = field.GetCustomAttributes(typeof(DescriptionAttribute), true);

                        string displayName;
                        if (descriptions.Length > 0)
                        {
                            displayName = ((DescriptionAttribute)descriptions[0]).Description;
                        }
                        else
                        {
                            try
                            {
                                // Use type converter to support enums with custom TypeConverters.
                                displayName = m_converter.ConvertToString(fieldValue);
                            }
                            catch (Exception)
                            {
                                displayName = field.Name;
                            }
                        }
                        EnumItem item = new EnumItem() { Value = fieldValue, DisplayValue = displayName };
                        
                        if (IndexOfValue(item.Value) < 0)   // Do not add item if it already exists.
                        {
                            Add(item);
                        }
                    }
                }
                ReadValuesFromResources();
                OnPropertyChanged(new PropertyChangedEventArgs("EnumType"));
            }
        }

        /// <summary>
        /// Get or sets the type for the typed resource file to load localized enum value representations from.
        /// </summary>
        public Type ResourceType
        {
            get { return m_resourceType; }
            set 
            {
                if( value == m_resourceType ) return;
                m_resourceType = value;
                ReadValuesFromResources();
                OnPropertyChanged(new PropertyChangedEventArgs("ResourceType"));
            }
        }

        private void ReadValuesFromResources()
        {
            if (m_resourceType == null || m_enumType == null) return;
            ResourceManager resourceManager = new ResourceManager(m_resourceType);

            foreach( EnumItem item in this )
            {
                if( item.Value != null )
                {
                    String resourceName = m_enumType.Name+"_"+item.Value.ToString();
                    Object itemResource = resourceManager.GetObject(resourceName);
                    if( itemResource != null )
                    {
                        if (itemResource is Bitmap)
                        {
                            item.DisplayValue = ImageSourceHelpers.CreateFromBitmap((Bitmap)itemResource);
                        }
                        else if (itemResource is Icon)
                        {
                            item.DisplayValue = ImageSourceHelpers.CreateFromIcon((Icon)itemResource);
                        }
                        else
                        {
                            item.DisplayValue = itemResource;
                        }
                    }
                    else
                    {
                        Debug.Print("{0}: Failed to find a matching resource '{1}' for enum item '{2}' in resource file '{3}'.", 
                                    GetType().Name, resourceName, item.Value, resourceManager.BaseName);
                    }
                }
            }
            if (m_comparer != null) Sort(m_comparer);
        }

        /// <summary>
        /// Sorts the <see cref="EnumItemList"/> according to a custom comparer.
        /// </summary>
        /// <param name="comparer">Comparer to use</param>
        public void Sort(IComparer<EnumItem> comparer)
        {
            if (comparer == null) throw new ArgumentNullException();
            int newIndex = 0;
            foreach (EnumItem item in Items.OrderBy(k => k, comparer))
            {
                int oldIndex = Items.IndexOf(item);
                if (oldIndex != newIndex)
                {
                    Move(oldIndex, newIndex);
                }
                newIndex++;
            }
        }

        private class EnumItemComparer : IComparer<EnumItem>
        {
            private EnumItemsOrder m_sortBy;

            public EnumItemComparer(EnumItemsOrder sortBy)
            {
                m_sortBy = sortBy;
            }

            #region IComparer<EnumItem> Members

            public int Compare(EnumItem x, EnumItem y)
            {
                switch (m_sortBy)
                {
                    case EnumItemsOrder.Value:
                        return Comparer<Object>.Default.Compare(x.Value, y.Value);
                        
                    case EnumItemsOrder.DisplayValue:
                        return Comparer<Object>.Default.Compare(x.Value, y.Value);
                        
                    case EnumItemsOrder.Text:
                        return StringComparer.CurrentCultureIgnoreCase.Compare(x.Text, y.Text);
                    default:
                        return 0;
                }
            }

            #endregion
        }

        private IComparer<EnumItem> m_comparer;

        /// <summary>
        /// Gets or sets the comparer to use to sort the enum items.
        /// </summary>
        public IComparer<EnumItem> Comparer
        {
            get { return m_comparer; }
            set {
                m_comparer = value; OnPropertyChanged(new PropertyChangedEventArgs("Comparer"));
                if (m_comparer != null)
                {
                    Sort(m_comparer);
                }
            }
        }
        

        private EnumItemsOrder m_sortBy;

        /// <summary>
        /// Gets or sets the sort order to use.
        /// </summary>
        [Localizability(LocalizationCategory.None,Modifiability=Modifiability.Modifiable,Readability=Readability.Readable)]
        public EnumItemsOrder SortBy
        {
            get { return m_sortBy; }
            set {
                if (value == m_sortBy) return;
                Comparer = value == EnumItemsOrder.Custom ? null : new EnumItemComparer(value);
                m_sortBy = value; 
                OnPropertyChanged(new PropertyChangedEventArgs("SortBy"));
            }
        }
                 
        private EnumItem m_defaultItem;

        /// <summary>
        /// Gets or sets the default <see cref="EnumItem"/> that is returned when no other matching item is found
        /// during conversion.
        /// </summary>
        public EnumItem DefaultItem
        {
            get { return m_defaultItem; }
            set
            {
                if (value != null)
                {
                    ConvertValueToEnumType(value);
                }
                m_defaultItem = value;
                OnPropertyChanged(new PropertyChangedEventArgs("DefaultItem")); 
            }
        }
        

     #endregion
        
    #region Protected members

        /// <summary>
        /// Called when a new item is added from XAML or code.
        /// </summary>
        /// <param name="index">Index of new item.</param>
        /// <param name="item">Item to insert</param>
        /// <remarks>
        /// This is overridden to 
        /// 1) Convert value to EnumType
        /// 2) Ensure that there is only one item for each unique value. 
        /// In case there is already an item with the same value that one will be removed. 
        /// 3) Insert the item in correct order when the list is sorted.
        /// </remarks>
        protected override void InsertItem(int index, EnumItem item)
        {
            // Skip in designer mode
            if (m_isInDesignerMode)
            {
                return;
            } 

            if (item == null) throw new ArgumentNullException();
           
            ConvertValueToEnumType(item);
            
            // search for existing item with same value.
            int oldIndex = IndexOfValue(item.Value);
            // remove old item with same value.
            if (oldIndex >= 0)
            {
                
                RemoveAt(oldIndex);
                if (oldIndex < index)
                {
                    index--;
                }
            }

            if (m_comparer != null)
            {
                index = ((List<EnumItem>)Items).BinarySearch(item, m_comparer);
                if( index < 0 ) index = ~index;
            }
            
            base.InsertItem(index, item);

        }

        /// <summary>
        /// Called when an item is set through an indexer.
        /// </summary>
        /// <param name="index"></param>
        /// <param name="item"></param>
        /// <remarks>
        /// Overriden to ensure that item is not null, value is convertible to item type and that the item
        /// is inserted in the correct order when the list is sorted.
        /// </remarks>
        protected override void SetItem(int index, EnumItem item)
        {
            // Skip in designer mode
            if (m_isInDesignerMode)
            {
                return;
            } 

            RemoveAt(index);
            InsertItem(index, item);
        }

	#endregion
        
    #region IValueConverter Members

        /// <summary>
        /// Converts a enumeration value to the corresponding EnumItem found in this list
        /// </summary>
        /// <param name="value"></param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            
            // Skip in designer mode
            if (m_isInDesignerMode)
            {
                return value;
            } 

            if (targetType == null) throw new ArgumentNullException("Target type is null!");
            
            if (value != null && m_enumType != null && value.GetType() != m_enumType && m_converter != null && m_converter.CanConvertFrom(value.GetType()) )
            {
                value = m_converter.ConvertFrom(value);
            }
            EnumItem item = FindItemByValue(value);
            if (item == null)
            {
                if (DefaultItem != null)
                {
                    // Return a clone of the default item with the Value set to passed in value.
                    item = new EnumItem() { Value = value, DisplayValue = DefaultItem.DisplayValue, Text = DefaultItem.Text };
                }
                else if (value == null)
                {
                    return null;
                }
                else
                {
                    //throw new ArgumentException(String.Format("'{0}' was not recognized as a valid enumeration value. Set DefaultItem of the EnumList to get rid of this debug message", value));
                    Debug.Print(String.Format("'{0}' was not recognized as a valid enumeration value. Set DefaultItem of the EnumList to get rid of this debug message", value));
                    return null;
                }
            }
            
            if( targetType.IsAssignableFrom(typeof(EnumItem)) == false )
            {
                // Return display value directly if it is directly compatible with target type
                Object displayValue = item.DisplayValue;
                if (displayValue == null || targetType.IsAssignableFrom(displayValue.GetType()) )
                {
                    return displayValue;
                }
                // Else return EnumItem and let the EnumItemTypeConverter try to convert the value.
            }
            return item;
            
        }



        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            // Skip in designer mode
            if (m_isInDesignerMode)
            {
                return null;
            } 

            EnumItem item = value as EnumItem;
            if (item != null && Contains(item)) return item.Value;
            item = FindItemByDisplayValue(value);
            if (item != null) return item.Value;
            throw new ArgumentException(String.Format("'{0}' was not recognized as a valid enumeration value.", value));
        }


        #endregion
    }

    /// <summary>
    /// A generic version of <see cref="EnumItemList"/> which provides type-safe methods to add items
    /// programmatically.
    /// </summary>
    /// <typeparam name="TEnum">Type of enumeration to be managed by this list.</typeparam>
    public class EnumItemList<TEnum> : EnumItemList
    {
        /// <summary>
        /// Creates a new typed EnumItemList.
        /// </summary>
        public EnumItemList()
            : base(typeof(TEnum))
        {
        }

        /// <summary>
        /// Creates a new typed EnumItemList with the specified sorting order.
        /// </summary>
        /// <param name="order">Sorting order.</param>
        public EnumItemList(EnumItemsOrder order)
            : base(typeof(TEnum))
        {
            SortBy = order;
        }

        /// <summary>
        /// Creates a new typed EnumItemList with the specified items.
        /// </summary>
        /// <param name="items">Items to use.</param>
        public EnumItemList(IEnumerable<EnumItem> items)
            : base(typeof(TEnum), items)
        {
        }

        /// <summary>
        /// Changes how an enumeration value is represented.
        /// </summary>
        /// <param name="value">Enumeration value to add or change</param>
        /// <param name="displayValue">Display value to be used.</param>
        /// <param name="itemText">Text to be shown.</param>
        public void SetItem(TEnum value, object displayValue, string itemText)
        {
            Add(new EnumItem() { Value = value, DisplayValue = displayValue, Text = itemText });
        }

        /// <summary>
        /// Changes how an enumeration value is represented.
        /// </summary>
        /// <param name="value">Enumeration value to add or change</param>
        /// <param name="displayValue">Display value to be used.</param>
        /// <param name="itemText">Text to be shown.</param>
        public void SetItem(TEnum value, object displayValue)
        {
            Add(new EnumItem() { Value = value, DisplayValue = displayValue });

        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Sweden Sweden
Henrik Jonsson is a Microsoft Professional Certified Windows Developer (MCPD) that currently works as an IT consultant in Västerås, Sweden.

Henrik has worked in several small and large software development projects in various roles such as architect, developer, CM and tester.

He regularly reads The Code Project articles to keep updated about .NET development and get new ideas. He has contributed with articles presenting some useful libraries for Undo/Redo, Dynamic Linq Sorting and a Silverlight 5 MultiBinding solution.

Comments and Discussions