Updated! The source code has been updated to make the dynamic types more robust!
Table of Contents
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.
The real functionality is in the two classes in the ViewModel
. The UserControl
is just a glorified DataTemplate
for the ExpressionList
.
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>());
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.
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 ExpressionVM
s. - Expression
CompleteExpression
- which combines the Expressions
from all of the ExpressionVM
s 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 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 ExpressionVM
s appending the Expressions it gets from their MakeExpression
s to the lambda based on their CombineOperator
.
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 enum
s. So their lists are generated on the fly like this:
public Array AvailableCompareOperators
{
get { return Enum.GetValues(typeof(ComparisonOperators)); }
}
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