Reflection Namespace The Reflection namespace contains classes that support constructive access and management of assemblies. The Assembly class refers to an assembly, which includes the application and its metadata. There are classes representing modules, files, types, fields, global variables , attributes, parameter information, and much more. Reflection is a big namespace that contains a lot of information. We will not be able to cover all of the capabilities of the Reflection namespace in this chapter, but it will act as a good starting point and provide you with a good foundation for further exploration of the Reflection capabilities. AssembliesAssemblies are represented in the CLR by the Assembly class. For practical purposes, an assembly is your application. In reality an assembly is your application and the metadata that describes the things that your application is made up of. Note Manifest information is information that makes an assembly self-describing . Refer to the later section "Manifest Resource Information." The Assembly class is a root class in the Reflection namespace that is a starting point for obtaining manifest information and identifying and obtaining references to objects in an assembly, like modules, files, methods , and entry points. When you have these objects, you can dynamically invoke behaviors defined by elements in an assembly. Assembly ClassThe Assembly class implements getter methods that represent the objects in a module. The System.Reflection.GetAssembly method is a shared method that returns a reference to the assembly object containing the specified argument class. Note To use the capabilities of the Reflection namespace, add an Imports System. Reflection statement in the module that will use reflection, or add the System. Reflection namespace to the project imports. By calling the Reflection.Assembly.GetAssembly method, passing the System.Type information to the method, you get a reference object to the assembly containing that object. For example, Reflection.Assembly.GetAssembly(Me.GetType) returns the assembly object containing the object represented by Me. If Me is a form's reference to self, a reference to the assembly containing that form will be returned. (An alternative to GetAssembly is GetCallingAssembly, which requires no parameters and returns a reference to the assembly containing the statement GetCallingAssembly.) From the Assembly object you can obtain references to the modules, files, manifest information, entry point, and types defined in a specific assembly. Assembly MembersTable 6.1 provides an overview of the members of the Assembly class. Table 6.1. Members of the Assembly Class
Refer to the sections that follow for individual code examples demonstrating some of the capabilities of the Assembly class. Manifest Resource InformationManifests contain data that describes how the elements of an assembly relate to each other. Manifests contain assembly name, culture, strong name, files in the assembly, type reference, and information indicating referenced assemblies. The assembly name, version number, culture information, and string information describe the assembly's identity, allowing assemblies with the same filename to coexist in the Global Assembly Cache (GAC). Note Culture information includes such things as language and country to support multinational application development. You can change manifest information using attributes (see Chapter 12), including trademark, copyright, product, company, and version information. The ManifestResourceInfo defines FileName, ResourceLocation, and ReferencedAssembly properties. The FileName property defines the resource containing the manifest information. ResourceLocation defines the location of the manifest resource file, and ReferencedAssembly returns a reference to the assembly containing this manifest. Module ObjectAssembly.GetModule returns a named module in an assembly. Module in this context does not refer to the Module construct. An assembly's module is equivalent to an application, like an executable or DLL module. Tip You can use keywords in code if you wrap the use of those keywords in square brackets. For example, Assembly is a reserved word. To declare a type as an Assembly type, you can prefix the Reflection namespace to the type or declare the type using brackets as follows : Dim A As [Assembly]. IntelliSense will automatically place the Assembly keyword in brackets when it is selected from a member list. When you obtain a reference to a Module object, you can request information about Type objects, like classes and structures, and information contained in those types. When we are referring to an assembly's module and reflection, we are referring to an application. When you have a specific application (module), you can obtain information about types, like the forms and classes in the module, and from those objects you can derive information about methods, fields, properties, events, and parameters. Listing 6.1 contains code that walks the methods and fields of a single type in a module. Listing 6.1 Walking the fields and methods of a class in a module1: Private Function GetAssembly() As Reflection.Assembly 2: 'Return Reflection.Assembly.GetAssembly(Me.GetType()) 3: Return Reflection.Assembly.GetCallingAssembly 4: End Function 5: 6: Private Sub InsertText(ByVal Text As String) 7: TextBox1.Text &= Text & vbCrLf 8: End Sub 9: 10: Private Sub InsertModuleElements(ByVal AType As Type) 11: Dim Enumerator As IEnumerator = _ 12: AType.GetMethods.GetEnumerator 13: 14: InsertText("Methods:") 15: While (Enumerator.MoveNext) 16: InsertText(CType(Enumerator.Current, MethodInfo).Name) 17: End While 18: 19: InsertText("Fields:") 20: Enumerator = AType.GetFields.GetEnumerator() 21: While (Enumerator.MoveNext) 22: InsertText(CType(Enumerator.Current, FieldInfo).Name) 23: End While 24: 25: End Sub 26: 27: 28: Private Sub WalkModule() 29: InsertText(GetAssembly.GetModules()(0).Name) 30: InsertText(GetAssembly.GetTypes()(0).Name) 31: InsertModuleElements(GetAssembly.GetTypes()(0)) 32: End Sub 33: 34: Private Sub MenuItem10_Click(ByVal sender As System.Object, _ 35: ByVal e As System.EventArgs) Handles MenuItem10.Click 36: WalkModule() 37: End Sub Note To run the sample code, open ReflectionDemo.sln and choose Demo, Assembly, GetModule. To run the code separately, create a new Windows application, include Imports System.Reflection, add a multiline TextBox, and define a new way to invoke WalkModule. GetAssembly, defined on lines 1 through 4, is a query method that simplifies referring to an Assembly object. WalkModule uses GetAssembly to return the assembly containing the code in the listing. The Assembly object is used to get the array of Modules on line 29 and the array of Types on line 30. The name from the 0th element of the modules is displayed and the name of the 0th element of the Types array is displayed. InsertModuleElements on line 31 uses the Type at index 0 as the target type to walk. The sample code retrieves type Form1, which is a class, and passes that type to the InsertModuleElements procedure. The sample code gets Form1's methods and fields arrays on lines 1112 and 20 and uses the enumerator implemented by those arrays to walk each method and field defined in Form1. You have seen verbose code that uses arrays and enumerators in earlier chapters, but I will repeat it here for clarity by elaborating on the code that examines each method. (The preceding code example is closer to the code we might like to deploy, but may be a little ambiguous.) From Listing 6.1 we have the code on lines 11 through 17 that displays all of the methods in the type represented by AType. 11: Dim Enumerator As IEnumerator = _ 12: AType.GetMethods.GetEnumerator 13: 14: InsertText("Methods:") 15: While (Enumerator.MoveNext) 16: InsertText(CType(Enumerator.Current, MethodInfo).Name) 17: End While A verbose form of the same code follows. Dim MethodInfos() As MethodInfo MethodInfos = AType.GetMethods Dim Enumerator As IEnumerator Enumerator = MethodInfos.GetEnumerator() TextBox1.Text &= "Methods:" & vbCrLf Dim AMethodInfo As MethodInfo While (Enumerator.MoveNext) AMethodInfo = CType(Enumerator.Current, MethodInfo) TextBox1.Text &= AMethodInfo.Name & vbCrLf End While The revised verbose code performs singular operations on each line of code. Note, however, that it requires almost twice as many lines of code. The first revised line declares an array of MethodInfo types. The second line initializes that array by calling Type.GetMethods. The third and fourth lines declare and initialize an IEnumerator (introduced in Chapter 2 and explored further in Chapter 3). IEnumerator is an interface that defines two methods, MoveNext and Reset, and a property Current, providing you with a consistent interface for iterating elements of a System.Array. The fifth line adds some text to a TextBox indicating that the text that follows consists of method names. The sixth line declares a temporary variable used to store each MethodInfo object, and the loop appends the name of each methodAMethodInfo.Nameto the TextBox control. Production systems should have concise rather than verbose code. A method named DisplayMethodNames that walks through the MethodInfo objects of a Type without creating several temporaries would yield clear and concise code. File ObjectThe Assembly class contains GetFile and GetFiles methods. These methods return a FileStream object and an array of FileStream objects, respectively. The FileStream objects returned by the Assembly class are no different than any other FileStream object derived in any other manner; the methods and attributes are identical, only the state is different. Listing 6.2 demonstrates how to load the bytes representing a file. Assembly.GetFile returns a FileStream representing the bytes of a module in an assembly. Listing 6.2 Displaying some of the bytes in a module file as hexadecimal bytes using the ThreadPool and the FileStream returned by GetFiles()(0)1: Private Contents As String 2: 3: Private Sub WriteFileAsHex(ByVal state As Object) 4: 5: Dim Stream As IO.FileStream = _ 6: GetAssembly.GetFiles()(0) 7: 8: Dim B(Stream.Length) As Byte 9: Stream.Read(B, 0, Stream.Length) 10: 11: Dim I As Integer 12: 13: SyncLock GetType(Form1) 14: For I = 0 To B.Length() - 1 15: If (I Mod 8 = 0 And I > 0) Then Contents &= " " 16: If (I Mod 16 = 0 And I > 0) Then Contents &= vbCrLf 17: Contents &= Hex(B(I)).PadLeft(2, "0") 18: 19: ' Remove if you want the whole file! 20: If (I > 50000) Then Exit For 21: Next 22: End SyncLock 23: 24: Invoke(CType(AddressOf UpdateText, MethodInvoker)) 25: 26: End Sub 27: 28: Private Sub UpdateText() 29: TextBox1.Text = Contents 30: StatusBar1.Text = "Loaded" 31: End Sub 32: 33: Private Sub MenuItem11_Click(ByVal sender As System.Object, _ 34: ByVal e As System.EventArgs) Handles MenuItem11.Click 35: 36: StatusBar1.Text = "Loading File..." 37: TextBox1.Clear() 38: 39: Threading.ThreadPool.QueueUserWorkItem(_ 40: AddressOf WriteFileAsHex) 41: End Sub Note To avoid repeating a complete Windows application listing, the code in Listing 6.2 is an excerpt from ReflectionDemo.sln. To make the code a standalone example, create a new Windows application, add the Imports System. Reflection statement, copy GetAssembly from Listing 6.1, and replace the Windows Forms controls and events with the controls and events in your application. When the user chooses Demo, Assembly, GetFile in ReflectionDemo.sln, the Click event handler on line 33 is invoked. The StatusBar.Text is updated, the TextBox is cleared, and something happens with the ThreadPool. We will come back to line 39 in a minute. Let's take a look at WriteFileAsHex. Regardless of how WriteFileAsHex is invoked, what WriteFileAsHex does is request the FileStream represented by the Reflection.File object returned by indexing the 0th element of the array returned by Assembly.GetFiles. All of the bytes in the FileStream are read into the array of bytes B. Lines 14 through 21 convert all of the bytes to hexadecimal, formatting the bytes in two columns of eight bytes each. Let's return to line 29 now. What is Threading.ThreadPool.QueueUserWorkItem? As you know, Visual Basic .NET supports free threading. There is a pool manager called the ThreadPool that contains some threads waiting around for you to give them some work to do. The ThreadPool manages the creation and allocation of threads; all you have to do is give them work by queuing a work item. Work items are represented by callback functions, also known as delegates. There are several new topics here, including delegates, threads, thread pools, and how to safely use threads with controls. For now, you will have to take on faith that the code works correctly and safely. In summary, the ThreadPool is a pool of available threads, a delegate is a class representing a pointer to a function, and Windows Forms controls must be manipulated on the same thread as the one that owns them. The last point explains why the code uses the Invoke method on line 24 to update the TextBox. Chapters 8 and 9 provide more information on delegates, and using the ThreadPool safely with Windows Forms controls is described in Chapter 15's section titled "Using the ThreadPool." The motivation for using threads in the example is to give you a first look at an advanced subject, multithreading, and to offload a potentially process-heavy task to a background thread, allowing the graphical user interface to remain responsive. Pushing big processes onto a different thread to allow our GUI to remain responsive reflects an actual reason we might want to use threads. More information about multithreaded applications can be found in Chapter 14, "Multithreaded Applications." Location PropertyThe Assembly.Location object returns the complete path information of the assembly containing the manifest for the assembly represented by the Assembly object. EntryPoint PropertyAssembly.EntryPoint returns the MethodInfo object representing the method that is the entry point for an assembly. For example, a console application would return the shared sub main procedure where your console application begins executing. (Refer to the section on MethodInfo for more information on the type returned by the EntryPoint method.) GlobalAssemblyCache PropertyThe Assembly.GlobalAssemblyCache property returns a Boolean value. If the property returns True, the assembly is registered in the GAC and is a public assembly; otherwise , the assembly is a private assembly. TypeThe System.Type object is an object that describes a type. When you invoke GetType or index one of the types in the array of types returned by GetTypes, you have an object that describes the interfaces, classes, structures, modules, and enumerations in an assembly. GetTypes returns nested types, too, like nested classes or structures. The System.Type object is critical to obtaining and managing objects using Runtime Type Information (RTTI) and consequently central to reflection. Type objects allow you to query the object to determine whether the type represents a class or COM object, whether the type is nested, and the access modifiers that were applied to the type. Reflection is used implicitly during certain types of operations. In VB6 you could declare a variable as a variant, assign a specific object to that variant, and call methods of the specific object without performing any type conversions. (Refer to the following example.) 'VB6 Example Dim V As Variant Set V = New Collection V.Add "Foo" This type of code implicitly performs a call to QueryInterface. If the actual objectin this instance a Collectiondoes not support the interface, VB6 raises a runtime error. The preceding operation demonstrates late binding, or runtime binding. Late binding is supported in VB .NET by employing reflection. Unfortunately, this kind of lazy code defers a possible error until runtime. We want errors to manifest themselves as early as possible. For this reason, Chapter 2 suggested that Option Strict On be used to prevent late binding. Refer to the help information for a complete reference of the Type members. BinderBinder objects are responsible for performing type conversions between arguments presented and the actual argument type needed. Individual programming languages load and bind methods in a module individually. You can load assemblies and invoke methods on those assemblies using late binding through reflection. This is referred to as custom binding. The most common scenario where this might be employed is by tools that allow users to explicitly invoke operations. Because the developer of such an application would not know in advance what a user may want to do, operations could not be associated with a visual metaphor in advance, such as a button or menu. For example, a user might pick from a selection of a thousand operations residing in any number of assemblies. Instead of trying to write static code for dynamic behavior, reflection and custom binding would allow you to load an assembly and invoke behaviors on that assembly as needed. The subsections that follow demonstrate some of the ways in which assemblies can be loaded dynamically using reflection. Loading AssembliesAssemblies can be loaded at runtime without an application knowing what that assembly is in advance. Loading assemblies dynamically is probably not something you will do in the run-of-the-mill application, but just recently a developer from a local software company called me and asked me how to do this identical operation in a Delphi application. My response was "Oh, you're talking about reflection!" Unfortunately for that developer, the mechanics are not as thoroughly supported in Delphi as they are in the CLR. The call reminded me that dynamic type determination can be useful, especially for general tools builders. I am not sure, but it is a reasonable bet that reflection was designed for vendors building tools, and perhaps for Visual Studio .NET as a tool, too. An assembly is an application containing metadata, which makes the assembly more than an application. Assemblies are self-contained files that look like applications on the outside but contain additional information like manifests and string name, public key data. To load an assembly on the fly, you will need the name of an application containing assembly manifest information. From that application you can create an instance of the assembly returned by the Shared Assembly.Load function. And, of course, as we have been talking about, after you have loaded the assembly you can find out information about that assembly and invoke methods on types defined in the assembly. The end result is completely dynamic interaction with assemblies, or, applications, whichever you prefer. The following fragment demonstrates loading an assembly: Dim AName As String = _ "C:\ Temp\ ReflectionEventDemo.exe"" Dim App As [Assembly] = [Assembly].LoadFrom(AName) The assembly MyLibrary.dll is a Class Library project. The example demonstrates a hard-coded path, but you could just as easily return a dynamic path from the OpenFileDialog or a list of assemblies. The first statement declares a constant representing the filename, AName. The application name is used to initialize the assembly via the Assembly.LoadFrom method. After this code fragment is executed, you have roughly the equivalent of LoadLibrary from the Win32 API days, or a late-bound COM object. (Remember this is a rough equivalence to LoadLibrary or late-bound COM objects.) Assembly is a keyword and a class; wrapping the word Assembly in brackets indicates that we are using the class name rather than the keyword associated with assembly attributes (see Chapter 12). Now that we have a loaded assembly, we can use reflection and the Type.InvokeMethod operation to dynamically invoke behaviors on that assembly. Invoking Members on Dynamic AssembliesEarlier in the chapter, I mentioned that System.Type was essential to RTTI and reflection. One of those essential features is the ability to use a type rather than an instance to invoke members defined by a type. Continuing with the example from the preceding section, we now have an assembly. We can use techniques from early sections of this chapter to discover what types are defined by that assembly and invoke behaviors defined by those types. For our example, we will use known information to keep the example simple (see Listing 6.3). Listing 6.3 The listing for MyLibrary.dll and CustomBinding.vproj, both in CustomBinding.sln1: ' MyLibrary.dll - Class1.vb 2: Public Class Class1 3: 4: Public Sub Echo(ByVal Message As String) 5: MsgBox(Message) 6: End Sub 7: 8: End Class 9: 10: 11: ' CustomBinding.exe - Form1.vb 12: 13: Imports System.Reflection 14: 15: Public Class Form1 16: Inherits System.Windows.Forms.Form 17: 18: [ Windows Form Designer generated code ] 19: 20: Private Sub LoadAndInvoke() 21: ' Modify to point to location of DLL on your PC 22: Const FileName As String = "C:\ temp\ MyLibrary.dll" 23: 24: 25: Dim AName As String = FileName 26: 27: 28: Dim MyAssembly As [Assembly] = [Assembly].LoadFrom(AName) 29: 30: Dim Arguments() As Object = {"Custom Binding Demo"} 31: 32: Dim AType As Type = MyAssembly.GetTypes()(0) 33: Dim O As Object = MyAssembly.CreateInstance(AType.FullName) 34: 35: AType.InvokeMember("Echo", BindingFlags.Default Or _ 36: BindingFlags.InvokeMethod, _ 37: Nothing, O, Arguments) 38: End Sub 39: 40: 41: Private Sub Button1_Click(ByVal sender As System.Object, _ 42: ByVal e As System.EventArgs) Handles Button1.Click 43: LoadAndInvoke() 44: End Sub 45: End Class The first section of Listing 6.3 implements the class library MyLibrary.dll as a class library that contains a single class with a single method, Echo. Beginning on line 10, the second section of the listing contains a simple application with a single form, Form1. Form1 contains a button. When the button is clicked (see lines 41 to 44), Form1.LoadAndInvoke is called. (Keep in mind that you could easily pick assemblies using dialogs or manually enter them by selecting an assembly from the GAC.) LoadAndInvoke declares the path to the application containing the assembly manifest information and uses that object to LoadFrom the assembly with which the application is associated. (This is the code we covered in the prior section.) For the sake of brevity, line 30 picks up by creating an array of objects. The array of objects plays the role of arguments passed to the dynamically invoked method. On line 32 we arbitrarily retrieve the first type defined in the assembly. We know that MyLibrary.dll only defines one type, Class1. We could just as easily have displayed a list of types and let the user pick one. Line 33 uses type information contained in the type object to create an instance of the type with Assembly.CreateInstance. CreateInstance uses the default constructor that every type possesses to create an uninitialized instance of the type. Finally, Type.InvokeMethod passes a string naming the method we want to call, Echo. The second argument indicates the BindingFlags; we want to invoke a method, so we pass the BindingFlags.Default or'd with BindingFlags.InvokeMethod, indicating the kind of invocation we are making. The third parameter is Binder; we will come back to the Binder parameter in a minute. The fourth parameter is the object that will be used to invoke the method, and the final parameter, Arguments, is the array of arguments to pass to the method. From the listing we know Echo takes a single string argument, which is what Arguments was initialized with. The net result is that we invoked a method in an assembly that the user could have supplied, with a name that the user could have picked from a list of available names, and passed methods that we could have expressed to the user by displaying the ParameterInfo names for any particular method. As a result we have completely dynamic code. Where Does Binder Fit In?Binder is an abstract class that we must inherit from. Binder is defined with the MustInherit modifier. MustInherit means that we must declare an instance of a class that inherits from Binder and implement all of the methods labeled MustOverride. What Binder is designed to do is describe a transposition between actual argument types to formal argument types based on the type of member invoked, binding arguments, and specific parameters. Chapter 7, "Creating Classes," covers inheritance in detail. Listing 6.4 implements a do-nothing binder subclass demonstrating the syntactical mechanics of inheriting from an abstract class and implementing virtual methods; that is, methods modified with MustOverride. Listing 6.4 A do-nothing binder demonstrating the syntactical mechanics of inheriting an implementing MustOverride Binder method1: Imports System.Globalization 2: 3: Public Class MyBinder 4: Inherits Binder 5: 6: Public Overrides Function ChangeType(_ 7: ByVal value As Object, _ 8: ByVal type As Type, _ 9: ByVal culture As CultureInfo _ 10: ) As Object 11: 12: Return value 13: 14: End Function 15: 16: Public Overrides Function SelectProperty(_ 17: ByVal bindingAttr As BindingFlags, _ 18: ByVal match() As PropertyInfo, _ 19: ByVal returnType As Type, _ 20: ByVal indexes() As Type, _ 21: ByVal modifiers() As ParameterModifier _ 22: ) As PropertyInfo 23: 24: End Function 25: 26: Public Overrides Function BindToMethod(_ 27: ByVal bindingAttr As BindingFlags, _ 28: ByVal match() As MethodBase, _ 29: ByRef args() As Object, _ 30: ByVal modifiers() As ParameterModifier, _ 31: ByVal culture As CultureInfo, _ 32: ByVal names() As String, _ 33: ByRef state As Object _ 34: ) As MethodBase 35: 36: End Function 37: 38: Public Overrides Function BindToField(_ 39: ByVal bindingAttr As BindingFlags, _ 40: ByVal match() As FieldInfo, _ 41: ByVal value As Object, _ 42: ByVal culture As CultureInfo _ 43: ) As FieldInfo 44: 45: End Function 46: 47: 48: Public Overrides Sub ReorderArgumentArray(_ 49: ByRef args() As Object, _ 50: ByVal state As Object _ 51: ) 52: 53: End Sub 54: 55: Public Overrides Function SelectMethod(_ 56: ByVal bindingAttr As BindingFlags, _ 57: ByVal match() As MethodBase, _ 58: ByVal types() As Type, _ 59: ByVal modifiers() As ParameterModifier _ 60: ) As MethodBase 61: 62: End Function 63: 64: End Class As you can glean from the listing, the Binder methods use a relatively high number of arguments and many of those are array arguments. (System.Globalization contains the CultureInfo class used in the parameter for ChangeType.) ChangeTypeChangeType is used to implement changing the type represented by the value parameter to the type argument. SelectPropertySelectProperty returns a property from the available properties based on the selection criteria arguments. BindToMethodBindToMethod picks a specific method based on the argument information provided. BindToFieldBindToField selects one of the available fields based on the argument values specified. ReorderArgumentArrayReorderArgumentArray allows you to reorganize the array of arguments passed to InvokeMethod (see Listing 6.4). SelectMethodSelectMethod provides the Binder with a chance to select a method based on the arguments passed. When you invoke a method using historical technology, keep in mind that method invocation and parameter passing are constrained by the name, types, and order prescribed by the language that the application was implemented in. The .NET Framework replaces problems caused by language implementations and calling orders with classes in the Reflection namespace that allow the user to describe how method invocations are bound. Listing 6.3 passes Nothing for the Binder object because we are working with languages that are core languages implemented by Microsoft. Custom binding objects will probably come into play when third-party vendors begin implementing tools in other languages that will be integrated into Visual Studio .NET. Activator ClassThe Activator class is defined in the System namespace. Activator contains methods for creating object instances locally or remotely or obtaining instances of existing objects. When you call Type.CreateInstancesee Listing 6.3, line 33an instance of an Activator is created by the Type class to create the instance. MethodInfo ClassMethodInfo objects are instances of classes that represent a method of a type. MethodInfo allows you to discover return types, modifiers, and attributes applied to the method, and a list of parameters used by the method. As demonstrated throughout this chapter, discovery of method names and arguments allows the dynamic invocation of methods. InsertModuleElements from Listing 6.1 demonstrates dynamic discovery of method names. Combine dynamic name discovery with dynamic parameter discovery, and you can invoke methods through the Type.InvokeMember method. ParameterInfo ClassParameterInfo objects represent the arguments associated with methods. ParameterInfo has properties that enable you to determine the name and type of the parameter, whether the parameter is optional, whether it has default values, and whether it is an input or output parameter. FieldInfo ClassFieldInfo objects represent fields defined as members of a type. FieldInfo objects allow you to discover attributes, modifiers, names, and types at runtime. PropertyInfo ClassBy convention, fields contain private data represented by public property methods. It is intuitive that reflection supports all aspects of dynamic discovery and invocation, including property information. EventInfo ClassThe concept of the function pointer is new to Visual Basic .NET. In Chapters 8 and 9 you will learn that function pointers are types that point to the address of a function and those functions can be invoked via their addresses. Additionally, function pointers in .NET are wrapped in classes called Delegates. Reflection supports the dynamic discovery of types, methods, fields, and events. This illustrates just how much Visual Basic has changed under .NET. In addition to having support for inheritance (refer to Chapter 7) and function pointers, the CLR supports the dynamic discovery of those function pointers, and subsequently, the dynamic assignment of event handlers to delegates. Listing 6.5 demonstrates an application that loads a second executable assembly, creates an instance of the main form in that assembly, and assigns an event handler in the first application to an event in the second application. Listing 6.5 Dynamic event handling using reflection1: Private Sub AssignEvent() 2: 3: Dim AName As String = "C:\ TEMP\ ReflectionEventDemo.exe" 4: 5: Dim App As [Assembly] = [Assembly].LoadFrom(AName) 6: Dim AType As Type = App.GetTypes()(0) 7: 8: Dim AForm As Form = App.CreateInstance(AType.FullName) 9: AForm.Show() 10: 11: Dim Events() As EventInfo = AType.GetEvents 12: Dim D As EventHandler = AddressOf Handler 13: 14: Dim I As Integer 15: For I = 0 To Events.Length - 1 16: If (Events(I).Name = "Click") Then 17: Events(I).AddEventHandler(AForm, D) 18: Exit For 19: End If 20: Next 21: 22: End Sub 23: 24: Private Sub Handler(ByVal sender As System.Object, _ 25: ByVal e As System.EventArgs) 26: MsgBox("Called from " & Application.ProductName) 27: End Sub Caution Listing 6.5 demonstrates dynamic event handler assignment across assembly boundaries using reflection. This is not the recommended way to assign event handlers. Read Chapters 8 and 9 for normal event-handling operations. Listing 6.5 creates an assembly object containing a simple Windows application. Line 8 creates the assembly with the Assembly.Load method. Line 9 gets the first type in the assembly. We know there is only one, the main form; in a practical example we would want to perform some sanity checking on the Type returned as AType on line 9. Lines 14 through 23 get all of the events defined for the Form type, searching for the Click event. When the Click event is found, a handler defined in the first assembly is assigned to a delegate in the second assembly. Note To test the dynamic event assignment using reflection, you will need to choose Demo, Dynamic Event Reflection in the ReflectionDemo.exe to assign the event handler and click on the main form in the second application, ReflectionEventDemo.exe. When you compile and run both of the assemblies in ReflectionDemo.sln, you will see that clicking on the second assemblyafter the event handler is assignedruns code in the first assembly. You can verify this operation by placing a breakpoint on line 29 from the listing. |
Team-Fly |
Top |