简体   繁体   中英

How to search ObservableCollection for text match

I would like to search an observable collection for a text match. The collection is made up of a class with about five properties.

I was hoping that I could do this without knowing the specific property names of the class. This way I could use this search on any collection.

Either literally doing a iterative search or using a filter.

After finding the items I will be setting the one property that is always present 'IsSelected' to true;

Is this possible? If so how?


[EDIT] After trying the following, due to my inexperience, I'm having an issue with forming the PredicateBuilder class, not sure where to start

User uvm = new User();
var yourObjectType = uvm.GetType();

var properties = yourObjectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty).Where(p => p.PropertyType == typeof(string)).Select(p => p.Name).ToList();
var param = System.Linq.Expressions.Expression.Parameter(yourObjectType, "p");
var seed = PredicateBuilder.False<User>();
var whereExpression = properties.Aggregate(seed, (p1, p2) => p1.Or(GetNavigationExpression(param, SearchTextboxValue, p2)));
var match = UserCollection.Where(whereExpression).ForEach(i => i.IsSelected = true);


[EDIT 2] The following has an issue with GetNavigationExpression not being in context. This is from my ViewModel on clicking a button it executes.

private void SelectTextMatchCommandExecute(object parameter)
{
    string textSearchValue = (string)parameter;

    UserViewModel uvm = new UserViewModel();
    var yourObjectType = uvm.GetType();

    var properties = yourObjectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty).Where(p => p.PropertyType == typeof(string)).Select(p => p.Name).ToList();
    var param = System.Linq.Expressions.Expression.Parameter(yourObjectType, "p");
    var seed = PredicateBuilder.False<UserViewModel>();
    var whereExpression = properties.Aggregate(seed, (p1, p2) => p1.Or(GetNavigationExpression(param, textSearchValue, p2)));
    var match = UserCollection.Where(whereExpression).ForEach(i => i.IsSelected = true);
}

I have placed all of your suggestions into the following, I used a copy of GetNavigationExpressionProperties() from here and renamed it GetNavigationProperties(). I just assumed it all need to be together as MakeLambda() uses ParameterVisitor().

I guess I'm just having trouble piecing all this together

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace UserManagement
{
    /// <summary>
    /// Enables the efficient, dynamic composition of query predicates.
    /// </summary>
    public static class PredicateBuilder
    {
        /// <summary>
        /// Creates a predicate that evaluates to true.
        /// </summary>
        public static Expression<Func<T, bool>> True<T>() { return param => true; }

        /// <summary>
        /// Creates a predicate that evaluates to false.
        /// </summary>
        public static Expression<Func<T, bool>> False<T>() { return param => false; }

        /// <summary>
        /// Creates a predicate expression from the specified lambda expression.
        /// </summary>
        public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; }

        /// <summary>
        /// Combines the first predicate with the second using the logical "and".
        /// </summary>
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, System.Linq.Expressions.Expression.AndAlso);
        }

        /// <summary>
        /// Combines the first predicate with the second using the logical "or".
        /// </summary>
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
        {
            return first.Compose(second, System.Linq.Expressions.Expression.OrElse);
        }

        /// <summary>
        /// Negates the predicate.
        /// </summary>
        public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
        {
            var negated = System.Linq.Expressions.Expression.Not(expression.Body);
            return Expression.Lambda<Func<T, bool>>(negated, expression.Parameters);
        }

        /// <summary>
        /// Combines the first expression with the second using the specified merge function.
        /// </summary>
        static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
        {
            // zip parameters (map from parameters of second to parameters of first)
            var map = first.Parameters
                .Select((f, i) => new { f, s = second.Parameters[i] })
                .ToDictionary(p => p.s, p => p.f);

            // replace parameters in the second lambda expression with the parameters in the first
            var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);

            // create a merged lambda expression with parameters from the first expression
            return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
        }

        class ParameterRebinder : ExpressionVisitor
        {
            readonly Dictionary<ParameterExpression, ParameterExpression> map;

            ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
            {
                this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
            }

            public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
            {
                return new ParameterRebinder(map).Visit(exp);
            }

            protected override Expression VisitParameter(ParameterExpression p)
            {
                ParameterExpression replacement;

                if (map.TryGetValue(p, out replacement))
                {
                    p = replacement;
                }

                return base.VisitParameter(p);
            }
        }

        private class ParameterVisitor : ExpressionVisitor
        {
            public Expression Parameter
            {
                get;
                private set;
            }
            protected override Expression VisitParameter(ParameterExpression node)
            {
                Parameter = node;
                return node;
            }
        }

        public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            foreach (var item in source)
                action(item);

            return source;
        }

        public static Expression GetNavigationExpression(Expression parameter, int test, params string[] properties)
        {
            Expression resultExpression = null;
            Expression childParameter, navigationPropertyPredicate;
            Type childType = null;

            if (properties.Count() > 1)
            {
                //build path
                parameter = Expression.Property(parameter, properties[0]);
                var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
                //if it´s a collection we later need to use the predicate in the methodexpressioncall
                if (isCollection)
                {
                    childType = parameter.Type.GetGenericArguments()[0];
                    childParameter = Expression.Parameter(childType, childType.Name);
                }
                else
                {
                    childParameter = parameter;
                }
                //skip current property and get navigation property expression recursivly
                var innerProperties = properties.Skip(1).ToArray();
                navigationPropertyPredicate = GetNavigationExpression(childParameter, test, innerProperties);
                if (isCollection)
                {
                    //build methodexpressioncall
                    var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
                    anyMethod = anyMethod.MakeGenericMethod(childType);
                    navigationPropertyPredicate = Expression.Call(anyMethod, parameter, navigationPropertyPredicate);
                    resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
                }
                else
                {
                    resultExpression = navigationPropertyPredicate;
                }
            }
            else
            {
                //Formerly from ACLAttribute
                var childProperty = parameter.Type.GetProperty(properties[0]);
                var left = Expression.Property(parameter, childProperty);
                var right = Expression.Constant(test, typeof(int));
                navigationPropertyPredicate = Expression.Equal(left, right);
                resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
            }
            return resultExpression;
        }

        private static Expression MakeLambda(Expression parameter, Expression predicate)
        {
            var resultParameterVisitor = new ParameterVisitor();
            resultParameterVisitor.Visit(parameter);
            var resultParameter = resultParameterVisitor.Parameter;
            return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
        }
    }
}

With the code from here you could do it like this using reflection:

var properties = yourObjectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty).Where(p => p.PropertyType == typeof(string)).Select(p => p.Name).ToList();
var parameter = Expression.Parameter(yourObjectType,"p");
var seed = PredicateBuilder.False<yourObjectType>();
var whereExpression = properties.Aggregate(seed, (p1,p2) => p1.Or(GetNavigationExpression(parameter, yourSearchTerm,p2));
var match = yourCollection.Where(whereExpression).ForEach(i => i.IsSelected = true);

Edit: The PredicateBuilder was taken from here . The ForEach extension can be taken from namespace Microsoft.Practices.Prism.ObjectBuilder or

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    if (action == null)
        throw new ArgumentNullException("action");

    foreach (var item in source)
        action(item);

    return source;
}

If you want to change the search predicate you would have to make it more dynamic. For example with an enum like this

public enum OperatorComparer
{
    Equals = ExpressionType.Equal,
    Contains,
    StartsWith,
    GreaterThan = ExpressionType.GreaterThan
}

  var childProperty = parameter.Type.GetProperty(properties[0]);
    var left = Expression.Property(parameter, childProperty);
    var right = Expression.Constant(test, typeof(int));
    if(!new List<OperatorComparer>{OperatorComparer.Contains,OperatorComparar.StartsWith}.Contains(operatorComparer))
   {
        navigationPropertyPredicate = Expression.MakeBinary((ExpressionType)operatorComparer,left, right);
   }
   else
   {
      var method = GetMethod(value, operatorComparer); //get property by enum-name from type
      navigationPropertyPredicate = Expression.Call(left, method, right);
   }
    resultExpression = MakeLambda(parameter, navigationPropertyPredicate);

There is very little reason to use the System.Linq.Expressions namespace, given that you are working on in-memory collections. Granted I understand that you prefer not to use reflection to access each property, however you can use

public bool Match<T>(T item, string searchTeam)
{
    //You should cache the results of properties here for max perf.
    IEnumerable<Func<T, string>> properties = GetPropertyFunctions<T>();
    bool match = properties.Any(prop => prop(item) == "Foobar");
    return match;
}
public IEnumerable<Func<T, string>> GetPropertyFunctions<T>()
{
    var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty).Where(p => p.PropertyType == typeof(string)).ToList();
    var properties = propertyInfos.Select(GetProperyFunc<T>);
    return properties;        
}

public Func<T, string> GetProperyFunc<T>(PropertyInfo propInfo)
{
    ParameterExpression x = Expression.Parameter(typeof(User), "x");
    Expression<Func<User, string>> expression = Expression.Lambda<Func<User, string>>(Expression.Property(x, propInfo), x);
    Func<User, string> propertyAccessor = expression.Compile();
    return propertyAccessor;
}

I would then just use LinqToObjects to work with this code instead. Getting the LinqToObject compiler to compile down to Expressions you have above would in theory produce (very slightly) faster IL, but you would lose all your perf in the actual compilation stage. Not to mention you could cache the propertyAccessors for speed. Plus its much simplier code to work with.

*NB the perf hit here would be that each property accessor would be wrapped in a Func<User,string> . http://www.codeproject.com/Articles/584720/ExpressionplusbasedplusPropertyplusGettersplusandp

For example...

List<Func<User, string>> properties = ...
User user = ...
bool match = properties.Any(prop => prop(user) == "Foobar");

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM