Reflection

Overview

Reflection is used to retrieve the internal details of assemblies and types at runtime. Reflection is commonly used to discover which classes exist in an assembly, and which properties, methods, and events exist in a class. Collectively, this information is known as metadata. You can also use reflection to dynamically generate code, instantiate types or call methods by name, and interact with unknown objects. Simply put, reflection is the slightly mind-bending technique of exploring code structures programmatically.

Reflection is a key ingredient in many Microsoft .NET Framework features. For example, reflection is required to support Microsoft ASP.NET data binding, to pre-compile regular expression classes, and to allow some types of Web Service extensibility. In most cases, you'll use reflection indirectly without even realizing it. However, there are some tasks that do require your code to use reflection directly. One example is if you want to create a highly modular, extensible application, in which case you'll use reflection to load types at runtime (see recipe 9.6). Other examples of reflection include loading an assembly from the Internet (recipe 9.7), using custom attributes (recipe 9.9), and compiling code programmatically (recipe 9.12). We'll examine all these techniques in this chapter, along with the basics of exploring assemblies, types, and members (recipes 9.1 to 9.5).


Generate a Dynamic About Box

Problem

You want to retrieve version information at runtime for display in an About box.

Solution

Retrieve a reference to the current assembly using Assembly.GetExecutingAssembly, and retrieve its AssemblyName, which includes version information.

Discussion

It's important for an application to correctly report its version (and sometimes additional information such as its filename and culture) without needing to hardcode this data. Reflection provides the ideal solution because it allows you to retrieve these details directly from the assembly's metadata.

The following code snippet displays several pieces of information about the current assembly using reflection. It also shows how you can retrieve some of the same information indirectly from the System.Windows.Forms.Application class (regardless of the application type).

Public Module TestReflection
 
 Public Sub Main()
 Dim ExecutingApp As System.Reflection.Assembly
 ExecutingApp = System.Reflection.Assembly.GetExecutingAssembly()
 
 Dim Name As System.Reflection.AssemblyName
 Name = ExecutingApp.GetName()
 
 ' Display metadata information.
 Console.WriteLine("Application: " & Name.Name)
 Console.WriteLine("Version: " & Name.Version.ToString())
 Console.WriteLine("Code Base: " & Name.CodeBase)
 Console.WriteLine("Culture: " & Name.CultureInfo.DisplayName)
 Console.WriteLine("Culture Code: " & Name.CultureInfo.ToString())
 ' (If the assembly is signed, you can also use Name.KeyPair to
 ' retrieve the public key.)
 
 ' Some additional can be retrieved from the Application class.
 ' The version information is identical.
 Console.WriteLine("Assembly File: " & _
 System.Windows.Forms.Application.ExecutablePath)
 Console.WriteLine("Version: " & _
 System.Windows.Forms.Application.ProductVersion)
 
 ' The Company and Product information is set through the
 ' AssemblyCompany and AssemblyProduct attributes, which are
 ' usually coded in the AssemblyInfo.vb file.
 Console.WriteLine("Company: " & _
 System.Windows.Forms.Application.CompanyName)
 Console.WriteLine("Product: " & _
 System.Windows.Forms.Application.ProductName)
 
 ' The culture information retrieves the current culture
 ' (in this case, en-US), while the reflection code
 ' retrieves the culture specified in the assembly
 ' (in this case, none).
 Console.WriteLine("Culture: " & _
 System.Windows.Forms.Application.CurrentCulture.ToString())
 Console.WriteLine("Culture Code: " & _
 System.Windows.Forms.Application.CurrentCulture.DisplayName)
 
 Console.ReadLine()
 End Sub
 
End Module

Note that GetExecutingAssembly always returns a reference to the assembly where the code is executing. In other words, if you launch a Microsoft Windows application (assembly A) that uses a separate component (assembly B), and the component invokes GetExecutingAssembly, it will receive a reference to assembly B. You can also use GetCallingAssembly, which retrieves the assembly where the calling code is located, or GetEntryAssembly, which always returns the executable assembly for the current application domain.

  Note

Assembly is a reserved keyword in Microsoft Visual Basic .NET. Thus, if you want to reference the System.Reflection.Assembly type, you must use a fully qualified reference or you must enclose the word Assembly in square brackets.

' This works.
Dim Asm As System.Reflection.Assembly
 
' This also works, assuming you have imported the 
' System.Reflection namespace.
Dim Asm As [Assembly]
 
' This generates a compile-time error because the word Assembly is reserved.
Dim Asm As Assembly


List Assembly Dependencies

Problem

You want to list all the assemblies that are required by another assembly.

Solution

Use the Assembly.GetReferencedAssemblies method.

Discussion

All .NET assemblies include a header that lists assembly references. If the referenced assembly has a strong name, the header includes the required version and public key for the referenced assembly.

Once you retrieve a reference to an assembly, it's easy to find its dependencies using the GetReferencedAssemblies method. Consider this code, which iterates through the assembly references of the current executing assembly:

Public Module TestReflection
 
 Public Sub Main()
 Dim ExecutingAssembly As System.Reflection.Assembly
 ExecutingAssembly = System.Reflection.Assembly.GetExecutingAssembly()
 
 Dim ReferencedAssemblies() As System.Reflection.AssemblyName
 ReferencedAssemblies = ExecutingAssembly.GetReferencedAssemblies()
 
 Dim ReferencedAssembly As System.Reflection.AssemblyName
 For Each ReferencedAssembly In ReferencedAssemblies
 Console.Write(ReferencedAssembly.Name & " (")
 Console.WriteLine(ReferencedAssembly.Version.ToString() & ")")
 Next
 
 Console.ReadLine()
 End Sub
 
End Module

This code produces output such as the following:

mscorlib (1.0.3300.0)
Microsoft.VisualBasic (7.0.3300.0)
System (1.0.3300.0)
System.Data (1.0.3300.0)
System.Xml (1.0.3300.0)

You can also find the assembly references for any assembly on the computer hard drive. Use the Assembly.LoadFrom method, as shown here:

Asm = Assembly.LoadFrom("c:	empmyassembly.dll")

If the assembly is found in the global assembly cache (GAC), you can use the Assembly.Load or Assembly.LoadWithPartialName methods instead, which retrieve the assembly using all or part of its strong name. For example, you can find out what assemblies are required to support the core System.Web.dll assembly using this code:

Asm = Assembly.LoadWithPartialName("System.Web")
 


Get Type Information from a Class or an Object

Problem

You want to retrieve information about any .NET type (class, interface, structure, enumeration, and so on).

Solution

Use the Visual Basic .NET command GetType with the class name. Or use the Object.GetType instance method with any object.

Discussion

The System.Type class is one of the core ingredients in reflection. It allows you to retrieve information about any .NET type and drill down to examine type members such as methods, properties, events, and fields. To retrieve a Type object for a given class, you use the Visual Basic GetType command, as shown here:

' Retrieve information about the System.Xml.XmlDocument class.
Dim TypeInfo As Type
TypeInfo = GetType(System.Xml.XmlDocument)

Alternatively, you can retrieve type information from an object by calling the GetType method.

' Create a "mystery" object.
Dim MyObject As Object = New System.Xml.XmlDocument()
 
' Retrieve information about the object.
Dim TypeInfo As Type = MyObject.GetType()

Both of these approaches have equivalent results. The only difference is that one works with uninstantiated class names, and the other technique requires a live object.

Finally, you can also create a Type object using a string with a fully qualified class name and the shared Type.GetType method.

Dim TypeName As String = "System.Xml.XmlDocument"
Dim TypeInfo As Type = Type.GetType(TypeName)
  Note

The shared Type.GetType method will only consider the types in the current (executing) assembly and any of its referenced assemblies. In other words, if you try to retrieve the type XmlDocument, you must have a reference to the System.Xml.dll assembly, or the call will fail. To get around this limitation, you can retrieve a type from a specific assembly using the Assembly.GetType instance method, as described in recipe 9.5.

The Type class provides a large complement of methods and properties. The following code snippet shows a simple test for retrieving basic type information:

Public Module TestReflection
 Public Sub Main()
 Dim TypeInfo As Type
 TypeInfo = GetType(System.Xml.XmlDocument)
 
 Console.WriteLine("Type Name: " & TypeInfo.Name)
 Console.WriteLine("Namespace: " & TypeInfo.Namespace)
 Console.WriteLine("Assembly: " & TypeInfo.Assembly.FullName)
 
 If TypeInfo.IsClass Then
 Console.WriteLine("It's a Class")
 ElseIf TypeInfo.IsValueType Then
 Console.WriteLine("It's a Structure")
 ElseIf TypeInfo.IsInterface Then
 Console.WriteLine("It's an Interface")
 ElseIf TypeInfo.IsEnum Then
 Console.WriteLine("It's an Enumeration")
 End If
 
 Console.ReadLine()
 End Sub
 
End Module

The output of this code is as follows:

Type Name: XmlDocument
Namespace: System.Xml
Assembly: System.Xml, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c
561934e089
It's a Class

One of the most interesting operations you can perform with a type is to examine its members. This technique is demonstrated in recipe 9.4.


Examine a Type for Members

Problem

You want to retrieve information about the properties, events, methods, and other members exposed by a type.

Solution

Use methods such as Type.GetMethods, Type.GetProperties, Type.GetEvents, and so on.

Discussion

The Type class is a starting point for a detailed examination of any .NET type. You can use the following methods to delve into the structure of a type:

  • GetConstructorsretrieves an array of ConstructorInfo objects, which detail the constructors for a type.
  • GetMethodsretrieves an array of MethodInfo objects, which describe the functions and subroutines provided by a type.
  • GetPropertiesretrieves an array of PropertyInfo objects, which describe the properties for a type.
  • GetEventsretrieves an array of EventInfo objects, which describe the constructors for a type.
  • GetFieldsretrieves an array of FieldInfo objects, which represent the member variables of a type.
  • GetInterfacesretrieves an array of Type objects, which represent the interfaces implemented by this type.

All the xxxInfo classes are contained in the System.Reflection namespace and derive from MemberInfo. They add additional informational properties. For example, using MethodInfo, you can determine the data type of all method arguments and return values. In addition, you can retrieve a single MemberInfo array for a type by using the GetMembers method. This array will contain all the events, properties, constructors, and so on for the type.

As a rule of thumb, the xxxInfo methods return all the members of type, whether they are public, private, shared, or instance members. You can filter which members are returned by passing in values from the System.Reflection.BindingFlags enumeration when you call the method. For example, use BindingFlags.Instance in conjunction with BindingFlags.Public to retrieve public instance members only.

The following example demonstrates a test program that asks for the name of a class and then provides information about all its members. To shorten the amount of code required, all members are printed using the generic DisplayMembers subroutine shown here:

Private Sub DisplayMembers(ByVal members() As MemberInfo)
 
 Dim Member As MemberInfo
 For Each Member In members
 Console.WriteLine(Member.ToString())
 Next
 Console.WriteLine()
 
End Sub

The disadvantage of this approach is that every type of member is dealt with as a generic MemberInfo and displayed using the ToString method. ToString lists all the important information about a method, but it uses C# syntax, which means that data types precede variable names and function definitions, subroutines are distinguished from functions using the void keyword, and so on. A more detailed reflector would create a Visual Basic–specific display by examining the properties of the specialized MemberInfo classes.

Below is a partial listing of the code. For the full example, consult the book's sample code for this chapter.

Public Module TestReflection
 
 Public Sub Main()
 Console.Write("Enter the name of a type to reflect on: ")
 Dim TypeName As String = Console.ReadLine()
 Dim TypeInfo As Type
 If TypeName <> "" Then TypeInfo = Type.GetType(TypeName)
 Console.WriteLine()
 If TypeInfo Is Nothing Then
 Console.WriteLine("Invalid type name.")
 Return
 End If
 
 ' List shared fields.
 Dim Fields As FieldInfo() = TypeInfo.GetFields((BindingFlags.Static _
 Or BindingFlags.NonPublic Or BindingFlags.Public))
 Console.WriteLine(New String("-"c, 79))
 Console.WriteLine("**** Shared Fields ****")
 Console.WriteLine(New String("-"c, 79))
 DisplayMembers(Fields)
 
 ' List shared properties.
 Dim Properties As PropertyInfo() 
 Properties = TypeInfo.GetProperties((BindingFlags.Static _
 Or BindingFlags.NonPublic Or BindingFlags.Public))
 Console.WriteLine(New String("-"c, 79))
 Console.WriteLine("**** Shared Properties ****")
 Console.WriteLine(New String("-"c, 79))
 DisplayMembers(Properties)
 
 ' (Remainder of code omitted.)
 End Sub
 
 ' (DisplayMembers function omitted.)
 
End Module

A typical test run produces the following (abbreviated) output:

Enter the name of a type to reflect on: System.String
 
------------------------------------------------------------------------------
**** Shared Fields ****
------------------------------------------------------------------------------
System.String Empty
Char[] WhitespaceChars
Int32 TrimHead
Int32 TrimTail
Int32 TrimBoth
 
------------------------------------------------------------------------------
**** Shared Methods ****
------------------------------------------------------------------------------
System.String Join(System.String, System.String[])
System.String Join(System.String, System.String[], Int32, Int32)
...


Examine an Assembly for Types

Problem

You want to display all the types in an assembly.

Solution

Use the Assembly.GetTypes method.

Discussion

The Assembly.GetTypes method returns an array of Type objects that represent all the classes, interfaces, enumerations, and other types defined in an assembly. You can use this method in conjunction with the methods of the Type class (shown in recipe 9.4) to "walk" the structure of an assembly.

The following example demonstrates a simple knock-off of the IL disassembler (ILDASM) included with the .NET Framework SDK. It's a Windows application that allows the user to choose an assembly file and then displays a hierarchical tree that shows all the types it contains. Figure 9-1 shows the test application at work on a thread test created for Chapter 7.

click to expand
Figure 9-1: A reflection browser that uses the TreeView control.

Using the reflector, you can drill down to find more information about members, including the data types for properties and method parameters, and the signature for event handlers, as shown in Figure 9-2.

click to expand
Figure 9-2: Viewing members in the reflection browser.

The bulk of the code in this example is in the Click event handler for the Reflect button. The event handler prompts the user to choose an assembly, loads it, and iterates through all the types and members.

Private Sub cmdReflect_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles cmdReflect.Click
 
 ' Show a dialog box that allows the user to choose an assembly.
 Dim dlgOpen As New OpenFileDialog()
 dlgOpen.Filter = "Assemblies (*.dll;*.exe) | *.dll;*.exe"
 
 If dlgOpen.ShowDialog() <> DialogResult.OK Then Return
 
 ' Load the selected assembly.
 Dim Asm As System.Reflection.Assembly
 Try
 Asm = System.Reflection.Assembly.LoadFrom(dlgOpen.FileName)
 Catch Err As Exception
 MessageBox.Show(Err.ToString, "Invalid Assembly", _
 MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
 Return
 End Try
 
 lblAssembly.Text = "Reflecting on assembly : " & Asm.FullName
 
 ' Define some variables used to "walk" the program structure.
 Dim Types(), TypeInfo As Type
 Dim Events(), EventInfo As System.Reflection.EventInfo
 Dim Methods(), MethodInfo As System.Reflection.MethodInfo
 Dim Parameters(), ParameterInfo As System.Reflection.ParameterInfo
 Dim Properties(), PropertyInfo As System.Reflection.PropertyInfo
 Dim nodeParent, node, subNode As TreeNode
 
 ' Build up the TreeView.
 ' Begin by iterating over all the types.
 treeTypes.Nodes.Clear()
 Types = Asm.GetTypes()
 For Each TypeInfo In Types
 nodeParent = treeTypes.Nodes.Add(TypeInfo.FullName)
 
 ' Add nodes for all the properties.
 node = nodeParent.Nodes.Add("Properties")
 Properties = TypeInfo.GetProperties()
 For Each PropertyInfo In Properties
 subNode = node.Nodes.Add(PropertyInfo.Name)
 
 ' Add information about the property.
 subNode.Nodes.Add("Type: " & PropertyInfo.PropertyType.ToString())
 subNode.Nodes.Add("Readable: " & PropertyInfo.CanRead)
 subNode.Nodes.Add("Writeable: " & PropertyInfo.CanWrite)
 Next
 
 ' Add nodes for all the Methods.
 node = nodeParent.Nodes.Add("Methods")
 Methods = TypeInfo.GetMethods()
 For Each MethodInfo In Methods
 subNode = node.Nodes.Add(MethodInfo.Name & "()")
 
 ' Add information about the method parameters.
 Parameters = MethodInfo.GetParameters()
 For Each ParameterInfo In Parameters
 subNode.Nodes.Add("Parameter '" & ParameterInfo.Name & _
 "': " & ParameterInfo.ParameterType.ToString())
 Next
 If MethodInfo.ReturnType.ToString() <> "System.Void" Then _
 subNode.Nodes.Add("Return: " & MethodInfo.ReturnType.ToString())
 Next
 
 ' Add nodes for all the events.
 node = nodeParent.Nodes.Add("Events")
 Events = TypeInfo.GetEvents()
 For Each EventInfo In Events
 subNode = node.Nodes.Add(EventInfo.Name)
 subNode.Nodes.Add(EventInfo.EventHandlerType.Name)
 Next
 Next
 
End Sub


Instantiate a Type by Name

Problem

You want to create an instance of an object that's named in a string.

Solution

Use the Assembly.CreateInstance method or the Activator.CreateInstance method.

Discussion

Both the System.Reflection.Assembly and the System.Activator classes provide a CreateInstance method. This recipe uses the Assembly class, and recipe 9.8 features an example with the Activator class.

To use CreateInstance, you supply a fully qualified type name. The CreateInstance method searches the assembly for the corresponding type, and then it creates and returns a new instance of the object (or a null reference if the object can't be found). You can also use overloaded versions of CreateInstance to supply constructor arguments or specify options that control how the search will be performed.

Here's an example that instantiates the MyClass type found in the MyNamespace namespace:

Dim MyObject As Object = Asm.CreateInstance("MyNamespace.MyClass")

The most common reason for loading a type by name is to support extremely configurable applications. For example, you might create an application that can be used with a variety of different logging components. To allow you to seamlessly replace the logging component without recompiling the code, you might load the logging component through reflection and interact with it through a generic interface. The assembly name and class name for the logging component would be read at startup from a configuration file.

To implement such a system, you would begin by defining a generic interface. In this case, we'll create an ILogger interface with one method, called Log.

Public Interface ILogger
 Sub Log(ByVal message As String)
End Interface

This interface is compiled into a separate assembly. You can then develop multiple logger classes, each of which will typically reside in its own assembly. Different logger classes might record messages in an event log, database, and so on. The following code shows a ConsoleEventLogger class, which simply displays the log message in a Console window:

Public Class ConsoleLogger
 Implements LogInterfaces.ILogger
 
 Public Sub Log(ByVal message As String) _
 Implements LogInterfaces.ILogger.Log
 Console.WriteLine(message)
 End Sub
 
End Class

To decide which logger to use, the main application uses a configuration file with two settings. LogAssemblyFilename indicates the name of the log assembly, and LogClassName indicates the name of the logging class in that assembly.



 
 
 
 
 
 

The main application reads these configuration files and uses reflection to load the corresponding assembly and instantiate the logging class. It then interacts with the object through the ILogger interface.

Public Module DynamicLoadTest
 
 Public Logger As LogInterfaces.ILogger
 
 Public Sub Main()
 Dim AssemblyName As String
 AssemblyName = ConfigurationSettings.AppSettings( _
 "LogAssemblyFilename")
 Console.WriteLine("Loading logger: " & AssemblyName)
 
 ' Load the assembly.
 Dim LogAsm As System.Reflection.Assembly
 LogAsm = System.Reflection.Assembly.LoadFrom(AssemblyName)
 
 Dim ClassName As String 
 ClassName = ConfigurationSettings.AppSettings("LogClassName")
 
 ' Create the class.
 Console.WriteLine("Creating type: " & ClassName)
 Logger = CType(LogAsm.CreateInstance(ClassName), _
 LogInterfaces.ILogger)
 
 ' Use the class.
 Logger.Log("*** This is a test log message. ***")
 
 Console.ReadLine()
 End Sub
 
End Module

When you run this sample, you'll see the log message in the Console window, as shown here:

Loading logger: ConsoleLogger.dll
Creating type: ConsoleLogger.ConsoleLogger
*** This is a test log message. ***


Load an Assembly from a Remote Location

Problem

You want to run an assembly from a server on your local network or the Internet.

Solution

Use the Assembly.LoadFrom method with a Uniform Resource Identifier (URI) that points to the remote assembly.

Discussion

The Assembly.LoadFrom method accepts an ordinary file path, a network universal naming convention (UNC) path, or a URL Web path. LoadFrom is sometimes used with highly dynamic applications that load components from the Web.

Here's a basic example that loads an assembly using a URI:

Dim Asm As System.Reflection.Assembly
Dim AsmPath As String = "http://myserver/mydir/myassembly.dll"
Asm = System.Reflection.Assembly.LoadFrom(AsmPath)

If you call LoadFrom and supply a path to a remote assembly, that assembly will be automatically downloaded to the GAC and then executed. The next time you use LoadFrom with the same path, the existing copy in the GAC will be used, unless a newer version is available at the indicated path. This approach ensures optimum performance.

Remember, the source of your code will influence the security context that is assigned. If you download code and then execute it from your hard drive, it will have full permissions. However, if you use LoadFrom and supply an intranet or Internet URL, the code will be assigned much lower permissions. (Typically, it will be given permission to execute but nothing more.) To circumvent this limitation, you can customize the security policy to grant additional permissions based on how the assembly is signed or the location from which it is downloaded. For more information, refer to a dedicated book about code access security, such as Visual Basic .NET Code Security Handbook, by Eric Lippert (Wrox Press, 2002).


Invoke a Method by Name

Problem

You want to invoke a method or set a property that's named in a string.

Solution

Use the Type.InvokeMember method.

Discussion

The Type class provides an InvokeMember method that's similar to the CallByName function in Visual Basic 6. It requires the object; the name of the field, property, or method (as a string); a flag that indicates whether the string corresponds to a field, property, or method; and an array of objects for any required parameters. For example, you can call a method with no arguments using this syntax:

Dim MyObject As New MyClass()
Dim TypeInfo As Type = MyObject.GetType()
 
' Call Refresh() on MyObject.
Dim Args() As Object = {}
TypeInfo.InvokeMember("Refresh", BindingFlags.Public Or _
 BindingFlags.InvokeMethod, Nothing, MyObject, Args)

Here's an example that calls a method that requires two arguments:

Dim Args() As Object = {42, "New Name"}
TypeInfo.InvokeMember("UpdateProduct", BindingFlags.Public Or _
 BindingFlags.InvokeMethod, Nothing, MyObject, Args)

You can even invoke shared members, such as the Math.Sin method, as shown here:

Dim TypeInfo As Type = GetType(Math)
Dim Args() As Object = {45}
Dim Result As Object
Result = TypeInfo.InvokeMember("Sin", BindingFlags.Public Or _
 BindingFlags.InvokeMethod Or BindingFlags.Static, Nothing, Nothing, _
 Args)
Console.WriteLine(Result.ToString()) ' Displays 0.85...

The following example allows a user to invoke any instance member for a class, provided that it doesn't require any parameters. The code creates the required type from the supplied string name using the System.Activator class. Figure 9-3 shows the results of a dynamic call to System.Guid.NewGuid.

click to expand
Figure 9-3: Dynamically invoking the Guid.NewGuid method.

Private Sub cmdInvoke_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles cmdInvoke.Click
 If txtClassName.Text = "" Then
 MessageBox.Show("Enter a class name.")
 Return
 End If
 
 ' Get the type.
 Dim TypeInfo As Type
 TypeInfo = Type.GetType(txtClassName.Text)
 If TypeInfo Is Nothing Then
 MessageBox.Show("Class name not recognized.")
 Return
 End If
 
 Try
 ' Try to create the object.
 ' The CreateInstance() method uses the constructor that
 ' matches the supplied parameters. (In this case, none.)
 Dim Target As Object = Activator.CreateInstance(TypeInfo)
 
 ' Invoke the method with no parameters.
 Dim Result As Object = TypeInfo.InvokeMember(txtMethodName.Text, _
 Reflection.BindingFlags.InvokeMethod, Nothing, Target, _
 New Object() {})
 
 ' Check if a result is retrieved, and display its string
 ' representation.
 If Not Result Is Nothing Then
 txtResult.Text = Result.ToString()
 End If
 Catch Err As Exception
 MessageBox.Show(Err.ToString)
 End Try
 
End Sub
  Note

You can also access methods, properties, and fields using the appropriate MemberInfo-derived class. For example, you can use the GetValue and SetValue methods of the PropertyInfo class and the Invoke method of the MethodInfo class.


Create, Apply, and Identify a Custom Attribute

Problem

You want to use custom attributes to decorate members and classes.

Solution

Create a class that derives from System.Attribute, apply it to a class or a member, and use the Type.GetCustomAttributes method to retrieve it during reflection.

Discussion

Attributes are a cornerstone of .NET extensibility. Using attributes, you can specify additional information about a type or a member that doesn't relate directly to the code. For example, .NET uses attributes to tell the debugger how to treat code, to tell Microsoft Visual Studio .NET how to display components and controls in the Properties windows, to implement COM+ services such as object pooling, and to support Web Services and Web Service–related extensibility mechanisms such as SOAP headers and SOAP extensions. You can also define and use your own custom attributes and then check for them during reflection. Most likely, you'll use custom attributes if you need to support your own specialized extensibility mechanism or if you want to configure how a hosting application works with the objects it hosts (for example, in a .NET Remoting scenario).

The first step is to create a custom attribute class by deriving from the System.Attribute class and adding the required properties. By convention, the name of this class should end with Attribute. For example, the custom LegacyAttribute class shown in the following code might be used to support an internal software tracking and auditing system by identifying code that is migrated over from a non-.NET platform:

Public Enum PlatformType
 VisualBasic6
 CPlus
 C
 VBScript
End Enum
 
 _
Public Class LegacyAttribute
 Inherits Attribute
 
 Private _PreviousPlatform As PlatformType
 Private _MigratedBy As String
 Private _MigratedDate As DateTime
 
 Public Property PreviousPlatform() As PlatformType
 Get
 Return _PreviousPlatform
 End Get
 Set(ByVal Value As PlatformType)
 _PreviousPlatform = Value
 End Set
 End Property
 
 Public Property MigratedBy() As String
 Get
 Return _MigratedBy
 End Get
 Set(ByVal Value As String)
 _MigratedBy = Value
 End Set
 End Property
 
 Public Property MigratedDate() As DateTime
 Get
 Return _MigratedDate
 End Get
 Set(ByVal Value As DateTime)
 _MigratedDate = Value
 End Set
 End Property
 
 Public Sub New(ByVal previousPlatform As PlatformType, _
 ByVal migratedBy As String, ByVal migratedDate As String)
 Me.PreviousPlatform = previousPlatform
 Me.MigratedBy = migratedBy
 Me.MigratedDate = DateTime.Parse(migratedDate)
 End Sub
 
End Class

Notice that the date is passed to the constructor as a string. A string is used because of the type restrictions placed on attribute declarations. You can use any integral data type (Byte, Short, Integer, Long) or floating-point data type (Single and Double), as well as Char, String, Boolean, any enumerated type, or System.Type. However, you can't use any other type, including more complex objects and the DateTime structure.

Every custom attribute class requires the AttributeUsage attribute, which defines the language elements you can use with the attribute. You can use any combination of values from the AttributeTargets enumeration, including All, Assembly, Class, Constructor, Delegate, Enum, Event, Field, Interface, Method, Module, Parameter, Property, ReturnValue, and Struct. The custom LegacyAttribute can be used on all language elements that support attributes.

 _
Public Class LegacyAttribute

The next step is to put the custom attribute to use. The following code shows the contents of an extremely simple assembly that defines two empty classes, one with the custom attribute and one without:

 _
Public Class ClassWithAttribute
 ' (Code omitted.)
End Class
 
Public Class ClassWithoutAttribute
 ' (Code omitted.)
End Class

The following Console application searches for LegacyAttribute using reflection, and reports its findings to the user:

Public Module CustomAttributeTest
 
 Public Sub Main()
 Console.WriteLine("Reporting legacy code in SampleAssembly.dll")
 
 ' Get the assembly.
 Dim Asm As System.Reflection.Assembly
 Asm = System.Reflection.Assembly.LoadFrom("SampleAssembly.dll")
 
 ' Examine all types.
 Dim Types(), TypeInfo As Type
 Types = Asm.GetTypes()
 
 For Each TypeInfo In Types
 Dim Attributes() As Object
 ExamineAttributes(TypeInfo.GetCustomAttributes(False), _
 TypeInfo.Name)
 
 ' Search members as well.
 Dim Members(), MemberInfo As System.Reflection.MemberInfo
 Members = TypeInfo.GetMembers()
 For Each MemberInfo In Members
 ExamineAttributes(MemberInfo.GetCustomAttributes(False), _
 MemberInfo.Name)
 Next
 Next
 
 Console.ReadLine()
 End Sub
 
 ' Check the collection of custom attributes for a LegacyAttribute.
 Private Sub ExamineAttributes(ByVal attributes() As Object, _
 ByVal searchElement As String)
 Dim CustomAttribute As LegacyAttribute
 For Each CustomAttribute In attributes
 Console.WriteLine()
 Console.WriteLine("Found a legacy component in " & searchElement)
 Console.WriteLine("Previous Platform: " & _
 CustomAttribute.PreviousPlatform.ToString())
 Console.WriteLine("Migrated By: " & CustomAttribute.MigratedBy)
 Console.WriteLine("Migrated On: " & _
 CustomAttribute.MigratedDate.ToShortDateString())
 Next
 End Sub
 
End Module

The results are as follows:

Reporting legacy code in SampleAssembly.dll
 
Found a legacy component in ClassWithAttribute
Previous Platform: VBScript
Migrated By: Matthew
Migrated On: 12/01/2003


Identify the Caller of a Procedure

Problem

You want your class to determine some information about the calling code, probably for diagnostic purposes.

Solution

Use the System.Diagnostics.StackTrace class.

Discussion

You can't retrieve information about the caller of a procedure through reflection. Reflection can only act on metadata stored in the assembly, whereas the caller of a procedure is determined at runtime. However, .NET includes two useful diagnostic classes that fill this role: StackTrace and StackFrame.

The stack holds a record of every call that is open and not yet completed. As new calls are made, new methods are added to the top of the stack. For example, if method A calls method B, both method A and B will be on the stack (with method B occupying the top position because it is the most recent).

The StackTrace object holds a picture of the entire stack. Each method call on the stack is represented by an individual StackFrame object. You can retrieve a StackFrame by calling StackTrace.GetFrame and supplying the index number for the frame. The stack is numbered from bottom to top, with the StackFrame at position 0 representing the root method. Figure 9-4 shows the StackTrace in a sample case where method A calls method B.


Figure 9-4: A simple StackTrace.

The StackFrame class includes methods such as GetFileName and GetFileLineNumber, which can help you track down the source of the call. The StackFrame class is also a jumping-off point for a more detailed exploration using reflection. Namely, you can use the StackFrame.GetMethod method to retrieve a MethodBase object for the corresponding method, and then you can examine details such as the data type of the method, the data types of the method parameters, and so on.

If you create a StackTrace object using the default parameterless constructor, it will contain a picture of the current stack. You can also create a StackTrace object using an exception, in which case it will contain a picture of the stack at the time the exception was thrown. The Console application on the following page demonstrates both techniques.

Public Module StackFrameTest
 
 Public Sub Main()
 Try
 ' Launch the series of method calls that
 ' will ultimately end with an error.
 A()
 Catch Err As Exception
 ' Show the current stack.
 Dim TraceNow As New StackTrace()
 Console.WriteLine("Here are the methods currently on the stack:")
 DisplayStack(TraceNow)
 
 ' Show the stack at the time the error occurred.
 Dim TraceError As New StackTrace(Err, True)
 Console.WriteLine("Here are the methods that were on the " & _ 
 "stack when the error occurred:")
 DisplayStack(TraceError)
 End Try
 
 Console.ReadLine()
 End Sub
 
 Private Sub DisplayStack(ByVal stackTrace As StackTrace)
 Dim Frame As StackFrame
 Dim i As Integer
 For i = 0 To stackTrace.FrameCount - 1
 Frame = stackTrace.GetFrame(i)
 Console.Write((i + 1).ToString() & ": ")
 Console.Write(Frame.GetMethod().DeclaringType.Name & ".")
 Console.WriteLine(Frame.GetMethod().Name & "()")
 Console.Write(" in: " & Frame.GetFileName())
 Console.WriteLine(" at line: " & Frame.GetFileLineNumber())
 Next
 Console.WriteLine()
 End Sub
 
 Private Sub A()
 B()
 End Sub
 
 Private Sub B()
 C()
 End Sub
 
 Private Sub C()
 D()
 End Sub
 
 Private Sub D()
 Throw New Exception()
 End Sub
 
End Module

The output is as follows:

Here are the methods currently on the stack:
1: StackFrameTest.Main()
 in: at line: 0
 
Here are the methods that were on the stack when the error occurred:
1: StackFrameTest.D()
 in: C:VBCookbookChapter 09Recipe 9-10Module1.vb at line: 46
2: StackFrameTest.C()
 in: C:VBCookbookChapter 09Recipe 9-10Module1.vb at line: 42
3: StackFrameTest.B()
 in: C:VBCookbookChapter 09Recipe 9-10Module1.vb at line: 38
4: StackFrameTest.A()
 in: C:VBCookbookChapter 09Recipe 9-10Module1.vb at line: 34
5: StackFrameTest.Main()
 in: C:VBCookbookChapter 09Recipe 9-10Module1.vb at line: 7


Reflect on a WMI Class

Problem

You want to use reflection to retrieve information about a Windows Management Instrumentation (WMI) class.

Solution

Create a ManagementClass object for the WMI class, and then explore it using properties such as ManagementClass.Methods, ManagementClass.Properties, MethodData.InParameters, and MethodData.OutParameters.

Discussion

Windows Management Instrumentation is a core component of the Windows operating system that allows your code to retrieve a vast amount of system and hardware information using a query-like syntax. The basic unit of WMI is the WMI class, which is similar to a .NET class, exposing properties and methods. However, you can't use a WMI class directly from .NET code; instead, you access a WMI class by using the generic wrapper objects in the System.Management namespace, such as ManagementClass (which represents any WMI class) and MethodData (which represents the collection of data associated with a WMI method).

Because the WMI classes are not a part of the .NET Framework, you can't analyze them at runtime using .NET reflection. However, you can inspect the properties of the .NET WMI types (such as ManagementClass.Methods and ManagementClass.Properties) to retrieve similar information about the supported functionality in a WMI class. The .NET WMI types also allow you to check whether specific WMI functionality is available on the current computer (because some WMI methods are not available on all versions of Windows).

The following Console application displays the list of methods provided by the Win32_Printer WMI class (which is used to retrieve information about or interact with the currently installed printers). In order to use this code, you need to add a reference to the System.Management.dll assembly and import the System.Management namespace.

Public Module WMIReflectionTest
 
 Public Sub Main()
 Dim PrintClass As New ManagementClass("Win32_Printer")
 
 ' Find all the methods provided by this class.
 Dim Method As MethodData
 For Each Method In PrintClass.Methods
 
 ' Display basic method information.
 Console.WriteLine(New String("-"c, 79))
 Console.WriteLine("**** " & Method.Name & " ****")
 Console.WriteLine(New String("-"c, 79))
 Console.WriteLine("Origin: " & Method.Origin)
 
 ' Display the arguments required for this method.
 Dim InParams As ManagementBaseObject
 InParams = Method.InParameters
 Dim PropData As PropertyData
 If Not InParams Is Nothing Then
 For Each PropData In InParams.Properties
 Console.WriteLine()
 Console.WriteLine("InParam_Name: " & PropData.Name)
 Console.WriteLine("InParam_Type: " & _
 PropData.Type.ToString())
 Next PropData
 End If
 
 ' Display the output parameters (return value).
 Dim OutParams As ManagementBaseObject
 OutParams = Method.OutParameters
 If Not OutParams Is Nothing Then
 For Each PropData In OutParams.Properties
 Console.WriteLine()
 Console.WriteLine("OutParam_Name: " & PropData.Name)
 Console.WriteLine("OutParam_Type: " & _
 PropData.Type.ToString())
 Next PropData
 End If
 Console.WriteLine()
 Next
 
 Console.ReadLine()
 End Sub
 
End Module

Here's part of the output generated by this example:

------------------------------------------------------------------------------
**** Reset ****
------------------------------------------------------------------------------
Origin: CIM_LogicalDevice
 
OutParam_Name: ReturnValue
OutParam_Type: UInt32
 
------------------------------------------------------------------------------
**** Pause ****
------------------------------------------------------------------------------
Origin: Win32_Printer
 
OutParam_Name: ReturnValue
OutParam_Type: UInt32
 
------------------------------------------------------------------------------
**** Resume ****
------------------------------------------------------------------------------
Origin: Win32_Printer
 
OutParam_Name: ReturnValue
OutParam_Type: UInt32
. . .
  Note

You can find reference information about WMI classes online at http:// msdn.microsoft.com/library/en-us/wmisdk/wmi/wmi_start_ page.asp. You can also download a Visual Studio .NET component that allows you to browse WMI classes on the current computer via the Server Explorer window at http://msdn.microsoft.com/library/default.asp?url=/downloads/list/wmi.asp .


Compile Source Code Programmatically

Problem

You want to compile code from a string or a source file using a custom .NET program.

Solution

Use the Microsoft.VisualBasic.VBCodeProvider to create an ICodeCompiler object.

Discussion

The .NET Framework allows you to access the Microsoft Visual Basic, Visual C#, Visual J#, and JScript language compilers. To compile code using one of these engines, you call the CreateCompiler method of the appropriate code provider class. (In the case of Visual Basic .NET, this class is Microsoft.VisualBasic.VBCodeProvider.) The CreateCompiler method returns an ICodeCompiler object that allows you to create assemblies in memory or on disk.

Compiling code can be a painstaking task. You need to ensure that you supply all the required assemblies, include all the appropriate import statements, specify additional parameters that determine whether debug information will be generated, and so on. To test dynamic code compilation, you can use an application such as the one shown in Figure 9-5, which reads code from a text box, attempts to compile it into an executable file, and then launches it.


Figure 9-5: A program for dynamic assembly creation.

When the user clicks Compile, several steps happen. An ICodeCompiler object is created, a number of basic assembly references are added, and the code is compiled to an executable assembly. Then, provided that no errors are discovered, the application is launched, as shown in Figure 9-6.

click to expand
Figure 9-6: A dynamically generated assembly.

To run this code, you must import two namespaces: Microsoft.VisualBasic (where the code provider is defined), and System.CodeDom.Compiler. This code uses the CompileAssemblyFromSource method, which parses the code in a string. You could also use CompileAssemblyFromFile to compile the code found in any text file (such as a .vb file).

Private Sub cmdCompile_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles cmdCompile.Click
 
 ' Create the compiler.
 Dim VB As New VBCodeProvider()
 Dim Compiler As ICodeCompiler = VB.CreateCompiler()
 
 ' Define some parameters.
 ' In this case, we choose to save the assembly file to a file.
 Dim Param As New CompilerParameters()
 Param.GenerateExecutable = True
 Param.OutputAssembly = "TestApp.exe"
 Param.IncludeDebugInformation = False
 
 ' Add some common assembly references (based on the currently 
 ' running application).
 Dim Asm As System.Reflection.Assembly
 For Each Asm In AppDomain.CurrentDomain.GetAssemblies()
 Param.ReferencedAssemblies.Add(Asm.Location)
 Next
 
 ' Compile the code.
 Dim Results As CompilerResults
 Results = Compiler.CompileAssemblyFromSource(Param, txtCode.text)
 
 ' Check for errors.
 If Results.Errors.Count > 0 Then
 Dim Err As CompilerError
 Dim ErrorString As String
 For Each Err In Results.Errors
 ErrorString &= Err.ToString()
 Next
 MessageBox.Show(ErrorString)
 Else
 ' Launch the new application.
 Dim ProcessInfo As New ProcessStartInfo("TestApp.exe")
 Process.Start(ProcessInfo)
 End If
 
End Sub
  Note

It's also possible to dynamically create code using the types in the System.CodeDom namespace or emit IL instructions using the System.Reflection.Emit namespace. These types are fascinating, and they underlie some advanced features in .NET (such as regular expression compilation). However, they also require extremely lengthy code, are difficult to implement, and are of limited usefulness to most application developers.




Microsoft Visual Basic. Net Programmer's Cookbook
Microsoft Visual Basic .NET Programmers Cookbook (Pro-Developer)
ISBN: 073561931X
EAN: 2147483647
Year: 2003
Pages: 376

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net