Click here to Skip to main content
Click here to Skip to main content

Visual Expression Builder for Dynamic Linq

By , 2 Feb 2012
 

Updated! The source code has been updated to make the dynamic types more robust!

Table of Contents

ScreenShot.png

Introduction

Linq is a great way to declaratively filter and query data in in a Type-Safe, Intuitive, and very expressive way. However, your users should not have to call you every time they have a new way to filter or query their data.

So here is a way for your users to filter their data no matter where that data comes from. Whether its Linq2SQL or an in-memory structure, now your users can have the power.

It is the most valuable feature that you can add for your users in 4 lines of code.
I would also recommend actually reading the code. The entire project has only 233 lines of code in it - that's including the XAML.

Using the Code

The real functionality is in the two classes in the ViewModel. The UserControl is just a glorified DataTemplate for the ExpressionList.

ClassDiagram.png

I know that most of you will want to know how this bad boy works, so you can use the same techniques in your own solutions. However, you could just use the UserControl as-is by adding it straight into your XAML, and binding it to an ExpressionList, so that is what I will start with.

<uc:FilterUserControl DataContext="{Binding ExpressionList1}" />

Somewhere in your ViewModel:

public     ExpressionList ExpressionList1    { get; set; } 
ExpressionList1 = new ExpressionList() {Type=typeof(Contact)} ; 

Then once your user selected all of the filters, you get your final query like this:

 var filteredContacts = 
    contacts.Where(ExpressionList1.GetCompleteExpression<Contact>()); 

Background

I first tried many other Dynamic Linq solutions. There is the "DynamicLinq" project explained in ScottGu's Blog which allows you to parse expressions in strings. So your users could write their own expressions in a text box which you could then apply to your Linq. I learned a lot about expressions from their implementation.

Then, there is the solution I almost used - The Dynamic Linq tool from the VB tool. It did almost exactly what I needed it to do, and you will notice that it looks a lot like the tool that I ended up creating which I am showing here today. The biggest problem is that it was written for WinForms and the functionality was too interwoven with the presentation. I actually needed one of the main benefits of MVVM (previously MVP) - The ability to persist the state even when the UI disappears and easily create default and saved sets of filters.

What Exactly is Happening?

As I said before, the real functionality is in the two classes in the ViewModel (I considered the builtin Expression class to be my model):

  • ExpressionVM contains the necessary data and functionality to create an expression with one comparison.
  • ExpressionList is an observable collection of ExpressionVMs with one property to create a lambda expression out of all the child expressions.

ExpressionList is Just an ObservableCollection<ExpressionVM> with two important properties:

  • Type Type - which is used in making the expression and used to populate the AvailableProperties in the ExpressionVMs.
  • Expression CompleteExpression - which combines the Expressions from all of the ExpressionVMs into one lambda expression.

ExpressionVM's members are:

  • Type ObjectType - The type being filtered (usually set from the ExpressionList)
  • PropertyInfo PropertyInfo - The information about the property that the user chose to compare on this line.
  • string PropertyName - (readonly) comes from the PropertyInfo.
  • Type PropertyType - (readonly) comes from the PropertyInfo.
  • CombineOperator - chosen by the user to determine how to combine this line with the previous line
  • CompareOperator - chosen by the user to determine how to compare the property with the constant
  • object Value - The constant to be used in the comparison
  • AvailableCombineOperators, AvailableCompareOperators, AvailableProperties - lookup list to populate the ComboBoxes.
  • GetSupportedTypes - So that we don't give the user the option to filter by image :)
  • MakeExpression -Creates an expression based on the choices of the user

The Meat (or Tofu) and Potatoes of the Solution

The most interesting code is contained in two functions MakeExpression and the getter for CompleteExpression.

public LambdaExpression MakeExpression(ParameterExpression paramExpr = null)
{
    if(paramExpr == null) paramExpr = Expression.Parameter(ObjectType, "left");
            
    var callExpr = Expression.MakeMemberAccess(paramExpr, PropertyInfo);
    var valueExpr = Expression.Constant(Value, PropertyType);
    var expr = Expression.MakeBinary((ExpressionType)CompareOperator, 
        callExpr, valueExpr);
            
    return Expression.Lambda( expr, paramExpr);
}
public Expression CompleteExpression
{
    get
    {
        var paramExp = Expression.Parameter(Type, "left");
        if (this.Count == 0) return Expression.Lambda
        (Expression.Constant(true), paramExp);
        LambdaExpression lambda1 = this.First().MakeExpression(paramExp);
        var ret = lambda1.Body;
        foreach (var c in this.Skip(1))
            ret = Expression.MakeBinary((ExpressionType)c.CombineOperator, 
        ret, c.MakeExpression(paramExp).Body);
        return Expression.Lambda(ret, paramExp);
    }
} 

The CompleteExpression creates an input parameter out of the ObjectType and a lambda out of the first ExpressionVM. Then it loops through the rest of the ExpressionVMs appending the Expressions it gets from their MakeExpressions to the lambda based on their CombineOperator.

Other Interesting Code Snippets

I provide lookup lists for each ComboBox.

The AvailableProperties is populated every time the ObjectType changes.

set { 
        _ObjectType = value;
        AvailableProperties = from p in value.GetProperties()
                    where GetSupportedTypes().Contains(p.PropertyType)
                    select p;
        OnPropertyChanged("ObjectType");
    }

The CombineOperator and the CompareOperator are both enums. So their lists are generated on the fly like this:

public Array AvailableCompareOperators
{
    get { return Enum.GetValues(typeof(ComparisonOperators)); }
}

To Do

Yes, there is still a lot left to do, and there is a lot of room for improvement.
Here is a short list that I came up with:

  • Add more operators
  • Allow ANDs and ORs to be nested
  • Allow sub properties to be selected
  • Make WPF select specialized templates based off the PropertyType

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Rabb Moshe Plotkin
United States United States
Member
Co-director of Chabad Student Center @ SUNY New Paltz
Lecturer of Computer Science @ SUNY New Paltz

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionIt doesnt work what is my mistake ?membersven197825 Nov '12 - 7:57 
Hi.
 
The gui works but the compiler stops at GetCompleteExpression
 
public List<User> Users { get; set; }  // my List User is a class with some props..

.......
 
var filter = this.Users.Where(ExpressionList1.GetCompleteExpression<User>());
 
The compiler says there is no overload method for where and the "System.Linq.Enumerable.Where(System.Collections.Generic.IEnumerable, System.Func)" has wrong arguments.
 
Here is the orginal output : ( Sorry , it's German , I cannot install English pack because its only express edition.
 
Fehler  1   Instanzenargument: Konvertierung von 'System.Collections.Generic.List<WpfApplication1.User>' in 'System.Linq.IQueryable<WpfApplication1.User>' ist nicht möglich.   C:\Users\Sven\Downloads\Sourcecode\WpfApplication1\MainWindow.xaml.cs   52  27  WpfApplication1
Fehler  2   "System.Collections.Generic.List<WpfApplication1.User>" enthält keine Definition für "Where", und die Überladung der optimalen Erweiterungsmethode "System.Linq.Enumerable.Where<TSource>(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,int,bool>)" weist einige ungültige Argumente auf.    C:\Users\Sven\Downloads\Sourcecode\WpfApplication1\MainWindow.xaml.cs   52  26  WpfApplication1
Fehler  3   2-Argument: Kann nicht von "System.Linq.Expressions.Expression<System.Func<WpfApplication1.User,bool>>

AnswerRe: It doesnt work what is my mistake ?membersven197826 Nov '12 - 7:17 
Ok solved it.
 
Change the line from :
 
var filter = this.Users.Where(ExpressionList1.GetCompleteExpression());
 
to
 
var filter = this.Users.Where(ExpressionList1.GetCompleteExpression()Compile());
 
Smile | :) Smile | :)
QuestionI added 'StartsWith' and 'EndsWith' to MakeExpressionmemberPaul Brower23 Mar '12 - 9:27 
Don't bother answering any of my other questions ... got it all figured out. I did end up adding the ability to search strings with 'StartsWith' or 'EndsWith'. Thought someone may find this helpful, so here is the code:
 

        public LambdaExpression MakeExpression(ParameterExpression paramExpr = null)
        {
            if (paramExpr == null) paramExpr = Expression.Parameter(ObjectType, "left");
 
            var callExpr = Expression.MakeMemberAccess(paramExpr, PropertyInfo);
            var valueExpr = Expression.Constant(Value, PropertyType);
            Expression expr;
 
            if (CompareOperator == ComparisonOperators.Contains)
            {
                expr = Expression.Call(callExpr, PropertyType.GetMethod("Contains"), valueExpr);
            }
            else if (CompareOperator == ComparisonOperators.StartsWith)
            {
//NOTE:  You need to add ComparisonOperator.StartsWith
                string theValue = Convert.ToString(valueExpr);
                theValue = theValue.Replace("\"","");
                ConstantExpression val = Expression.Constant(theValue, typeof(String));
                ConstantExpression comp = Expression.Constant(StringComparison.CurrentCultureIgnoreCase,
                                                              typeof (StringComparison));
                expr = Expression.Call(callExpr,
                                       typeof (string).GetMethod("StartsWith",
                                                                 new[] {typeof (string), typeof (StringComparison)}),
                                       val, comp);
            }
            else if (CompareOperator == ComparisonOperators.EndsWith)
            {
//NOTE:  You need to add ComparisonOperator.EndsWith
                string theValue = Convert.ToString(valueExpr);
                theValue = theValue.Replace("\"", "");
                ConstantExpression val = Expression.Constant(theValue, typeof(String));
                ConstantExpression comp = Expression.Constant(StringComparison.CurrentCultureIgnoreCase,
                                                              typeof(StringComparison));
                expr = Expression.Call(callExpr,
                                       typeof(string).GetMethod("EndWith",
                                                                 new[] { typeof(string), typeof(StringComparison) }),
                                       val, comp);
            }
            else
            {
                expr = Expression.MakeBinary((ExpressionType)CompareOperator, callExpr, valueExpr);
            }
            return Expression.Lambda(expr, paramExpr);
        }

QuestionWinForms version?memberPaul Brower21 Mar '12 - 4:27 
I'm really interested in being able to use this control in a WinForms application. Do you know how I could convert this to be a standard WinForms user control?
AnswerRe: WinForms version?memberPaul Brower22 Mar '12 - 2:29 
I was able to convert all this to run in a Winforms environment. (Took me most of last night!).
 
I have everything working the way I need it to now, EXCEPT I cannot figure out how to modify the following method to ignore case
 
        public LambdaExpression MakeExpression(ParameterExpression paramExpr = null)
        {
            if (paramExpr == null) paramExpr = Expression.Parameter(ObjectType, "left");
 
            var callExpr = Expression.MakeMemberAccess(paramExpr, PropertyInfo);
            var valueExpr = Expression.Constant(Value, PropertyType);
            Expression expr;
 
            if (CompareOperator == ComparisonOperators.Contains)
            {
                expr = Expression.Call(callExpr, PropertyType.GetMethod("Contains"), valueExpr);
            }
            else
            {
                expr = Expression.MakeBinary((ExpressionType)CompareOperator, callExpr, valueExpr);
            }
            return Expression.Lambda(expr, paramExpr);
        }
 
I think using the 'IndexOf' method instead of 'Contains' would work, but that requires an additional argument on the Call (StringComparison.ComparisonType). Any ideas how to make this work?
QuestionRe: WinForms version?memberbsculley26 Mar '12 - 12:34 
Hi. Are you interested in sharing the Winforms version you created? I'm about to try the same thing and could do without the sleepless night.
 
I'll be happy to share any improvements I might make.
AnswerRe: WinForms version?memberPaul Brower26 Mar '12 - 15:33 
Here you go ... knock yourself out: http://dl.dropbox.com/u/56170812/WinFormVisualExpressionBuilder.zip[^]
 
Note: I ripped all this out a a current assembly and made it work, but the TextBox control needs to have TAB hit after you enter a value before the databinding will accept it. Also, the buttons on the filter form don't have an image ... one is to add one is to remove. I'm sure this will help. Don't have time to do a full blown article right now.
QuestionProject VSmembermdenicola1 Mar '12 - 6:42 
Can you put a Sample Project?
 
thank you
 
mdn
AnswerRe: Project VSmemberksecrist19 Jul '12 - 10:06 
The Engine is all here, but it would nice to see the user-interface portion where the aspx page is using the web control etc etc...
GeneralMy vote of 5memberDean Oliver5 Feb '12 - 23:11 
very useful
GeneralMy vote of 4memberHoyaSaxa933 Feb '12 - 6:37 
Very promising work... Look forward to seeing its progress.
QuestionSample ProjectmemberShaun Brown16 Aug '11 - 6:45 
Hi there - I'm having difficulty getting this working in a test WPF project. Do you have a sample project showing how this works please?
 
Shaun
GeneralContains/LikememberNeil Alderson21 Jul '10 - 0:11 
Really great work, very useful indeed!
 
The only operator that I think would be very useful to add is Contains or Like. Is this possible?
 
Also some of the operators don't always apply to the field type. E.g. a string field cannot have GreaterThan applied to it.
 
Cheers,
Neil Alderson
GeneralRe: Contains/LikememberMoshe Plotkin21 Jul '10 - 2:28 
Great.
 
Add that functionality and email it to me and I will upload it. Blush | :O
Rabbi Moshe Plotkin
Co-director of Chabad Student Center @ SUNY New Paltz
Lecturer of Computer Science @ SUNY New Paltz

GeneralRe: Contains/LikememberNeil Alderson6 Aug '10 - 1:33 
Hi,
I've played with the code in a project I'm working on and have added the following improvements:
 
1) Added an Attribute that can be applied to properties you DON'T want the user to query on
2) Added "Contains" operator for string queries
3) Modified AvailableCompareOperators to be dynamic depending on type to disallow illegal operators for the type
4) Changed the TextBox bound to Value to be ContentControl and added a ValueControl property that chooses an appropriate control for the selected PropertyType
 
Not sure how to email the changes over to you so let me know if you want them and how to send them.
 
Regards,
Neil
GeneralRe: Contains/LikememberRabb Moshe Plotkin6 Aug '10 - 2:20 
WOW!!! That is Awesome!
 
my email address is jewpaltz at gmail.com
just zip it and send it
 
I will post your changes and mention you in the beginning of the article
GeneralMy vote of 4membercdkisa2 Jul '10 - 13:32 
This has great potential!
GeneralGood startmvpJosh Fischer1 Jul '10 - 10:51 
I like where you are going with this, but it definitely needs some work to be a little more robust and general purpose. You may want to make the code more API’ish (yeah, I just made that word up) or reformat the article to talk more about how expressions and lambdas helped you solve the problem and let people take it from there.
Good job though; I was particularly impressed with how little code it took to get this working.
Josh Fischer

GeneralRe: Good startmemberMoshe Plotkin1 Jul '10 - 12:29 
B"H
 
Yes. After I posted the article I started to see a few issues with the late type checking particularly from within WPFs Data Binding.
f course my unit tests ran perfectly and it wasn't until I put everything together did I find the issues.
 
But anyway, I have fixed those issues. Very proud of myself Shucks | :-> and I did it once again with minimal code.
That is kinda my thing. Big ideas with little code.
It even works with WPF validation (for instance if you type an invalid date once you've selected a property of type date)
 
I had to create a kinda variant for C# to make it work right.
 
I am going to upload a working project in a moment and then I will add explanations to the article when I get a chance.
GeneralWow!memberhmlch30 Jun '10 - 23:00 
Just WOW! Thanks!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 2 Feb 2012
Article Copyright 2010 by Rabb Moshe Plotkin
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid