Click here to Skip to main content
15,893,564 members
Articles / Web Development / ASP.NET
Article

Searching Generic Objects and Collections using Reflection and Generic Predicates

Rate me:
Please Sign up or sign in to vote.
2.88/5 (5 votes)
18 Jan 2008CPOL9 min read 34K   376   18   4
Generic Predicate Wrapper using Reflection to Search through Lists of Objects and the Objects themselves
GenericPredicateWrapper1.gif

Introduction

As .NET developers, we are constantly working with collections of objects in some form or fashion. Most objects (if not all) can be represented by a collection properties or attributes of some sort. The .NET framework has provided us from the beginning with the ability to create specialized collections of objects. These objects can encapsulate and implement features which allow them to be more easily exchanged, filtered, or sorted by different controls and/or code. For a typical smaller project (or even some really large ones, collections of this nature can too often become intensely complex and can create too much overhead to get a project done in a timely manner. Enter Generics. Generics add to .NET the capability to create and consume various objects in a completely type neutral manner, making room for all sorts of new scenarios and obstacles.

Two distinct ways that we can see the benefits of generics and type neutrality right now in our code:

  1. Generics give us the ability to create lists and objects that encapsulate all of the features of their defined type on the fly (i.e. a list of textboxes or employees or strings or functions can all be defined in one line of code). This cuts the amount of time it takes to code lists of specialized objects, without abstracting the properties and methods and the properties of those objects from us at run time.
  2. Generics cut down on the amount of type checking involved in compilation a great deal. In other words, it greatly reduces the amount of CIL (aka MSIL) generated for your code, making it leaner and faster.

Description

The purpose of this article is to demonstrate how to, and to provide a means for, easily searching objects and lists of objects (whether they are type neutral or not) via the use of a Generic Predicate Wrapper Class and Reflection as needed. This is not an article intended to argue for (or against) the use of Reflection. There are plenty of those out there already. It is simply an article to demonstrate how to and provide a means for quickly searching arbitrary objects or lists of objects.

In this article, I show a simple example of how to search generic objects and collections, but in the source code above, I provide a much more complete solution (although nothing is perfect). However, before you jump into using the code provided with this article for your production environment, you should do your own research on Reflection and you should evaluate your scenario to make sure that the balance is appropriate for your current feature set's requirements.

Here's a great place to Start Your Research: ref. MSDN Magazine Reflection: Dodge Common Performance Pitfalls to Craft Speedy Applications

What is a Predicate?

In a visual meaning of the term, a predicate validates something (i.e. the fact that I am a man predicates the fact that I am human). In a short, .NET-oriented meaning of the term, a Predicate is a delegate method that evaluates to true if a certain condition is met and takes a parameter of the type to be predicated. In general, to search a list of objects we must use a Predicate method to find out if the Object does or does not meet the conditions we specify. So, an example is if in your code you had a list of Textboxes called lstTxt and you wanted to find out which one of them actually had some text in it. You could create a Predicate method for lstTxt called with a name like HasTextPredicate that would take a TextBox as its parameter and would return true if the TextBox had text in it. Let's look at an example of how creating the HasTextPredicate would look in code.

VB
'Declare a variable as a Predicate for the TextBox
'and Delegate the Method that will check for text
Dim HasTextPredicate as Predicate(Of TextBox) = addressOf checkHasText

'This is the Predicate method
Function checkHasText(txt as TextBox) as Boolean
 If txt.Text.Length > 0 Then
  Return True
 End If
End Function

   '*********************** Usage
    'Create 3 New TextBoxes and put text in 2 of them
    Dim t1, t2, t3 As New TextBox
    t1.Text = "Test Text" : t2.Text = "More Text"

    'Now create a Generic List of the 3 Textboxes (2 Of 3 Have Text)
    Dim lstTxt as New List(Of TextBox)
    lstTxt.Add(t1) : lstTxt.Add(t2) : lstTxt.Add(t3)
    
   'Now Create a List of TextBoxes from the list that have 
   'text by calling the List.FindAll method with
   'our predicate as a parameter. This will return a list 
   'of the type that was Predicated
    Dim foundTxts as List(Of TextBox) = lstTxt.FindAll(HasTextPredicate)
     
    System.Debug.WriteLine("TextBoxes with Text=" & foundTxts.Count)
     'Output: "TextBoxes with Text=2"

For further info on Predicates:

For further info on Predicates and List.Find:

What's the Problem?

Using the example above, you are now able to retrieve all of the textboxes in lstTxt that contain text by simply calling lstTxt.FindAll(HasTextPredicate). That's great, right? What if you now need to search a List of Labels for text, as well, or just a random set of Objects to see if they even have a Text property? For each List type, you would be forced to re-write your predicate method to take the appropriate parameter and essentially recreate your Predicate every time you encountered a new list. However, thanks to some nice features of generics and reflection, we can implement a pretty solid solution for this type of issue and focus on more important things in life, like enhancing the user experience of our apps.

So, at this point, our problem has actually branched into two main directions. First, we have to come up with a way to make our Predicate methods more aware of their surroundings, but still neutral enough to not have to recreate them every time (i.e. in some cases our Predicate will need to be able consume parameters beyond the type of the current instance to better retrieve results). We must also have the ability to reuse our methods regardless of the scenario (i.e. Labels or Textboxes or Objects).

What's the Solution?

The great thing about code is that we can achieve the same goal in a variety of different ways. In this case, we will achieve our goals by wrapping our Predicate in its own generic class. This will allow us to pass in a few constructor parameters about our current scenario to our Predicate Wrapper class, which will make them available to our Predicate methods inside the class. Because the class will be generic, it gives us the ability to support multiple types/scenarios from one convenient maintainable location.

Let's look at a basic example of what this generic wrapper class GenericPredicate looks like if we want to search an object for a property that has a specific name, or check if a property by a specific name has a specific value, or even just check whether the Text property has a value at all. I've chosen the short and to-the-point name, GenericPredicate, but in reality, the Predicate Class is actually already generic; we are just wrapping it in another generic layer. For that reason, the code provided with the download in this article uses the name "GenericPredicateWrapper".

In some cases, to really get the true anonymity we desire, we'll need to add a little overhead to our normally extra lightweight generics by using a bit of reflection to interact with all of the possible types and situations that we may encounter. From a code reuse perspective, though, this solution will often work out to be the most time/cost efficient way to get the job done, without a significant impact on performance. The provided solution doesn't currently perform any case insensitive lookups via reflection or invoke any methods through reflection which would normally have a costly effect on performance.

A Sample Solution

Here's a simple proof of concept demonstration of the code in question. The sample project/source goes into more detail.

VB
Imports System.Reflection
'The generic constructor "Of T" allows us to dynamically type this class 
Public Class GenericPredicate(Of T)
    'Enums make it easy to use your code among other things
    'TypeOfSearch will be exactly what type of search to do
    Public Enum TypeOfSearch As Integer
        'An Option to search for a Property with a specific name
        PropertyNamed = 0
        'An Option to search for a Property with a Specific Name and Value
        PropertyNamedWithValue = 1
        'An Option to search for a Text Property and to check for data in it
        ObjectHasText = 2
    End Enum

    ''' <summary>
    ''' Property to house the string value of the search string to be looked for.
    ''' </summary>
    Public Property StringToFind() As String
        Get
            Return _stringToFind
        End Get
        Set(ByVal value As String)
    If value Is Nothing Then
    _stringtoFind = String.Empty
    Else
    _stringToFind = value    
    End If
        End Set
    End Property
    Private _stringToFind As String
    
    ''' <summary>
    ''' Property to house the string value of the Secondary 
    ''' search string to be looked for.
    ''' </summary>
    '''<remarks> Used to search when a property name and value are searched for</remarks>
    Public Property ValueToFind() As String
        Get
            Return _valueToFind
        End Get
        Set(ByVal value As String)
    If value Is Nothing
            _valueToFind = String.Empty
    Else
        _valueToFind = value
    End If
        End Set
    End Property
    Private _valueToFind As String    
    
    'Declare a Predicate method of the typed defined by the class when it was created
    Public PredicateMethod As Predicate(Of T)

    'the constructor takes an integer or enum value for the type of search, a String To
    'Find in the Type, and a value to find if needed
    Public Sub New(ByVal match As TypeOfSearch, ByVal strStringToFind As String,
        Optional ByVal strValueToFind As String = "")

        Me.StringToFind = strStringToFind
        Me.ValueToFind = strValueToFind

        'determine which method we need to be the Predicate Method based on the match
        'passed
        Select Case match
            Case TypeOfSearch.PropertyNamed
                PredicateMethod = AddressOf MatchPropertyName
            Case TypeOfSearch.PropertyNamedWithValue
                PredicateMethod = AddressOf MatchPropertyNameAndValue
            Case TypeOfSearch.ObjectHasText
                PredicateMethod = AddressOf MatchObjectHasText
        Case Else
        PredicateMethod = AddressOf MatchObjectHasText
        End Select

    End Sub

    'This Function checks for the property named "Text" in the object type, if found it
    'looks for a value and returns true if it finds it
    Public Function MatchObjectHasText(ByVal itemType As T) As Boolean
        'determine the type we're dealing with and setup return value
        Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
        'This is very similar to working with the IO class
        'Use reflection to determine if the itmType contains the property name specified
        'in StringToFind
        Dim pI As PropertyInfo = objType.GetProperty("Text")
        'if the property exists
        If Not (pI Is Nothing) Then
        Dim tstObj As Object = pI.GetValue(itemType, Nothing)
            Dim str As String = Nothing
            If Not (tstObj Is Nothing) Then : str = tstObj.ToString : End If
            If str Is Nothing Then : str = String.Empty : End If
            If str.Length > 0 Then
                returnbool = True
            End If
        End If

        pI = Nothing
        objType = Nothing : Return returnbool
    End Function


    'This function will return true if a property by the name specified in
    'strStringToFind exists in the object type
    Public Function MatchPropertyName(ByVal itemType As T) As Boolean
        'determine the type we're dealing with and setup return value
        Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
        'This is very similar to working with the IO class
        'Use reflection to determine if the itmType contains the property name specified
        'in StringToFind
        Dim pI As PropertyInfo = objType.GetProperty(Me.StringToFind)
        'if the property exists
        If Not (pI Is Nothing) Then
            returnbool = True
        End If

        pI = Nothing
        objType = Nothing : Return returnbool
    End Function

    'This function will return true if a property by the name specified in
    'strStringToFind exists in the object type and contains the value from strValueToFind
    Public Function MatchPropertyNameAndValue(ByVal itemType As T) As Boolean
        'determine the type we're dealing with and setup a return value
        Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
        'get a PropertyInfo for the current type 
        Dim pI As PropertyInfo = objType.GetProperty(Me.StringToFind)
        'if the property exists
        If Not (pI Is Nothing) Then
            'use the PropertyInfo of the current type to retrieve the property value
            'of the instance passed
            Dim tstObj As Object = pI.GetValue(itemType, Nothing)
            Dim str As String = Nothing
            If Not (tstObj Is Nothing) Then : str = tstObj.ToString : End If
            If str Is Nothing Then : str = String.Empty : End If 
            If str.ToLower = Me.ValueToFind.ToLower Then
                returnbool = True
            End If
        End If

        pI = Nothing
        objType = Nothing : Return returnbool
    End Function
End Class

The above class gives you the functionality needed to handle all of the situations described above by picking from 1 of the 3 enum choices. Here's an example of how you would use this example. We are going to use the class above to check whether a list of Labels, TextBoxes, and Objects have a property named Text with any string data in it. You'll need to import the System.Collections.Generic namespace.

VB
'Im mirroring the enum for search options here with a friendly name to demonstrate
'that you can really make the code easy to read as well
Public Enum Where As Integer
    'An Option to search for a Property with a specific name
    PropertyNamed = 0
    'An Option to search for a Property with a Specific Name and Value
    PropertyNamedWithValue = 1
    'An Option to search for a Text Property and to check for data in it
    ObjectHasText = 2
End Enum

Private Sub TestForm1_Load(ByVal sender As Object,
    ByVal e As System.EventArgs) Handles Me.Load
    'Create 3 Labels  (2 Of Them With Text)
    Dim l1, l2, l3 As New Label
    l1.Text = "Test Text" : l2.Text = "More Text"
    'Create a List of 3 Labels
    Dim lstLbl As New List(Of Label)
    lstLbl.Add(l1) : lstLbl.Add(l2) : lstLbl.Add(l3)
    'Create 3 Textboxes (2 Of Them With Text)
    Dim t1, t2, t3 As New TextBox
    t1.Text = "Test Text" : t2.Text = "More Text"
    'Create a List of 3 Textboxes
    Dim lstTxt As New List(Of TextBox)
    lstTxt.Add(t1) : lstTxt.Add(t2) : lstTxt.Add(t3)

    'Create a List Of 6 Objects  (4 Of Them With Text(2 Labels + 2 TextBoxes))
    Dim lstObj As New List(Of Object)
    lstObj.Add(l1) : lstObj.Add(l2) : lstObj.Add(l3) : lstObj.Add(t1) :
        lstObj.Add(t2) : lstObj.Add(t3)

    'Declare some instances of our Predicate Class with "ObjectHasText" search from
    'our "TypeOfSearch" Enum

    '*******NOTE******
    'There are a few different ways you can access the type of search
    'enumerator (some are much more graceful than otheres
    'Here's 3 different examples on how to create our GenericPredicate Class
    'Parameter StringToFind isn't used for ObjectHasText, so im passing nothing
    '******************

    'This example uses labels and uses our local enum.
    'This is the most readable method.
    ' I like this method a lot. I find it very useful to mirror the enum in my root
    'namespace to make it easier to call
    Dim myLabelPredicate As New GenericPredicate(Of Label)(Where.ObjectHasText,
        Nothing)


    'This example uses textboxes and passes the integer equivalent of ObjectHasText.
    ' It's shorter, but harder to understand if you're not familiar with the code
    Dim myTextBoxPredicate As New GenericPredicate(Of TextBox)(2, Nothing)

    'This example uses objects(which both Textbox and Label inherit from)
    'This is the longhand version of calling the enum.
    'I am referencing an instance of the generic classes type and
    'then accessing the enum inside of it.
    Dim myObjectPredicate As New GenericPredicate(Of Object)(
        GenericPredicate(Of Object).TypeOfSearch.ObjectHasText, Nothing)


    'Now we can use our PredicateMethod Property to find the objects in question

    'Create a String variable to house our result text
    Dim result As String

    'Create a List of all the Labels in lstLbl that have text by calling
    'List.FindAll with the class's Predicate Method
    Dim lbls As List(Of Label) = lstLbl.FindAll(myLabelPredicate.PredicateMethod)

    '2 Labels have a text property with text in it (l1 & l2) from above
    result = "Labels with Text=" & lbls.Count

    'Create a List of all the TextBoxes in lstTxt that have text by calling
    'List.FindAll with the class's Predicate Method
    Dim txts As List(Of TextBox) = lstTxt.FindAll(myTextBoxPredicate.PredicateMethod)

    '2 TextBoxes should have a text property with text in it (t1 & t2 from above)
    result &= " TextBoxes with Text=" & txts.Count

    'Create a List of all the Objects in lstObj that have text by calling
    'List.FindAll  with the class's Predicate Method
    Dim objs As List(Of Object) = lstObj.FindAll(myObjectPredicate.PredicateMethod)

    'Based on the above results, 4 objects should have a text property with text
    '(2 labels + 2 textboxes)
    result &= " Objects with Text=" & objs.Count

    'so the final result should look like
    '"Labels with Text=2 TextBoxes with Text=2 Objects with Text=4"

    'create result label to display it and set it's width to 500 so we can see
    'all our test results
    Dim lblResult As New Label : lblResult.Width = 500
    lblResult.Text = result
    Me.Controls.Add(lblResult)
End Sub

The code above simply makes 3 generic lists of Labels, TextBoxes, and Objects (which are the Labels and Textboxes). The code then searches the lists using List.FindAll to see if they have a Text Property with text in it or not by calling the same Predicate Method each time with the type of the list to be searched. This demonstrates both the flexibility of the code concept as well the ability to quickly reuse the code as needed. The actual use of the code is essentially done in these two lines.

VB
Dim myObjectPredicate as New GenericPredicate(Of Object)(2,Nothing)
     Dim objs as List(Of Object) = lstObj.FindAll(myObjectPredicate.PredicateMethod)

The first line creates an object reference to the predicate wrapper class we've created called "GenericPredicate" with the constructor parameters required by the current scenario. The second line creates a list of objects that meet the true condition of the Predicate that we chose in the line before.

That's pretty well it, as far as usage goes. You can now search through a list of arbitrary objects and pull out the ones that meet your criteria without knowing a thing about the object. This wrapper or "helper" class additionally makes it very easy to extend your predicates to include custom search abilities as new scenarios make themselves known. With good type-checking, it's not going to always be necessary to do the work with Reflection, but it will often prove to be a very effective way to get the job done, at any rate.

Conclusion

Predicates offer many benefits for developers seeking to quickly filter objects and lists of objects based on their own criteria. Additionally, through the use of a generic wrapper class and a little bit of Reflection, we can easily provide ourselves with a simple, "reusable" model for working with generic Lists of any type. The source/project provided with this article dives into these concepts further and provides some more options for filtering lists of objects.

Using the Code Provided in the Download

For the sake of brevity and complexity, I did not use the class I provided in the source/demo project here. It doesn't differ greatly, but I may, at some point in the future (if requested), release a more in-depth article discussing the provided demo project and source. The concept behind the methods of the code from the demo project does not differ greatly from what is done in the sample here, but it does expose some new options and I consider it a much more complete solution. I've listed the name/values of the TypeOfSearch enum from the demo project below to give you an idea of the additional features in it.

VB
Public Enum TypeOfSearch As Integer
    PropertyNamed = 0
    PropertyNamedWithValue = 1
    PropertyNamedWithLikeValue = 2
    PropertyWithValue = 3
    PropertyWithLikeValue = 4
    PropertyOfType = 5
    PropertyNamedAndOfType = 6
    MethodNamed = 7
    MethodOfReturnType = 8
    MethodNamedAndOfReturnType = 9
    MethodWithParamNamed = 10
    MethodWithParamNameAndOfType = 11
    ObjectHasText = 12
    ToStringEquals = 13
    EventNamed = 14
End Enum

Additionally, in order to work with these new options, the constructor for the GenericPredicate class in the demo/source files has some different parameter names to accommodate for the additional options. The constructor now looks more like this:

VB
Public Sub New(ByVal SearchType As TypeOfSearch, ByVal StringOrTypeToFind As String,
    Optional ByVal TypeOrValueToFind As String = "System.String")

The demo project/article source code is fully documented for ease of use. The demo app shows all of the options and gives you 3 Labels and Textboxes to test against. The demo app also interactively explains which parameters are used for each TypeOfSearch scenario.

GenericPredicateWrapper2.gif

Follow Up

What do you think? Did you learn anything interesting from this article? Is there anything that you'd like me to further explain or demonstrate here? Do you have any suggestions or comments about scenarios or Predicate options that I didn't cover in the source, which you feel others could benefit from? Feel free to comment below.

History

  • 10 January, 2008 -- Initial release
  • 18 January, 2008 -- Article content updated

License

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


Written By
Software Developer LEAP
United States United States
Call Center Developer for a leading, independent provider of integrated health solutions

Comments and Discussions

 
GeneralOutstanding Pin
Jim Mullis17-Mar-08 17:00
Jim Mullis17-Mar-08 17:00 
GeneralRe: Outstanding Pin
werD18-Mar-08 19:56
werD18-Mar-08 19:56 
Generalouch Pin
cechode10-Jan-08 16:19
cechode10-Jan-08 16:19 
ouch Frown | :(
GeneralRe: ouch Pin
werD11-Jan-08 3:34
werD11-Jan-08 3:34 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.