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.
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.
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.
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.
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. |