Home > OS >  Can I get an enum value having a given description attribute using a generic function?
Can I get an enum value having a given description attribute using a generic function?

Time:09-25

I'm creating a generic filtering user control to allow the user to apply various filters on a CollectionView, on an WPF app.

So, I have a filled CollectionView with entities, with properties. So, instead of creating a different user control for each entity, I came up with this:

foreach (PropertyInfo propertyInfo in _typeMessaging.Mensagem.GetProperties())
{
    var attrs = propertyInfo.GetCustomAttributes(true);
    foreach (object attr in attrs)
    {
        if (attr is DescriptionAttribute descr)
            fields.Add(new FilteringInfo() { Property = propertyInfo, Description = descr.Description }); ;
    }
}

foreach (FilteringInfo filteringInfo in fields.OrderBy(x => x.Property.Name))
{
    Columns.Add(filteringInfo);
}

So I just bind Columns to a combo box and the user can select which column (i.e. property) they want to filter their view by, all I need is to set the properties I want the user to be able to filter by with a description attribute. If the property type is string, DateTime, int or decimal, the user simply enters the info they want to filter by and it generates a filter to be applied on parent ViewModel's CollectionView. It then returns a FilteringInfo object to the parent ViewModel, which has the chosen PropertyInfo and the value the user wants to filter by preceded by a filtering word as a parameter.

This FilteringInfo is passed to a FiltersCollection which stores all the filters requested by the user and returns a Filter to be added to the CollectionView:

public class FiltersCollection
{
    private readonly GroupFilter _filtros = new();
    public Predicate<object> AddNewFilter(EntityBase entity)
    {
        FilteringInfo filteringInfo = entity as FilteringInfo;
        switch (filteringInfo.FilterInfo.Split(':')[0])
        {
            case "wholefield":
                _filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).Equals(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
                break;
            case "contains":
                _filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).Contains(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
                break;
            case "startswith":
                _filtros.AddFilter(x => x is EntityBase entityBase && ((string)entityBase.GetPropValue(filteringInfo.Property.Name)).StartsWith(filteringInfo.FilterInfo.Split(':')[1], StringComparison.OrdinalIgnoreCase));
                break;
            case "datebetween":
                string[] dates = filteringInfo.FilterInfo.Split(':')[1].Split(';');
                DateTime start = DateTime.Parse(dates[0]);
                DateTime end = DateTime.Parse(dates[1]).AddDays(1).AddSeconds(-1);
                _filtros.AddFilter(x => x is EntityBase entityBase && ((DateTime)entityBase.GetPropValue(filteringInfo.Property.Name)).IsBetween(start, end));
                break;
            case "valuebetween":
                string[] valuesBetween = filteringInfo.FilterInfo.Split(':')[1].Split(';');
                decimal startValue = decimal.Parse(valuesBetween[0]);
                decimal endValue = decimal.Parse(valuesBetween[1]);
                _filtros.AddFilter(x => x is EntityBase entityBase && ((decimal)entityBase.GetPropValue(filteringInfo.Property.Name)).IsBetween(startValue, endValue));
                break;
            case "enumvalue":
                _filtros.AddFilter(x => x is EntityBase entityBase && ((Enum)entityBase.GetPropValue(filteringInfo.Property.Name)).Equals(Enum.Parse(filteringInfo.Property.PropertyType, filteringInfo.FilterInfo.Split(':')[1])));
                break;
            case "abovevalue":
                string[] values = filteringInfo.FilterInfo.Split(':')[1].Split(';');
                if (filteringInfo.Property.PropertyType == typeof(int))
                {
                    int headValue = int.Parse(values[0]);
                    _filtros.AddFilter(x => x is EntityBase entityBase && (int)entityBase.GetPropValue(filteringInfo.Property.Name) >= headValue);
                }
                if (filteringInfo.Property.PropertyType == typeof(decimal))
                {
                    decimal headValue = decimal.Parse(values[0]);
                    _filtros.AddFilter(x => x is EntityBase entityBase && (decimal)entityBase.GetPropValue(filteringInfo.Property.Name) >= headValue);
                }
                break;
            case "clearfilters":
                _filtros.RemoveAllFilters();
                return null;
        }
        return _filtros.Filter;
    }
}

GroupFilter:

public class GroupFilter
{
    private List<Predicate<object>> _filters;

    public Predicate<object> Filter { get; private set; }

    public GroupFilter()
    {
        _filters = new List<Predicate<object>>();
        Filter = InternalFilter;
    }

    private bool InternalFilter(object o)
    {
        foreach (var filter in _filters)
        {
            if (!filter(o))
            {
                return false;
            }
        }

        return true;
    }

    public void AddFilter(Predicate<object> filter)
    {
        _filters.Add(filter);
    }

    public void RemoveFilter(Predicate<object> filter)
    {
        if (_filters.Contains(filter))
        {
            _filters.Remove(filter);
        }
    }

    public void RemoveAllFilters()
    {
        _filters.Clear();
    }
}

The issue is when the property the user wants to filter by is an enum. I can easily use a converter to populate the combo box with the enum's description attributes:

public class EnumDescriptionConverter : IValueConverter
{
    private string GetEnumDescription(Enum enumObj)
    {
        if (enumObj is null) return String.Empty;
        if (Enum.IsDefined(enumObj.GetType(), enumObj) is false) return String.Empty;

        FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());

        object[] attribArray = fieldInfo.GetCustomAttributes(false);

        if (attribArray.Length == 0)
        {
            return enumObj.ToString();
        }
        else
        {
            DescriptionAttribute attrib = attribArray[0] as DescriptionAttribute;
            return attrib.Description;
        }
    }

    object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Enum myEnum = (Enum)value;
        string description = GetEnumDescription(myEnum);
        return description;
    }
}

However, I'm having a hard time getting the Enum from a given description. I found https://stackoverflow.com/a/3422440/ which states I can use LINQ to iterate through Enum.GetValues(myEnum), but it requires passing the enum I want to evaluate, which the binding does not; as far as the converter knows, the target type it's trying to convert back to is just Enum.

I tried passing the list of enums used to populate the available values so ConvertBack could use it, but I was told bound data cannot be used as converter parameters. Is there a way I can do this? If not, are there other ways I could do it?

CodePudding user response:

If the targetType parameter in IValueConverter.Convert is really only giving you Enum, rather than the specific type of enumeration you need to convert to, then I don't think converting from the Description value alone will be possible. Actually, it might not be reliable anyway, because nothing is stopping anyone from creating two different values in the same enumeration and giving them identical descriptions (thus resulting in ambiguity).

Here's my suggestion: Instead of returning a string return a custom struct. Something like this:

public struct EnumValue
{
    public EnumValue(Enum value, string description)
    {
        Value = value;
        Description = description;
    }

    public Enum Value { get; }
    public string Description { get; }

    public override string ToString()
    {
        return Description;
    }
}

Returning something like the above instead of just the description string value, will allow you to convert back to the enumeration value just by reading the Value property.

(You could also go a step further and put the actual logic for retrieving the description into the EnumValue struct, removing the description parameter from the constructor.)

  • Related