1

This is the UI of my application. It is a WPF based application that analyzes between devices (Something very similar to Wireshark).enter image description here

The class that is being binded to the DataGrid is the following (Everything except Error and FuncType is bound to the grid:)

public class CommDGDataSource
{
    public int Number { get; set; }
    public string Time { get; set; }
    public string Protocol { get; set; }
    public string Source { get; set; }
    public string Destination { get; set; }
    public string Data { get; set; }
    public bool Error { get; set; }
    public FunctionType FuncType { get; set; }
}

Basically, I'm trying to design something where users can enter certain filter commands and only the rows that matches the condition should be displayed. Here are some examples (without the quote),

  1. Entering "Error" should only display the datarows whose Error property has been set to true

  2. Entering "source==someipaddress" should only display the matching IP address

  3. Entering "number>100" should only display rows with number greater than 100

  4. Multiple conditions should apply if separated by comma. (Error,source==someipaddress)

I've already created ICollectionView for my binding data to handle the filtering but I'm not sure about the approach I should take to parse the commands and properly handle the filtering to meet the above requirements.

Any guidance would be appreciated.

TtT23
  • 6,876
  • 34
  • 103
  • 174

3 Answers3

3

The 10,000 feet overview

Filtering data in a DataGrid is done by attaching a handler to the CollectionViewSource.Filter event, so the actual problem is how to create a FilterEventHandler from user input. This problem can be solved in a powerful manner with expression trees.

I 'm not going to go into detail with regards to how to parse the input string because that can range from very simple to very complicated; a powerful approach would be to use a tool like ANTRL to parse the input into an abstract syntax tree.

What I will show is how you can create a filter given that the input is parsed and the user's intent is known (it is also quite easy to create the filter on the fly while parsing).

Let's assume that the user entered "Number > 10" for the filter. If this filter were hardcoded, the FilterEventHandler would look like this:

public void CustomFilterHandler(object sender, FilterEventArgs e)
{
    CommDGDataSource source = (CommDGDataSource)e.Item;
    return source.Number > 10;
}

Constructing a filtering event handler dynamically

What we 're going to do here is dynamically construct this method on the fly using expression trees. Let's begin with some preamble:

var sourceType = typeof(CommDGDataSource);
var eventArgsType = typeof(System.Windows.Data.FilterEventArgs);

We 're going to be constructing a LambdaExpression which has two parameters and a body. Let's construct the parameters first:

var parameters = new[] {
    Expression.Parameter(typeof(object), "sender"),
    Expression.Parameter(eventArgsType, "e"),
};

The body is going to be a BlockExpression (this is not strictly necessary, but could come in handy later on). A BlockExpression uses a number of variables and is comprised of any number of other expressions; very importantly, the last of these will be the block's return value.

The first line of the mock event handler given above tells us that we need one variable:

var variable = Expression.Variable(sourceType, "source");

And we definitely need a predicate that produces the return value:

var predicate = Expression.GreaterThan(
    Expression.MakeMemberAccess(variable, sourceType.GetProperty("Number")),
    Expression.Constant(10));

We are now ready to produce the body of the BlockExpression. The body will need to access the Item property of the second parameter of the event handler (parameters[1]) and cast it to sourceType because FilterEventArgs.Item is of type object, so we can't use it directly. The result of the cast should be stored in variable and then predicate will perform the test:

// Some intermediate variables to cut down on the line length
var itemProperty = eventArgsType.GetProperty("Item");
var itemAccessExpression = Expression.MakeMemberAccess(parameters[1], itemProperty);
var castItemToCorrectType = Expression.TypeAs(itemAccessExpression, sourceType);

// And the body is comprised of these two expressions:
var body = new[] { Expression.Assign(variable, castItemToCorrectType), predicate };

Since predicate is the last expression of the body it will also produce its return value.

We can now construct the block expression and then finally the filtering lambda expression itself:

var block = Expression.Block(new[] { variable }, body);
var filter = Expression.Lambda<Func<object, FilterEventArgs, bool>>(block, parameters);

Plugging in the filter

One of the reasons we went to all this trouble is that now the compiler can construct a "real" method from the filter expression for us automatically! We can then simply plug it into the CollectionViewSource.Filter event and enjoy filtering:

// It's a good idea to keep a reference to this around for now,
// so that it can be removed from the event handler later.
var filterMethod = filter.Compile();

collectionViewSource.Filter += filterMethod;

Constructing complicated filters

The other reason to go with this approach is that it's quite easy to extend the filtering logic. For example it's easy to imagine that instead of hardwiring the predicate like this:

var predicate = Expression.GreaterThan(
    Expression.MakeMemberAccess(variable, sourceType.GetProperty("Number")),
    Expression.Constant(10));

We could construct it from user input like this:

// Maps operators to Expression factory methods
var dict = new Dictionary<string, Func<Expression, Expression, BinaryExpression>>
{
    { "==", Expression.Equal },
    { ">", Expression.GreaterThan },
    { "<", Expression.LessThan },
    // etc
};

var predicate = Expression.Constant(true); // by default accept all rows

// Logical AND everything in parseResult to construct the final predicate
foreach (parseResult in parsedUserInput)
{
    var exprConstructor = dict[parseResult.Operator];
    var property = sourceType.GetProperty(parseResult.PropertyName);
    var target = Expression.MakeMemberAccess(variable, property);
    var additionalTest = exprConstructor(target, Expression.Constant(parseResult.Value));
    predicate = Expression.AndAlso(predicate, additionalTest);
}
Jon
  • 428,835
  • 81
  • 738
  • 806
  • Exceptional answer. Really appreciate your effort. – TtT23 Aug 14 '12 at 11:51
  • This is looking good to me, except for the last part. `collectionViewSource.Filter += filterMethod;` does not compile, and complains that it `Can't convert Func to Predicate` – Peter M May 19 '17 at 13:25
  • @PeterM then it seems what you have on the left hand side in your code is not an event, as no event would ever accept a `Predicate` as a handler. – Jon May 22 '17 at 20:04
  • Yeah I agree that it is not an event. And I see my confusion now. `CollectionViewSource.Filter` is an *event* and is constructed as per your code, but `CollectionView.Filter` (which I am using) is a *predicate* with the signature I mentioned. I was working with WPF examples that applied the predicate when I went looking for expression trees and found your code, and I conflated the two ideas due to the similarity in the class names. (BTW interested in taking stab at my current issue? https://stackoverflow.com/q/44089953/31326) – Peter M May 22 '17 at 20:36
0

After you click Start you should raise notify change event for the collection the grid is bound to, let's say Devices. The getter for Devices will apply the filter and return only the filtered records.

void OnStartClicked(object sender, EventArgs e)
{
   NotifyPropertyChanged("Devices");
}

public IEnumberable<Device> Devices
{
   get 
   {
      if(string.IsNullOrEmpty(Filter)) return _devices;
      return _devices.Where(d => d.Satisfies(Filter));
      // or
      return _devices.Where(d => _filter.Satisfies(d, Filter));
   }
}

And in Device class or in some filtering class you will define Satisfies method that will work as you'd like to have it work.

For parsing filter you should you some grammar (ANTLR) or you can use the Roslyn project to translate string to C# code. In case you will have only limited number of very simple filters you can easily go with regular expressions.

Karel Frajták
  • 4,389
  • 1
  • 23
  • 34
  • 4
    It's a bad idea - to filter data source itself. WPF has excellent concept of collection views, and topicstarter is going right way. If you're filtering data source, as opposed to its **view**, you're loosing possibility to represent the same data with a different views. – Dennis Aug 14 '12 at 07:26
0

Personally, I don't like the approach you've described.

If you want to use SQL-like (or LINQ-like) syntax in filter string, then you oblige user to know that syntax... Especially, if you won't hold SQL (LINQ) syntax strongly, it will be the pain.

I think, placing expander over the grid, which will contain good old controls to set up filtering, and then, dynamic building of corresponding predicate to filter data in code, will be much better.

But, if you'll decide to do filtering this way, then your task boils down to "How to get a LINQ expression from string". This links should help you:
How to convert a String to its equivalent LINQ Expression Tree?

Parsing a string C# LINQ expression

Community
  • 1
  • 1
Dennis
  • 37,026
  • 10
  • 82
  • 150
  • Yeah I don't think this is a good idea either but my client specifically requested for the above approach so I'm stuck with it unfortunately :( – TtT23 Aug 14 '12 at 07:28