1

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);
        }
    }
}
Community
  • 1
  • 1
Hank
  • 2,456
  • 3
  • 35
  • 83
  • First, searching an `ObservableCollection` is not different from searching a `List` or `IList` or even `IEnumerable`. Second, the only way you can search a collection without knowing the property names is using reflection, which is not recommended unless you have to. – Mohammad Dehghan Apr 05 '14 at 04:20
  • I think you will have to do search with the help of property names. Searching with just the search term wont be possible on this Observable Collection.But you can use reflection to do so. – Afaq Apr 05 '14 at 04:21
  • Ok fair enough. This is not case case I'm at right now, but planning forward this is what I'm thinking; what if I allow a user to perform their own queries and display in a DataGrid. Then they want to perform a filter, then the collection is not really known to me. Open to suggestions – Hank Apr 05 '14 at 04:46
  • Replace var yourObjectType with the type of the object you want to query (User in this case). Also replace the int test in the GetNavigationProperties method signature with string test. Also replace PredicateBuilder.False with PredicateBuilder.False. But if you are working with in memory objects and don't need to query a database i would go with Aarons answer. – user3411327 Apr 07 '14 at 06:59

2 Answers2

2

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);
Community
  • 1
  • 1
user3411327
  • 1,031
  • 8
  • 14
  • I have posted above how I have implemented your solution, however could you give me a hand with the PredicateBuilder class, I'm not quite experienced enough in this area to figure it out. – Hank Apr 06 '14 at 02:58
  • just having issues with GetNavigationExpression, I'm guessing that this is the same as GetNavigationPropertyExpression in your link http://stackoverflow.com/questions/22672050/dynamic-expression-tree-to-filter-on-nested-collection-properties/22685407#22685407 I have posted what I have done above – Hank Apr 07 '14 at 01:44
2

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");
Aron
  • 15,464
  • 3
  • 31
  • 64
  • Ok I will try this out as well. Never gone to this level before so please bear with me. Is it possible to see a more complete solution? For my experience it is a bit fragmented. – Hank Apr 07 '14 at 03:43
  • Am I supposed to be casting var properties in GetPropertyFunctions() to something and casting propertyAccessor? – Hank Apr 07 '14 at 05:09
  • No casting. Everything should be the right type.... But i didn't compile this code... Since i am at work. – Aron Apr 07 '14 at 05:17
  • Don't want to get you in trouble so maybe answer after work. For properties I'm getting: Cannot implicitly convert type 'System.Collections.Generic.IEnumerable>' to 'System.Collections.Generic.IEnumerable>'. An explicit conversion exists (are you missing a cast?) – Hank Apr 07 '14 at 05:41
  • For propertyAccessor I get: Cannot implicitly convert type 'System.Func' to 'System.Func' – Hank Apr 07 '14 at 05:42
  • Edited...messed up on the generics. I had `User` instead of `T` at one point. Coding without an IDE straight into SO is hard! – Aron Apr 07 '14 at 05:44
  • Changed GetPropertyfunc as well, it works, definitely have to cache the properties. Just tried altering: bool match = properties.Any(prop => prop(item).Contains(searchTerm)); keeps throw null exception, how do you implement something like that? – Hank Apr 07 '14 at 06:57
  • It sounds like your item may have `null` for some of the values. So you really want this... `bool match = properties.Select(prop => prop(user)).Any(value => value != null && value.Contains(searchTerm));` – Aron Apr 07 '14 at 07:10
  • This runs well, every now and then search on a numeric would come in handy, is it possible to include numeric properties as well? But just in a text comparison rather numeric. – Hank Apr 07 '14 at 07:55
  • Fair enough too, to be continued at: http://stackoverflow.com/questions/22907052/how-to-expand-text-search-on-class-properties-to-include-numeric-text-comparisio – Hank Apr 07 '14 at 08:20