Code Focused

Reflecting on Generics

Determine whether an existing variable is a generic type and whether you have to use reflection in particular cases; create irregularly shaped forms; and enable remoting with single-instance applications.

Generics in .NET provide strong typing, high performance, and code reuse. In applications designed with generics in mind, generic parameters are passed throughout, enforcing constraints and providing the strong typing benefits. However, not all applications have the luxury of having been designed from the outset with generics in mind. Applications and frameworks written in .NET 1.0, 1.1, COM Interop, and even event-driven design still suffer from not having generic parameters available. The result is often an impedance mismatch, and gives rise to the one of the most frequent questions I get asked about generics: "How do I tell if this variable is a generic type?"

My first response to this is to question the question. Look at your code, look at your design, and see if you can factor it to either include generic parameters or to code completely without them. In the few cases where you do need to work with generic code from non-generic code, reflection allows you to build a bridge between the two.

One of the most common scenarios is trying to determine whether list items are of a particular type. For example, assume you have this class hierarchy:

Class Person
     ...
Class Customer : Inherits Person
     ...
Class Employee : Inherits Person

Now assume you have a routine that is meant to process Person objects from a List, and you are passed a List(Of Customer) as an Object:

Public Sub CallingCode
     Dim items as New List(Of Customer)
     ProcessPeople(items)
End Sub
....
Public Sub ProcessPeople(ByVal items _
     As Object)
     'work with items here
     '...
End Sub

Inside the ProcessPeople method, items is As Object. Now test to see whether items is a List(Of Person):

If TypeOf items Is GetType(List(Of _
     Person)) Then

This test fails. The reason for the failure is that there is no generic variance: A List(Of Person) is not interchangeable with a List(Of Customer), even though Customer derives from Person. This is an important concept to understand when dealing with generics.

Variance would make it an easier task to test whether a List(Of Customer) is a list containing items that derive from Person, but it would also impede type safety. If the list's Item property were As Person, you could add any type to the list, as long as the type derives from Person. You would lose the type safety of ensuring that only Customers are in the list. In the future, VB and C# might get some kind of limited variance with generics, but a solution that makes it intuitive, yet preserves type safety, is some time away.

Today, the best solution for dealing with the items variable in this particular case is to refactor the method and provide a generic overload:

Public Sub ProcessPeople(Of T As Person) _
     (ByVal items As List(Of T))

The generic method provides strong typing for the generic parameter T by enforcing a constraint that T must be As Person. You can code directly against any methods on the Person type as you iterate through the List. T will be As Customer, or As Employee, but that will be transparent to you. You should also consider making the items definition the minimal interface you need to work with. If you are iterating only the members of the list, you don't need to know it is a List. In that case, IEnumerable(Of T) is all you need:

Public Sub ProcessPeople(Of T As Person) _
     (ByVal items As IEnumerable(Of T))

One limitation of this approach is that the code that calls your method will also need to be recompiled because generic method resolution is done at compile time, not runtime. If the type is As Object or a non-generic type, the compiler will still match with the non-generic method. You're likely to be faced with this situation when dealing with legacy applications, COM interop, or late binding.

In situations where you can't use a generic method, the easiest solution is to work on the objects as if they weren't generic types.

For example, List(Of Customer) implements IList(Of T), which also implements the non-generic interface, IList. This means you can work with the items through the non-generic IList interface. When the framework team introduced the generic interfaces IList(Of T) and IEnumerable(Of T), they made both inherit the non-generic interfaces, IList and IEnumerable, respectively. This is critical for features such as data binding to work with generic collections, and allows you to work with common generic collections using non-generic code:

Public Sub ProcessPeople(ByVal items As IList)
     For Each p As Person In items
          ...
     Next
End Sub

The non-generic code is reasonably simple. Both VB and C# allow an implicit cast in for? each loops. In this case, each item is cast from Object to Person as the list is iterated. The downside of this approach is that the casting presents a small runtime overhead and it's also a risky operation that could fail. The type safety checks occur at runtime instead of at compile time. You can make that simple code more robust, by performing the cast yourself and testing each item's type:

For Each item As Object In items
     Dim p as Person = TryCast(item, Person)
     If p IsNot Nothing Then
          ...
     End If
Next

In cases where none of the aforementioned solutions work well within your design constraints, you'll have to resort to using reflection.

Use Reflection With Generics
To test whether the variable items is a generic list and the item type is derived from Person, you have to first get the variable's Type and see if it's a generic type. If so, you need to get its generic definition (sometimes referred to as "template") and compare that definition with the List(Of ) generic definition type. You can't instantiate a generic definition, but you can refer to its Type by omitting any generic parameters. Rather than write GetType(List(Of T)), you write GetType(List(Of)). For a Dictionary(Of TKey,TValue), you write GetType(Dictionary(Of , )). Note the comma symbolizing two generic parameters placeholders.

This code enables you to determine whether items is a generic List:

Dim typ as Type = item.GetType
If typ.IsGenericType AndAlso _
     typ.GetGenericTypeDefinition Is _
     GetType(List(Of )) Then
     ' ....

The problem with this code is that it assumes that items is a List(Of T). If items is a type that is derived from List(Of T), this code would fail to find that generic type definition:

Class CustomerCollection : _
     Inherits List(Of Customer)

To deal with cases such as custom collection classes, you need to get the items base class recursively until the generic type definition is found.

If you need to determine whether items is an IList(of T), you can iterate through its interfaces checking to see whether each one is a generic type. If it is, then get its generic type definition. A simpler approach is to use GetInterface and pass in the name of the interface as a string. Some people take a short cut here and use the short name:

"IList´1"

This works most of the time, but it's potentially fragile because you can end up matching another IList(Of ). A safer technique is to get the full name from the type:

Dim typ As Type = _
     items.GetType.GetInterface( _
     GetType(IList(Of )).FullName)

You can make life a bit simpler by using a helper method that finds the generic implementation, whether it's a generic interface or generic type definition that you're searching for (see the FindGenericImplementation method in Listing 1). Once you find the generic implementation, you need to check all GenericArguments to see whether their types match. This code checks whether the first GenericArguement is of type Person or derived from it:

Dim typ As Type = FindGenericImplementation( _
      items.GetType, _
     GetType(IList(Of )))

If typ IsNot Nothing AndAlso _
     GetType(Person).IsAssignableFrom( _
     typ.GetGenericArguments(0)) Then
     Debug.Print("it is a list of items " & _
          "where each item is a person or " & _
          "derived from person")
End If

Now you have determined that the variable is of the correct type, but you still have to write code to work with the actual item or items. Again you can use reflection to find a generic method signature to pass the data to. Assume you have a generic method with the signature we defined earlier:

Public Sub ProcessPeople(Of T As Person) _
     (ByVal items As IEnumerable(Of T))

To pass items to that method, you need to first get a Reflection.MethodInfo for the method. MethodInfo derives from Reflection.MemberInfo and provides all the reflection information about a method signature in your code. Once you find the correct MethodInfo, call MakeGenericMethod to bind that MethodInfo to a given set of type parameters, and then invoke the generated generic method. The difficult part is getting the correct MethodInfo. The Type.GetMethod function doesn't provide any means to specify the generic method over any other method of the same name. However, you can use Type.GetMethod, provided the method you're calling has no overloads. The alternative is to call Type.GetMember, passing in the method name and ensuring the binding flags include InvokeMember. GetMember returns an array of MemberInfo. This is in distinct contrast to GetMethod, which returns only one item or throws an exception if there are two or more methods of that same name. Because you include the InvokeMember flag, each of these MemberInfos is a MethodInfo.

You want to ensure the generic parameters are correct and that the constraints are enforced. You do this by calling GetGenericArguements to get a list of the generic parameters, and then, on each parameter, call GetGenericParameterConstraints and check whether each constraint can be assigned from the corresponding argument in the type you wish to pass to the method (Listing 2). This approach is rather complex. You can simplify the code by removing many of the checks and by providing a uniquely named method you want to call.

Be sure to check out the sample code for the full implementation of this approach (download sample code). Also, be sure to run the samples to compare the timings for the different approaches. Typically, reflection is the slowest approach, and you should generally avoid reflection because of both its poor performance and its complexity. But if you find that you do need reflection, the samples provide you with a good foundation for implementing it in your own applications.

Create Shapely Forms
No doubt you've seen applications with irregularly shaped windows—Microsoft's Media Player springs immediately to mind as an example. Achieving an irregularly shaped look was once a complex task, requiring the calculation of complex window regions. Windows 2000 made the task of creating irregularly shaped forms significantly easier by introducing layered windows. Layered windows enable transparency and opacity in your windows. And Windows.Forms exposed this on forms with the Opacity and TransparencyKey properties. Today, creating an irregularly shaped form is incredibly easy.

The first step is to choose your graphic and set that as the form's background. Set the form's transparency key to the color you want to hide, and set the form's border style to none. It doesn't require a single line of code; however, you do need to write code to allow the user to move and close the form.

The mistake some people make here is in their choice of graphic format. With developers who are accustomed to the old ways of achieving transparency, there is a tendency to think in terms of mask colors and to try to match the TransparencyKey to a mask color of the graphic. This technique works on some systems, but fails on others. Microsoft published a KB article on this problem (click here to read) that states:

"When the color depth of the monitor is set to a value that is greater than 24-bit, the TransparencyKey property is not effective. Therefore, the areas on the form that match the color you set the TransparencyKey property to are not rendered as transparent."

Unfortunately, their recommendation—to write a few lines of code—is misleading. The TransparencyKey works perfectly, however, transparency is exacting, and requires a perfect match of the colors. Areas of the form with the background color set to the same color as the TransparencyKey will be rendered transparent. The problem is the way that GDI+ renders some bitmaps on 32-bit channels. This varies, depending on the system. Basically there is a blending that occurs and what was a pure color in your bitmap is rendered as a range of colors. This is barely noticeable to the human eye, but if you have a color picker application and move the picker cursor over your bitmap, you can see that Windows.Forms and GDI+ render the colors differently.

The work around the KB suggests is to try to set the Transparency-Key at runtime. The only way you can get that to work is if you also set the form's background color at runtime.

There is a better and simpler solution. Since Windows.Forms supports transparent GIF's and transparent PNG's, simply set the areas you want transparent as transparent in your graphics application and save the graphic file. Then, on the form, set the transparency key to the same color as the form's background color and you're done. There's no need to write any code.

You now have irregularly shaped forms working properly. At this point you might be tempted to apply this technique to a user control inside a form. Unfortunately, layered windows work only for top-level windows—in other words, for forms only. However, you can tie one form to another form. To do this, you set the form's ShowInTaskBar property to False. When you call Show on the form, be sure to pass it coordinates and the Owner. At this point, it's a simple task to wire up the Owner form's move event to an event handler, and move the child form accordingly. One problem remains. When the child form gets focus, the main form's title bar draws as if it no longer has focus. You can resolve this issue by adding code to your child forms WndProc that tells the Owner form to draw its caption bar as if it still has focus:

Protected Overrides Sub DefWndProc( _
     ByRef m As System.Windows.Forms.Message)

     MyBase.DefWndProc(m)
     Const WM_NCACTIVATE As Int32 = &H86
	
     If Me.m_Owner IsNot Nothing AndAlso _
          m.Msg = WM_NCACTIVATE Then

          SendMessage(m_Owner.Handle, _
                    m.Msg, New IntPtr(1), IntPtr.Zero)
     End If
End Sub

This is the same technique you use when creating floating tool windows. Now, you can let your imagination go wild and create floating tool windows of all shapes.

The sample application contains all the relevant code you need to create and use irregularly shaped child forms (download sample code).

Remoting and the Single Instance Application
A good friend contacted me recently about a strange bug occurring in his application. The application used remoting and all worked well until he switched the client Windows.Forms application to be a single-instance application. The "make single instance application" option was introduced with VB 2005 as part of the My framework.

This option provides a robust means of ensuring a that only a single instance of the application is run, as well as providing elegant notification of command-line arguments from other attempted instances. To enable this message notification, the app uses TCP channel remoting over a secure channel. And therein lay the cause of the problem. My friend's application was an early prototype, and its creators hadn't set the channel as secure. So, the remoting call would end up being a secure channel calling into an unsecure one. Hence, it failed.

There are a couple of ways to get around this problem. One is to roll your own single instancing code. This is a relatively simple process that involves calling System.Diagnostics.Process.GetProcess and matching that with your process. Notifying that process of your command lines is more complex. An alternative to using remoting is to use window messages, registering a custom windows message. You'd then use WinAPI calls to SetForeGroundWindow as well as SendMessage, and add code in your main form's WndProc to handle that message.

A less complex solution is to mark the unsecure channel as having a higher priority. This works most of the time, but if the channel is busy, then the secure channel might be called and the code would fail.

My advice was to make the app's own remoting channel secure by giving it a custom name that wouldn't conflict with the other TCP channel.

Dim ChannelProperties As _
     IDictionary = New Hashtable()
ChannelProperties.Add("name", _
      "my_secure_channel")
ChannelProperties.Add("secure", _
     True)
' add other properties here.
Dim chnl As New Tcp.TcpChannel( _
     ChannelProperties, Nothing, _
     Nothing)
ChannelServices.RegisterChannel( _
     chnl, True)

Sometimes, the most secure code is also the simplest.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube