.NET 2 the Max

Enable Better Code Reuse

.NET Reflection can simplify many aspects of development by helping you create more generic and highly reusable code.

Technology Toolbox: VB.NET, C#

Reflection is one of the most exciting features of the .NET Framework, but many developers raise their hands and object when I teach reflection in seminars and courses. They say, "Reflection seems great to create object browsers, but we don't write that kind of code. Why should we bother with reflection in the first place?"

Reflection can do much more than list all the members in a .NET type. In fact, reflection is the key to creating highly generic and reusable code. Some of the techniques that reflection permits today will be available under .NET 2.0 through generic types, but reflection will continue to provide a degree of flexibility that you can't achieve otherwise. Indeed, reflection can make a big difference in several cases, especially when it comes to making your code more reusable (see Figure 1).

Build a Universal Comparer Type
The .NET Framework defines a couple interfaces that compare and sort objects: IComparable and IComparer. Implementing IComparable is necessary for a type to support sorting—indeed, all primitive .NET types implement this interface, including strings and numeric types. You implement the IComparer interface in auxiliary classes that work as comparers for other types, whether you've defined .NET or custom types. For example, suppose you're creating a list of birthdays for a group of people, and you want to sort dates according to the (month, day) pair without taking the year into account (see Listing 1). You use such a class in this manner:

' dates is an array of Date values
Array.Sort(dates, New MonthDayComparer)

You can expand the MonthDayComparer class to support other sort criteria easily; for example, you can sort in descending order simply by negating the value that the Compare method returns to its callers. Typically, you might pass a comparer's constructor an argument that specifies how values should be sorted.

Comparer types are useful for more than array sorting; you can use them in many other circumstances, such as when performing binary searches or when creating SortedList objects. The main problem with comparer types is that you must define a distinct comparer type for each possible sort criterion. Clearly, this requirement can soon become a nuisance.

I've implemented far more comparer classes than I'd like to admit, so I decided to implement a definitive comparer class, one that can work with any type of object and any combination of properties, as well as one that supports both ascending and descending sorts.

It might be helpful to understand how you can use the UniversalComparer type before discussing how it works internally. Assume you have a Person class that exposes the FirstName and LastName string properties. It's a simple matter to sort an array of Person objects with the universal comparer:

Dim comp As New UniversalComparer( _
   GetType(Person), _
   "LastName, FirstName")
Array.Sort(arrPersons, comp)

The UniversalComparer supports a string argument that resembles what you'd pass to the ORDER BY clause in SQL. You can even sort in descending mode on one or more fields:

Dim comp As New UniversalComparer( _
   GetType(Person), _
   "LastName DESC, FirstName DESC")

Not surprisingly, the UniversalComparer class relies heavily on reflection to do its magic (see Listing 2). The listing includes comments to explain how the class works, but you might need to rethink some basic facts about reflection. This class uses reflection intensively, so it isn't as fast as a more specific comparer might be. However, the speed difference wouldn't be noticeable in most cases. You can use the companion sample project to check how to use this class in a real application (get it here).

Run Multiple Threads Without the Hassle
Using the System.Threading.Thread class to run a piece of code in a separate thread is easy: You create a delegate to the method that you want to execute asynchronously, pass it to the Thread's constructor, then call the Thread.Start method:

Dim th As New Thread(New _
   ThreadStart(AddressOf PerformTask))

A few details can make working with threads difficult. First, there is no simple way to pass one or more arguments to the asynchronous method, because the ThreadStart delegate can point only to a procedure that takes no arguments. (Note that this limitation will be lifted in .NET 2.0.) The common solution to this issue is defining an auxiliary class that exposes one or more data fields and running the asynchronous method inside one instance of that class:

Sub RunThread()
   Dim tw As New ThreadWrapper
   tw.Data = "some data"
   Dim th As New Thread( _
      AddressOf tw.PerformTask)
End Sub

Class ThreadWrapper
   Public Data As String

   Sub PerformTask()
      ' access the Data field as needed
      ' ...
   End Sub
End Class

Using a wrapper class solves the problem of passing data to a thread, but leaves other issues unresolved. For example, if you're inside a Windows Forms application, the secondary thread shouldn't access user interface elements directly; instead, it should adopt a complex mechanism based on delegates and the Invoke method of the parent form.

I soon got bored of writing the same code over and over, so I decided to adopt a more structured approach for my thread-intensive Windows Forms applications. The result is the WorkerThreadBase abstract class, which encapsulates much of the code you need when working with threads (see Listing 3). Defining a background task requires only that you inherit from the WorkerThreadBase class and override its OnStart protected method:

Class MyWorkerThread
   Inherits WorkerThreadBase

   Protected Overrides Sub _
      OnStart(ByVal argument As Object)
      ' this runs in a different thread
      ' ...
   End Sub
End Class

The MyWorkerThread class inherits a Start method that can take an argument:

Dim mwt As New MyWorkerThread
mwt.Start("some data")

Note that you can even pass multiple arguments by storing them in an array.

The MyWorkerThread class also inherits the SynchronizingObject property, which works similarly to the property with the same name exposed by several .NET classes, such as FileSystemWatcher or EventLog. If you assign a form or a control to this property, the code in the class can invoke a method that runs in the thread implied by that form or control, which is exactly what you need to avoid problems in cross-thread calls:

' This code should run inside a form
Dim mwt As New MyWorkerThread
mwt.SynchronizingObject = Me
' Pass a Label control as an argument

The code inside any class that derives from WorkerThreadBase can invoke a method in the thread implied by SynchronizingObject using the InvokeMember protected method, whose syntax resembles the Type.InvokeMember method. For example, this code enables the thread to display a message in the Label control passed as an argument:

Protected Overrides Sub _
   OnStart(ByVal argument As Object)
   ' ...
   ' Retrieve the Label argument
   Dim lbl As Label = DirectCast( _
      argument, Label)
   ' Display "Done!" in the label
   Dim args() As Object = {"Done!"}
   InvokeMember(lbl.GetType(),"Text", _
      BindingFlags.SetProperty, _
      lbl, args)
End Sub

The first argument for InvokeMember is the System.Type that exposes the member; the second argument is the name of the field, method, or property you're accessing; and the third element is a BindingFlags enumerated value that tells whether you're setting a field or property, calling a method, and so forth. The fourth argument is the instance that exposes the method you're calling (use a null object reference if it's a static member). The last argument is an object array that holds all the arguments for the member being invoked. When setting a field or a property, this object array is a one-element array containing the value assigned to the member.

Once again, reflection enables you to simplify the structure of your code dramatically, while also making it more reusable.

Write Provider-Agnostic Code
When writing ADO.NET apps, reusable code means code that works well with multiple .NET data providers. In some cases, you can't achieve a complete independence from a specific provider. For example, you can't create a parameterized SQL query that works equally well with the OLE DB provider and the SQL Server provider, because the former uses question marks as parameters and the latter uses @-prefixed parameters. However, you should always attempt to write code that is as generic as possible because the additional effort usually pays off nicely in the long run.

Microsoft has done a decent job of encouraging developers to produce provider-agnostic code. For example, similar ADO.NET types living in different providers always expose a given interface or derive from the same base class. As a consequence, the common IDbConnection interface enables you to write a function that takes a connection string and returns a specific Connection object:

Function CreateConnection(ByVal _
   conn As String) As IDbConnection
   If conn.ToLower().IndexOf( _
      "provider=") >= 0 Then
      Return New OleDbConnection(conn)
   ElseIf conn.ToLower().IndexOf( _
      "driver=") >= 0 Then
      Return New OdbcConnection(conn)
      Return New SqlConnection(conn)
   End If
End Function

This code lets you leverage the CreateConnection function and other ADO.NET interfaces to issue a nonparameterized query against any .NET data provider:

Dim cn As IDbConnection = _

' Create a command on this connection.
Dim cmd As IDbCommand = _
cmd.CommandText = _
   "SELECT * FROM Publishers"

' Perform the query
Dim dr As IDataReader = _

' Process publisher data
Do While dr.Read
   ' ...

Alas, Microsoft left out the ability to instantiate some ADO.NET in a generic way. For example, it isn't possible to build a DataAdapter object corresponding to a given connection, short of testing each possible connection type. Fortunately, reflection can help here, so I created a CreateDataAdapter method that fits the bill quite nicely (see Listing 4). Using this method is simple:

sql = "SELECT * FROM Publishers"
Dim da As DbDataAdapter = _
   CreateDataAdapter(sql, cn)
da.Fill(myDataSet, "Publishers")

Granted, you can still use code that contains a lot of Select Case (VB) or switch (C#) statements, but the approach based on reflection is more elegant and works with any future provider, without the need to recompile the application (as long as the provider adheres to the naming standard employed by existing providers).

Portions of this column have been excerpted from Francesco Balena and Giuseppe Dimauro's book, Practical Guidelines and Best Practices for Microsoft Visual Basic and Microsoft C# Developers [Microsoft Press, 2005, ISBN: 0735621721].

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.