Welcome to AspAdvice Sign in | Join | Help

.Net Discoveries

An attempt to pass along some answers I have discovered in my .Net coding.
Passing a Parameter to a Predicate... and a Better Way!

Prologue

Recently I've been working on a rather complex and lengthy form page. One of the great ideas I had was to allow users to enter information (such as a city) into the field and have it auto-complete. The AJAX control toolkit has a great control for this, it's called the AutoCompleteExtender. Using this control is pretty easy to do, however, I found some difficulty in making my filtering code work correctly.

Problem

To use the AutoCompleteExtender, you have to setup an AJAX enabled web service that that takes 2 parameters. One is the prefix to filter for, and the other is the number of hits to return. This web service then returns a string array of the first X items matching the prefix filter.

Since I've latched onto generic lists and found them very easy to use, I assumed that there must be a really easy way to just take my list, search it for the prefix filter and return the results. If you look at a generic list, you'll see that there is a convenient .FindAll method. Problem solved, right? Not so fast. To use the .FindAll method, you have to define another, outside function that can do the actual finding, it doesn't do the finding for you. The problem arises in that you cannot pass the filtering criteria to the predicate as the call to the predicate cannot contain a parameter.

In searching for an answer to this dilemma there can be found a number of examples, even on Microsoft's site, that just have you save the criteria to some kind of a global variable and then pull this value from the global variable from within the predicate's code. I didn't think that this was particularly clean... especially since I'm calling all this in a web service. So I found a great article that solved the problem.

Then I found another better way.

Solution


A Predicate with a Parameter

First, let's examine how to 'add a parameter' to a predicate and then we'll look at an easier way to do this whole thing without the parameter.

First let's do a little setup. Instead of creating a web service and making it AJAX enabled so we can use our AutoCompleteExtender ours won't be as nice, but will be sufficiently functional. Let's setup a TextBox and allow a user to enter a letter or two, press a submit button and get a filtered list of states.

First, create a new .aspx page, and add the following: A TextBox named txtFilter, a ListBox named lstStates and a button named btnDoIt. Next, let's create a shell function to return our states. Add the following to your back-end code:

Private _states As String() = {"AL", "AK", "AZ", "AR", "CA", "CO", "CT", _
  "DE", "DC", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", _
  "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", _
  "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", _
  "UT", "VT", "VA", "WA", "WV", "WI", "WY"}

Public Function GetStates(Optional ByVal sFilter As String = "") As List(Of _
   String)

    Return _states.ToList()
End Function

Basically we're just creating an array containing all the state's codes and then (for the moment) our GetStates will return all the states as a list. Next, we'll have our btnDoIt's Click event handler get the call GetStates and bind the returned data to the lstStates control. Add the following to your btnDoIt_Click evene handler:

lstStates.DataSource = GetStates(txtFilter.Text)
lstStates.DataBind()

If you run your page, you'll see that if you click the button, the entire list of states will be put into the ListBox, regardless of what you put into the TextBox.

Now, let's work on getting the filtering to work. Notice that if you add a . to the end of your _states.ToList() statement, intellisense lets you know that you can add another method, FindAll(). Reading it's description makes it sound like it is just the thing we need. Pass in a filter and find everything with it. What could be easier, right? Again, not so fast, you don't pass in a string parameter to FindAll, you pass in a predicate. A WHAT? Basically, we pass in a delegate to another function that can do the actual finding work. Then we have create the routine that determines if the filter criteria was found or not found.

So, according to all the help on Microsoft's site, we would save our filter criteria to a global variable, then call our predicate and in our predicate, retrieve the global variable and process. Personally I don't think this is very clean (using global variables to pass parameters) and so I searched and found a cleaner solution (sorry I don't have the link to the original article).

The solution involves creating a filtering class that contains both our filtering parameter AND our predicate function. We'll create a constructor, and when we construct this object, we'll pass the parameter and then call it's only method which will use the 'parameter' stored in our class to do the filtering. Add the following to your project:

Private Class ListMatcher
    Private _criteria As String
    Public Sub New(ByVal sCriteria As String)
        _criteria = sCriteria
    End Sub
End Class

Here we've defined a class (ListMatcher) and created an internal variable _criteria. When we initialize this class, we pass in the filter criteria and this is stored within the object. Next, we'll create the actual predicate method. Add the following within your ListMatcher class:

Public Function FindAll(ByVal sItemToMatch As String) As Boolean
    Return sItemToMatch.StartsWith(_criteria)
End Function

Notice a couple things about this function:

  1. It has a parameter (we just can't use it) When called, this parameter will be passed the item to search (i.e. an individual state). This function will be used for each item in the array and it will be determined whether or not the criteria has been met.
  2. The parameter is of the same type as the list we're searching.
  3. The function returns a boolean, a True or False as to wether or not the criteria was found in the item searched.

Now we need to use our class. Change your GetStates method so that it reads as follows:

Dim objListMatch As New ListMatcher(txtFilter.Text)
Return _states.ToList().FindAll(AddressOf objListMatch.FindAll)

Here we initialize our matching object using our filter criteria (the contents of txtFilter), then we pass the FindAll method the address of our object's FindAll method. Notice that when we call the FindAll method of our object we DO NOT pass any parameters. This is why we can't add any in our code. The definition assumes that one and only one will be passed, and that is the item to check as the list is iterated over. This becomes the crux of why we can't pass our filter criteria in here.

Run your application and see if it works (Note, it IS case sensitive at the moment...). And, we can successfully filter. Now if you wanted to take this a little farther and make this a little more robust, you could make a whole set of parameters for initialization to tell it to match case, or to search as starts with, contains or ends with and a few others. I originally did this, so I'll include the code here:

Private Class ListMatcher
    Public Enum MatchType
        StartsWith
        Contains
        EndsWith
    End Enum
    Private _matchType As MatchType
    Private _matchCase As Boolean = False
    Private _criteria As String
    Public Sub New(ByVal sCriteria As String, _
              Optional ByVal iMatchType As MatchType = MatchType.Contains, _
              Optional ByVal bMatchCase As Boolean = False)

        _criteria = sCriteria
    End Sub
    Public Function FindAll(ByVal sItemToMatch As String) As Boolean
        Select Case _matchType
            Case MatchType.Contains
                If _matchCase Then _
                  Return sItemToMatch.Contains(_criteria) _
                Else _
                  Return sItemToMatch.ToLower().Contains(_criteria.ToLower())
            Case MatchType.StartsWith
                If _matchCase Then _ 
                   Return sItemToMatch.StartsWith(_criteria) _
                Else _
                  Return sItemToMatch.ToLower().StartsWith(_criteria.ToLower())
            Case MatchType.EndsWith
                If _matchCase Then _
                   Return sItemToMatch.EndsWith(_criteria) _
                Else _
                   Return sItemToMatch.ToLower().EndsWith(_criteria.ToLower())
        End Select
    End Function
End Class

This adjusted code will allow you to determine if it searches with case sensitivity and where to search for the filter phrase (start, contains or end). The code here can also be extended for other uses.

However, I found an easier way that I liked better. LINQ!

An Easier Way

We can do all the same work by using a LINQ statement, and cut out all this nonsense with the predicate all together. And we can do it with ONE line of code rather than an entire class. Change the contents of your GetStates function to the following:

Return (From state In _states Where state.ToLower. _
       StartsWith(txtFilter.Text.ToLower)).ToList()

Our LINQ statement searches for states within the _states array where the state (in lowered case) starts with the contents of txtFilter (in lowered case). When all the items are found, we cast the results of the query to a list and return it.

Granted it doesn't do all the little pieces like case sensitivity with that we could select with our full class, but this was ALOT simpler in the code and really I didn't need all that other stuff anyway.

Epilogue

To me, it seems like a terrible waste to propagate the idea that we should stuff values into global variables and the pull them later. My personal feeling is that this is a MUCH cleaner solution, all self packaged, and reusable.

But having said that I've been really liking the LINQ solution to things, it's pretty intuitive, behaves much like a SQL statement AND since I'm already leveraging it pretty strongly in my DAL, it only made sense to use it in the project other places to do some other types of query.

Sponsor
Posted: Monday, July 12, 2010 5:59 PM by Yougotiger

Comments

.Net Discoveries said:

Prologue I recently mentioned that I used the AutoCompleteExtender in a project I was working on. I keep

# July 14, 2010 1:54 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

Enter the code you see below

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS