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:
- 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.
- 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.
Dim HasTextPredicate as Predicate(Of TextBox) = addressOf checkHasText
Function checkHasText(txt as TextBox) as Boolean
If txt.Text.Length > 0 Then
Return True
End If
End Function
Dim t1, t2, t3 As New TextBox
t1.Text = "Test Text" : t2.Text = "More Text"
Dim lstTxt as New List(Of TextBox)
lstTxt.Add(t1) : lstTxt.Add(t2) : lstTxt.Add(t3)
Dim foundTxts as List(Of TextBox) = lstTxt.FindAll(HasTextPredicate)
System.Debug.WriteLine("TextBoxes with Text=" & foundTxts.Count)
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.
Imports System.Reflection
Public Class GenericPredicate(Of T)
Public Enum TypeOfSearch As Integer
PropertyNamed = 0
PropertyNamedWithValue = 1
ObjectHasText = 2
End Enum
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
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
Public PredicateMethod As Predicate(Of T)
Public Sub New(ByVal match As TypeOfSearch, ByVal strStringToFind As String,
Optional ByVal strValueToFind As String = "")
Me.StringToFind = strStringToFind
Me.ValueToFind = strValueToFind
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
Public Function MatchObjectHasText(ByVal itemType As T) As Boolean
Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
Dim pI As PropertyInfo = objType.GetProperty("Text")
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
Public Function MatchPropertyName(ByVal itemType As T) As Boolean
Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
Dim pI As PropertyInfo = objType.GetProperty(Me.StringToFind)
If Not (pI Is Nothing) Then
returnbool = True
End If
pI = Nothing
objType = Nothing : Return returnbool
End Function
Public Function MatchPropertyNameAndValue(ByVal itemType As T) As Boolean
Dim objType As Type = itemType.GetType : Dim returnbool As Boolean = False
Dim pI As PropertyInfo = objType.GetProperty(Me.StringToFind)
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.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.
Public Enum Where As Integer
PropertyNamed = 0
PropertyNamedWithValue = 1
ObjectHasText = 2
End Enum
Private Sub TestForm1_Load(ByVal sender As Object,
ByVal e As System.EventArgs) Handles Me.Load
Dim l1, l2, l3 As New Label
l1.Text = "Test Text" : l2.Text = "More Text"
Dim lstLbl As New List(Of Label)
lstLbl.Add(l1) : lstLbl.Add(l2) : lstLbl.Add(l3)
Dim t1, t2, t3 As New TextBox
t1.Text = "Test Text" : t2.Text = "More Text"
Dim lstTxt As New List(Of TextBox)
lstTxt.Add(t1) : lstTxt.Add(t2) : lstTxt.Add(t3)
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)
Dim myLabelPredicate As New GenericPredicate(Of Label)(Where.ObjectHasText,
Nothing)
Dim myTextBoxPredicate As New GenericPredicate(Of TextBox)(2, Nothing)
Dim myObjectPredicate As New GenericPredicate(Of Object)(
GenericPredicate(Of Object).TypeOfSearch.ObjectHasText, Nothing)
Dim result As String
Dim lbls As List(Of Label) = lstLbl.FindAll(myLabelPredicate.PredicateMethod)
result = "Labels with Text=" & lbls.Count
Dim txts As List(Of TextBox) = lstTxt.FindAll(myTextBoxPredicate.PredicateMethod)
result &= " TextBoxes with Text=" & txts.Count
Dim objs As List(Of Object) = lstObj.FindAll(myObjectPredicate.PredicateMethod)
result &= " Objects with Text=" & objs.Count
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.
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.
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.
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
Call Center Developer for a leading, independent provider of integrated health solutions