![]() |
Languages »
VB.NET »
General
Intermediate
License: The Code Project Open License (CPOL)
Searching Generic Objects and Collections using Reflection and Generic PredicatesBy werDGeneric Predicate Wrapper using Reflection to Search through Lists of Objects and the Objects themselves |
VB, Windows, .NET (.NET 2.0), ASP.NET, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
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:
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
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.
'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:
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).
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.
Here's a simple proof of concept demonstration of the code in question. The sample project/source goes into more detail.
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.
'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.
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.
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.
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.
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:
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.
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.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 18 Jan 2008 Editor: Genevieve Sovereign |
Copyright 2008 by werD Everything else Copyright © CodeProject, 1999-2009 Web15 | Advertise on the Code Project |