only for RuBoard |
Example 7-1 contains a listing for a class called ServerInfo . Consider it the humble beginnings of a load balancer. It can provide the machine name , an IP address, the processor usage, and the available memory of the machine on which it runs. However, in this chapter, it serves more form than function, so for now, forget about what it does and look at what it contains: an event, an enumeration, three methods (a sub, a function, and a shared function), and two read-only properties. While pondering these contents deeply, save it to a file named ServerInfo.vb and compile it to a class library. You will need this assembly as the basis of the rest of the chapter.
'vbc /t:library serverinfo.vb /r:system.dll Imports System Imports System.Diagnostics Imports System.Net Imports System.Threading Public Class ServerInfo Private machine As String Private ip As IPAddress Public Enum Tasks EnterInfiniteLoop = 1 WasteMemory = 2 Allocate2GigForTheBrowser = 3 RandomlyDestroyProcess = 4 End Enum Public Event TaskCompleted As EventHandler Public Sub New( ) 'Get machine info when object is created machine = Dns.GetHostName( ) Dim ipHost As IPHostEntry = Dns.GetHostByName(machine) ip = ipHost.AddressList(0) End Sub 'This routine only fires an event right now Public Sub DoTask(ByVal task As Tasks) RaiseEvent TaskCompleted(Me, EventArgs.Empty) End Sub 'Shared method Public Shared Function GetMachineTime( ) As DateTime Return DateTime.Now End Function 'Get % of process currently in use Public Function GetProcessorUsed( ) As Single If PerformanceCounterCategory.Exists("Processor") Then Dim pc As New PerformanceCounter("Processor", _ "% Processor Time", "_Total", True) Dim sampleA As CounterSample Dim sampleB As CounterSample sampleA = pc.NextSample( ) Thread.Sleep(1000) sampleB = pc.NextSample( ) Return CounterSample.Calculate(sampleA, sampleB) End If End Function 'Get MBytes of free memory Public Function GetAvailableMemory( ) As Long If PerformanceCounterCategory.Exists("Memory") Then Dim pc As New PerformanceCounter("Memory", "Available MBytes") Return pc.RawValue( ) End If End Function Public ReadOnly Property MachineName( ) As String Get Return machine End Get End Property Public ReadOnly Property IPAddress( ) As IPAddress Get Return ip End Get End Property End Class
The listing for ServerInfo.dll is in plain sight, so seeing its contents is easy. However, pretend that the source code doesn't exist. Can the assembly be queried programmatically for its type information? Sure it can. That's what reflection is all about.
The System.Reflection namespace and the System.Type class together will meet most reflection needs. This chapter will not discuss everything that can be accomplished with these classes. By the end of the chapter, though, your handle on reflection should be tighter than a G.I. Joe with Kung-Fu Action Grip.
The first step in type discovery is loading the assembly (the executable or library) in question; Example 7-2 shows the code that does this. Note that " Assembly " is a reserved word, so it is enclosed in square brackets in Example 7-2. If this were not done, System.Reflection.Assembly would have to be used instead. Who wants to type all that? The example also assumes that ServerInfo.dll is located in the same directory where the compiled executable will reside. If it is not, you will have to provide a full path to the Assembly.LoadFrom method.
Imports System Imports System.Reflection Public Class ObjectInfo Public Sub New (ByVal assemblyName As String) Dim a As [Assembly] = [Assembly].LoadFrom(assemblyName) End Sub End Class Public Class Application Public Shared Sub Main( ) Dim oi As New ObjectInfo("ServerInfo.dll") Console.ReadLine( ) End Sub End Class
Save this example to a file called reflect.vb . Or, as an alternative, add the Application class and the ObjectInfo class to the ServerInfo.dll assembly and recompile it as an executable. In that case, the constructor to ObjectInfo needs modification so it obtains a reference to the current assembly instead of ServerInfo.dll . Modify it by replacing the call to Assembly.LoadFrom with Assembly.GetExecutingAssembly :
'Get the currently executing assembly Dim a As [Assembly] = [Assembly].GetExecutingAssembly( )
At this point, the ObjectInfo class does not do much more than load an assembly. Before compiling, remedy the situation by grabbing all the types in ServerInfo.dll and writing them out to the console. Doing so will get the ball rolling. The code is as follows :
Public Class ObjectInfo Public Sub New (ByVal assemblyName As String) Dim a As [Assembly] = [Assembly].LoadFrom(assemblyName) Dim t As Type Dim types As Type( ) = a.GetTypes( ) For Each t In types Console.WriteLine(t.FullName) Next t End Sub End Class Public Class Application Public Shared Sub Main( ) Dim oi As New ObjectInfo("ServerInfo.dll") Console.ReadLine( ) End Sub End Class
Assembly.GetTypes returns an array of System.Type . This array represents every type in the assembly. The types contained in ServerInfo.dll will come back as follows:
ServerInfo.ServerInfo ServerInfo.ServerInfo+Tasks
There are two types in the assembly: the ServerInfo class and an enumeration named Tasks . To avoid confusion, properties, methods, and constructors are not categorized as types. They are considered elements of types.
Theoretically, the ServerInfo.dll assembly could contain more classes. As the types contained in the assembly are iterated in the ObjectInfo class, it might be beneficial to know what is what. System.Type contains several Boolean properties, shown in Table 7-1, that can help determine what a Type object represents.
Method | Description |
---|---|
IsArray | Type is an array. |
IsClass | Type is a class. |
IsCOMObject | Type is a COM object. |
IsEnum | Type is an enum. |
IsInterface | Type is an interface. |
IsPointer | Type is a pointer. |
IsPrimitive | Type is a primitive data type. |
IsValueType | Type is a structure. |
It's a good idea to modify the constructor in ObjectInfo to look for class types only, since for the sake of simplicity, this chapter will discuss only classes. If a class is found, we'll pass the type along to a private method named DumpClassInfo (the source code for which is presented later in this chapter):
For Each t In types If t.IsClass( ) Then DumpClassInfo(t) End If Next t
Try to follow the discussion for now. Don't worry about coding along. After everything is discussed, a final code listing will contain everything.
The private method, DumpClassInfo , relies on several methods provided by the System.Type class that return Info objects. This is a general name given to the .NET class library classes of the following types: ConstructorInfo , EventInfo , FieldInfo , MethodInfo , ParameterInfo , and PropertyInfo . Each class represents the attributes of a class entity, and each is derived from a common parent, MemberInfo .
You can obtain references for these objects by calling a corresponding Get method on System.Type . For instance, Type.GetConstructors returns an array of ConstructorInfo objects, and Type.GetMethods returns an array of MethodInfo objects. These classes provide information about the constructors and methods of a class, respectively. Each Get method also has a single form: Type.GetConstructor , Type.GetProperty , and so forth. Table 7-2 summarizes these methods.
Method | Returns |
---|---|
GetConstructor/GetConstructors | ConstructorInfo |
GetEvent/GetEvents | EventInfo |
GetField/GetFields | FieldInfo |
GetMethod/GetMethods | MethodInfo |
GetParameter/GetParameters | ParameterInfo |
GetProperty/GetProperties | PropertyInfo |
Using the specific classes in Table 7-2 is not necessary, but they do make life easier. Type.GetMembers returns an array of MemberInfo objects. From here, you can determine the specific type of each member by examining the MemberInfo object that corresponds to it. Here's a quick example. Given that t is an instance of Type (that represents a class), the following code is possible:
Dim member As MemberInfo Dim members( ) As MemberInfo = t.GetMembers( ) For Each member In members Select Case member.MemberType Case MemberTypes.Constructor Case MemberTypes.Event Case MemberTypes.Field Case MemberTypes.Method Case MemberTypes.Property End Select Next member
However, using the specific Info methods provided through Type is easier.
The time to write ObjectInfo.DumpClassInfo draws nigh. This method, once written, can be used to examine any class. The final listing will be fairly long, so this chapter will examine the code one step at a time. All pretenses aside, the primary motivation of this example is to cover as much of reflection as possible without turning the chapter into a reference manual.
Regardless, the example needs some semblance of credibility. To give it a purpose, let's approach it as if it will be used to generate class documentation. This is a practical for reflection.
DumpClassInfo should first output the name of the class by calling Type.FullName . This step returns the fully qualified name of the type (meaning it includes the namespace):
Private Sub DumpClassInfo(ByVal t As Type) Console.WriteLine(New String("-"c, 80)) Console.WriteLine("Class: {0}", t.FullName)
It's easy to get the constructors by calling Type.GetConstructors . This method returns an array of ConstructorInfo . One of the overrides of GetConstructors accepts a bitmask containing values from the BindingFlags enumeration as a parameter. This enumeration provides a means to restrict what is searched during reflection. Here, the Public and Instance members of the BindingFlags enumeration are used, so only the public instance constructors are asked for. Any nonpublic constructors will not be returned:
'Get constructors Dim c As ConstructorInfo Dim ci( ) As ConstructorInfo = _ t.GetConstructors(BindingFlags.Public Or BindingFlags.Instance) Console.WriteLine("Constructors:") For Each c In ci Console.WriteLine(" {0}", c) Next c
If BindingFlags.Instance (or BindingFlags.Static ) is not used in conjunction with BindingFlags.Public (or BindingFlags.NonPublic ), no members will be returned.
Examining the output of the previous block of code, note that the enumeration is no longer listed. It is not listed because the call to Type.IsClass wraps the call to DumpClassInfo . The output, thus far, should look similar to this:
--------------------------------------------------------------------------- Class: ServerInfo.ServerInfo Constructors: Void .ctor( )
Now, this example is definitely not Visual Basic. In C, C++, C#, and Java, void denotes a function that does not return a value. In VB.NET, a function that does not return a value is a Sub . To make this code readable to VB.NET programmers, you only need to replace Void with Public Sub . The .ctor is short for "constructor," or in VB.NET-speak, New .
Translating the output into VB.NET is a fairly straightforward string replacement. Instead of writing the raw constructor signature to the console, use this code:
For Each c In ci Dim s As String = String.Format("{0}{1}", vbTab, c.ToString( )) Console.WriteLine(s.Replace("Void .ctor", "Public Sub New")) Next c
Even if the BindingFlags enumeration were not used as a filter in this example, the ConstructorInfo class (like all the Info classes) contains all the members needed to determine everything about a class' attributes: Is it public? Is it private? Is it abstract? Is inheritance allowed? And so on.
This chapter does not cover the minutiae of it all. Just understand that all the classes that derive from MemberInfo are very similar in form and function.
The code for obtaining public instance properties is similar to that for constructors. Additionally, a call to PropertyInfo.CanRead and PropertyInfo.CanWrite can determine the property's accessibility. The following code, when added to the DumpClassInfo method, displays property information:
Dim p As PropertyInfo Dim pi( ) As PropertyInfo = _ t.GetProperties(BindingFlags.Public Or BindingFlags.Instance) Console.WriteLine( ) Console.WriteLine("Properties:") For Each p In pi Console.Write("Public ") If p.CanRead And p.CanWrite Then ElseIf p.CanRead Then Console.Write("ReadOnly ") ElseIf p.CanWrite Then Console.Write("WriteOnly ") End If Console.WriteLine("Property {0}( ) As {1}", p.Name, _ p.PropertyType.ToString( )) Next p
This code returns the properties as VB.NET declarations:
--------------------------------------------------------------------------- Class: ServerInfo.ServerInfo Constructors: Public Sub New( ) Properties : Public ReadOnly Property MachineName( ) As System.String Public ReadOnly Property IPAddress( ) As System.Net.IPAddress
Apparently, the PropertyInfo class does not return as much information about a property as some of the other Info classes do for their counterparts. For instance, if you look at the class definition, there does not appear to be a way to do much beyond determining whether a property is read-only, write-only, or both (using the CanWrite and CanRead properties).
However, this impression is not completely accurate. Behind the scenes, VB.NET properties are implemented with corresponding Get and Set methods (see Section 3.2 in Chapter 3). To get additional information about the property (such as whether it is private, inheritable, or shared), you must work with these methods rather than with the PropertyInfo class. The next section details the type of information you can obtain by examining a method. First, though, you need to get to the accessor methods.
You can access the accessor methods in one of two ways: by calling either PropertyInfo.GetGetMethod or PropertyInfo.GetSetMethod . Each returns a MethodInfo class that represents the appropriate accessor. Also, it is possible to call PropertyInfo.GetAccessors , which returns an array of MethodInfo objects. This class is very similar to ConstructorInfo and contains all the functionality necessary to describe the method in question.
To get the methods of a class, call Type.GetMethods , which returns an array of MethodInfo objects. This time, however, in the name of pure, unadulterated entertainment, the BindingFlags bitmask will not be used. Our goal is to demonstrate how the information provided by BindingFlags can be determined without using the bitmask.
While iterating through each method, you can see if the method is declared in the current class by calling MethodInfo.DeclaringType . Calling it filters out all methods that are inherited from System.Object or from another base class, had we used inheritance explicitly when creating the ServerInfo class. This way, the listing reflects the only current type, the ServerInfo class, and its unique methods and events. The following code accomplishes this:
'Get public shared methods Dim i As Integer Dim returnType As Type Dim returnString As String Dim m As MethodInfo Dim mi( ) As MethodInfo = t.GetMethods( ) Console.WriteLine("Methods:") For Each m In mi If m.DeclaringType.Equals(t) Then
You can filter out inherited members in the call to GetMethods by supplying the BindingFlags.DeclaredOnly constant using a method call like the following:
mi = t.GetMethods(BindingFlags.Public Or _ BindingFlags.Instance Or _ BindingFlags.DeclaredOnly)
To determine the accessibility of a method, various properties are available from MethodInfo . See how these property names map IL to VB.NET:
If m.IsPublic Then Console.Write("Public ") ElseIf m.IsPrivate Then Console.WriteLine("Private ") ElseIf m.IsFamily Then Console.WriteLine("Protected ") ElseIf m.IsAssembly Then Console.WriteLine("Friend ") ElseIf m.IsFamilyAndAssembly Then Console.WriteLine("Protected Friend ") End If If m.IsStatic Then Console.Write("Shared ") End If
These properties, such as IsPublic , IsFamily , and IsStatic , are defined in a class named MethodBase , which is the parent class of both MethodInfo and ConstructorInfo . ConstructorInfo functions the same way as MethodInfo , except it is focused on constructors.
|
The return type for the reflected method can be snagged by calling, oddly enough, the property named ReturnType . Who could have guessed? If the return type is equal to System.Void , the method is a Sub ; otherwise , it's a Function . After determining which is which, our code calls MethodInfo.Name to retrieve the name of the method.
returnType = m.ReturnType If String.Compare(returnType.ToString( ), "System.Void") <> 0 Then Console.Write("Function {0}(", m.Name) returnString = String.Format(" As {0}", returnType.ToString( )) Else Console.Write("Sub {0}(", m.Name) End If
Divining the parameters for the method is as simple as calling MethodInfo.GetParameters , which returns an array of ParameterInfo objects. This class can query the parameter's name and type, as shown in the next code fragment for DumpClassInfo :
Dim parms( ) As ParameterInfo = m.GetParameters( ) For i = 0 To parms.Length - 1 Console.Write(parms(i).Name) Console.Write(" As ") Console.Write(parms(i).ParameterType( )) If (i < parms.Length - 1) Then Console.Write(", ") End If Next i Console.Write(")") Console.WriteLine(returnString) End If Next m
Up to this point, the output of ServerInfo looks like this (if you reformatted it to fit in a book):
Methods: Public Sub remove_TaskCompleted(obj As ServerInfo.ServerInfo+TaskCompletedEventHandler) Public Sub add_TaskCompleted(obj As ServerInfo.ServerInfo+TaskCompletedEventHandler) Public Sub DoTask(task As ServerInfo.ServerInfo+Tasks) Public Shared Function GetMachineTime( ) As System.DateTime Public Function GetProcessorUsed( ) As System.Single Public Function GetAvailableMemory( ) As System.Int64 Public Function get_MachineName( ) As System.String Public Function get_IPAddress( ) As System.Net.IPAddress
get_MachineName and get_IPAddress are the read-only accessor methods of the MachineName and IPAddress properties, so they should probably not be listed here because they were already handled in the properties section. An additional property of the MethodInfo class called IsSpecialName can prevent the display of accessor methods. To use it, modify the If statement that tests to eliminate inherited members as follows:
If m.DeclaringType.Equals(t) AndAlso m.IsSpecialName = False Then
If you disassemble ServerInfo.dll , you will see that both accessor methods have the specialname IL attribute associated with them. This flag is provided for compiler writers and tool vendors and allows a member to be treated specially. For instance, IntelliSense uses it to prevent the display of accessor methods. However, it doesn't do anything that affects the way code is run.
Once we eliminate methods with special names, the output appears as follows:
Methods: Public Sub DoTask(task As ServerInfo.ServerInfo+Tasks) Public Shared Function GetMachineTime( ) As System.DateTime Public Function GetProcessorUsed( ) As System.Single Public Function GetAvailableMemory( ) As System.Int64
Example 7-3 contains a revised listing of the ObjectInfo class. It was rewritten slightly, but all the functionality is there (including events). Your assignment, should you choose to accept it, is to add support for other types, such as enumerations and value types. Also, nested types (that is, classes contained within classes) are not handled at all. To handle them, look at Type.GetNestedTypes , if possible. There is really nothing to it. Everything in reflection is aptly named. You can learn a lot just by hacking the example.
Example 7-3 is long, but don't run away. The wonderful world of reflection is far from fully explored. More needs to be covered before the topic can be put to rest. But check out the example and spend some quality time with it.
Public Class ObjectInfo Private Sub DumpConstructors(ByVal t As Type) Dim c As ConstructorInfo Dim ci( ) As ConstructorInfo = _ t.GetConstructors(BindingFlags.Public Or BindingFlags.Instance) Console.WriteLine( ) Console.WriteLine("Constructors:") For Each c In ci Dim s As String = c.ToString( ) Console.WriteLine(s.Replace("Void .ctor", "Public Sub New")) Next c End Sub Private Sub DumpEvents(ByVal t As Type) Dim e As EventInfo Dim ei As EventInfo( ) = _ t.GetEvents(BindingFlags.Public Or _ BindingFlags.Instance) Console.WriteLine( ) Console.WriteLine("Events:") For Each e In ei Console.WriteLine(e.Name) Next e End Sub Private Sub DumpProperties(ByVal t As Type) Dim p As PropertyInfo Dim pi( ) As PropertyInfo = _ t.GetProperties(BindingFlags.Public Or BindingFlags.Instance) Console.WriteLine( ) Console.WriteLine("Properties:") For Each p In pi Console.Write("Public ") If p.CanRead And p.CanWrite Then ElseIf p.CanRead Then Console.Write("ReadOnly ") ElseIf p.CanWrite Then Console.Write("WriteOnly ") End If Console.WriteLine("Property {0}( ) As {1}", _ p.Name, p.PropertyType.ToString( )) Next p End Sub Private Sub DumpMethod(ByVal m As MethodInfo) Dim i As Integer Dim returnType As Type Dim returnString As String If m.IsPublic Then Console.Write("Public ") ElseIf m.IsPrivate Then Console.WriteLine("Private ") ElseIf m.IsFamily Then Console.WriteLine("Protected ") ElseIf m.IsAssembly Then Console.WriteLine("Friend ") ElseIf m.IsFamilyAndAssembly Then Console.WriteLine("Protected Friend ") End If If m.IsStatic Then Console.Write("Shared ") End If returnType = m.ReturnType If String.Compare(returnType.ToString( ), "System.Void") <> 0 Then Console.Write("Function {0}(", m.Name) returnString = String.Format(" As {0}", returnType.ToString( )) Else Console.Write("Sub {0}(", m.Name) End If Dim parms( ) As ParameterInfo = m.GetParameters( ) For i = 0 To parms.Length - 1 Console.Write(parms(i).Name) Console.Write(" As ") Console.Write(parms(i).ParameterType( )) If (i < parms.Length - 1) Then Console.Write(", ") End If Next i Console.WriteLine(")") End Sub Private Sub DumpMethods(ByVal t As Type) Dim m As MethodInfo Dim mi( ) As MethodInfo = t.GetMethods( ) Console.WriteLine( ) Console.WriteLine("Methods:") For Each m In mi If m.DeclaringType.Equals(t) AndAlso _ m.IsSpecialName = False Then DumpMethod(m) End If Next m End Sub Private Sub DumpClassInfo(ByVal t As Type) Console.WriteLine(New String("-"c, 80)) Console.WriteLine("Class: {0}", t.FullName) DumpConstructors(t) DumpEvents(t) DumpProperties(t) DumpMethods(t) Console.WriteLine( ) End Sub Public Sub New(ByVal assemblyName As String) Dim a As [Assembly] If (assemblyName = Nothing) Then a = [Assembly].GetExecutingAssembly( ) Else a = [Assembly].LoadFrom(assemblyName) End If Dim t As Type Dim types As Type( ) = a.GetTypes( ) For Each t In types If t.IsClass Then DumpClassInfo(t) End If Next t End Sub End Class
only for RuBoard |