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?