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.)