LoadFrom(assemblyFile As String) As Assembly Both of these methods return an instance of the Assembly class. Assembly.Load takes assemblyString as a parameter. This parameter represents the name of a loaded (in the current application's domain) assembly. For example, if you create a form-based application and then list all loaded assemblies, one of them will be "System.Drawing" which is the name of an assembly (and namespace) used by the .NET forms engine. Therefore to get an instance of this loaded assembly, you would write the following code (in VB):
Dim myAssembly As [Assembly]
myAssembly = [Assembly].Load("System.Drawing")
You can also attach to your own loaded assembly. That is, your code can read reflected information about itself. To do so, you would simply use the same Load method above but replace "System.Drawing" with the name of your assembly.
Assembly.LoadFrom works in a similar fashion. However, this method takes a string parameter that points to a .NET assembly via its path and file name. LoadForm gives you the power to work with any .NET assembly and not just those loaded into the current application domain. The following is an example:
Dim myAssembly As [Assembly]
Dim myPath As String
myPath = "C:/Basics/Bin/Basics.exe"
myAssembly = [Assembly].LoadFrom(myPath)
Visual Basic developers can also use the keyword GetType to load an assembly. This operator takes a type as a parameter and returns a given type, which in turn provides access to its assembly data. The following is an example of GetType:
myAssembly = GetType( _
System.Data.DataRow).Assembly
Of course, the .NET Framework also provides a GetType method that is available in all objects written for .NET. You can use this method as well to return a type's assembly. For example:
Dim test As New MyTestClass()
myAssembly = test.GetType().Assembly
Now that you've seen numerous ways to create an instance of Reflection.Assembly, let's look at how to dig into an assembly and query the information it contains. To do so, you'll make extensive use of the System.Type class, which is used to represent all .NET types (classes, enumerations, arrays, etc.). You will use an Assembly instance to return Type instances that represent all the types in a given assembly. For example, suppose you have the following console application:
Imports System.Reflection
Module Basics
Public Enum testEnum
testValue = 1
End Enum
Sub Main()
'do something
End Sub
End Module
There are two types defined here at the assembly level: the module Basics and the enumeration testEnum. To access these types you call Assembly.GetTypes. This will return an array of Type objects that represent the loaded assembly's types. For example, add the following code to the Sub Main of the previous code snippet:
Dim myAssembly As [Assembly]
myAssembly = [Assembly].Load("Basics")
Dim myType As Type
For Each myType In myAssembly.GetTypes
' do something
Next
Console.ReadLine()
You've obtained a reference to the types from the assembly with myAssembly.GetTypes and are now able to iterate through those types. You'll use these type objects to make decisions in your code. For example, you could add the following check inside the For Each...Next loop:
If myType.IsClass Then
Console.WriteLine(myType.Name)
End If
You've now successfully loaded an assembly, iterated its types, and displayed all of its classes. This represents the basic pattern you follow to work with reflected metadata on an assembly. The rest of the article will dive deeper and show you what more to do with the various Reflection classes.
The Namespaces and Classes
There are two namespaces that contain the Reflection classes, System.Reflection and System.Reflection.Emit. The Reflection namespace contains objects related to type discovery and execution while the Reflection.Emit namespace dynamically generates code at runtime (this is the focus of part 2 of this article). Table 1 provides a quick and easy reference to the key classes you'll use when working with reflection.
Reflection Discovery: Searching and Filtering
Earlier in this article you learned how to load an assembly and gain access to its types. In fact, for any given .NET assembly you can return all of its types using Assembly.GetTypes. This method returns all of the global or public types (depending on your .NET security model or context) stored in a given assembly. For example, the following code creates an Assembly instance based on itself and iterates through the assembly's types:
Imports System.Reflection
Module Basics
Sub Main()
Dim mi As MethodInfo
Dim myAssembly As [Assembly]
myAssembly = GetType(Basics).Assembly
Dim t As Type
For Each t In myAssembly.GetTypes()
Console.WriteLine("Type:=" & t.Name)
Next
Console.ReadLine()
End Sub
End Module
This code is useful for looping through all of the public types exposed on a given assembly, but what if you have a large assembly or are simply after specific types such as a constructor or a property? If you've spent any time at all looking at the .NET Framework Class Library, you know that one Assembly can contain a large number of types. You may not need to know every type; you might prefer to filter or search for a very specific type or types. Fortunately, the System.Type class exposes a number of methods for accessing, filtering, and searching for specific types inside a given assembly.
Direct Access
Direct access to a given type implies you've made a decision and are now looking for an exact type. Perhaps your application queries the user for the type they are interested in, or perhaps you knew from the start the specific type to look for. In any case, the System.Type class provides a number of methods that provide direct access to specific types. Methods like GetConstructor, GetMethod, GetProperty, and GetEvent allow you to target their specific types. For example, suppose you have the following class:
Public Class SomeClass
Public Sub New()
End Sub
Public Sub New(ByVal someValue As Int32)
End Sub
Public Sub New(ByVal someValue As Int32, _
ByVal someOtherValue As Int32)
End Sub
Public Sub SomeMethod()
End Sub
End Class
Here is a class with three empty constructors and an empty method. Now assume that you want to access the constructor that takes one integer value as its only parameter. To do so you would use the GetConstructor method of the Type class. This method allows you to pass an array of objects that represent parameters for a given constructor. When executed, the method searches a type for any constructor that matches the signature defined by the array of parameters. GetConstructor then returns a ConstructorInfo object for your use (perhaps you want to invoke the constructor). For example, you would first create the array of parameters as follows:
Dim ts() As Type = {GetType(Int32)}
Finally, you would then call GetConstructor of your Type object as follows:
Dim ci As ConstructorInfo = _
GetType(SomeClass).GetConstructor(ts)
Similarly, Type.GetMethod provides you with direct access to the methods on a given object.
This method returns a MethodInfo instance for your use. It simply takes the name of the given method as a parameter. For example:
Dim mi As MethodInfo = _
GetType(SomeClass).GetMethod("SomeMethod")
You might wonder, "What if I have two methods with the same name but different signatures in the same class?" Well, in the previous case you would get an ambiguous matching error. However, there are a number of versions of all of these direct access methods that allow you to pinpoint specific types. For instance the GetConstructor method can filter methods based on an array of parameters; or you could filter based on calling conventions, return type, etc. You can apply this same pattern to directly access a specific property or an event contained by your type.
Filtering
The System.Type class also provides a number of methods for returning a filtered set of types contained inside a class or another type. The methods GetConstructors, GetMethods, GetProperties, and GetEvents allow you to either return all of the given types as an array or supply filter criteria to only return a specific set of types.
A typical filter involves setting binding flags. Binding flags represent search criteria. You use values of the BindingFlags enumeration to represent things such as public or non-public types. You can also indicate flags or static members. You can even combine flags to further narrow your search. Consider the following basic class:
Public Class SomeClass
Public Sub SomeMethod()
End Sub
Public Shared Sub SomeSharedMethod()
End Sub
Public Shared Sub SomeOtherSharedMethod()
End Sub
End Class
You can see that there are three public methods, two of which are static (or shared). Suppose that through reflection you want to find all public static methods for the SomeClass Type. You would call GetMethods and pass BindingFlag enumeration values such as the bindingAttr parameter. The following code retrieves an array of all methods in SomeClass that are both public and static:
Dim mi As MethodInfo() = _
GetType(SomeClass).GetMethods( _
BindingFlags.Public Or BindingFlags.Static)
You can use this same technique to return private types (provided you have the correct permission). To do so you would use the BindingFlag enumeration value BindingFlags.NonPublic. This can be combined with BindingFlags.Instance to return all instance methods that are private. This same pattern can be applied to return constructors, properties, and events.
Searching
As you may have guessed, searching is a very similar process to filtering. The only real difference is that searching is done via a more abstract method of System.Type: FindMembers. Rather than call a specific filter such as GetEvents you might use FindMembers and pass the value MemberTypes.Events as the memberType parameter. If you need a custom filter that doesn't involve a specific type, you can use the parameters of FindMembers to satisfy a number of different searching requirements. Here's an example.
Notice that the following Visual Basic class defines three fields: two private and one public.
Public Class SomeClass
Private myPrvField1 As Int32 = 15
Private myPrvField2 As String = _
"Some private field"
Public myPubField1 As Decimal = 1.03
End Class
Suppose that you need to use the FindMembers method of the Type class to return the private fields on an instance of SomeClass and display their values. You would do so by indicating the value Field of the enumeration MemberTypes for the memberTypes parameter of FindMembers. You then can indicate your BindingFlags, and FindMembers will return an array of MemberInfo objects that match your search criteria. The following code snippet provides an example:
Dim memInfo As MemberInfo()
Dim fi As FieldInfo
Dim sc As New SomeClass()
memInfo = sc.GetType.FindMembers( _
MemberTypes.Field, BindingFlags.NonPublic Or
BindingFlags.Instance, Nothing, Nothing)
Dim i As Int16
For i = 0 To memInfo.GetUpperBound(0)
fi = CType(memInfo(i), FieldInfo)
Console.WriteLine(fi.GetValue(sc))
Next
Once you find the target members, the code converts them into actual FieldInfo objects and is able to query them for their values.
Custom Searching
Even with all the aforementioned type searching capabilities you may find that you need to define a custom type search. Custom searching involves the method from the previous example, FindMembers. You may have noticed that in the previous example we set two of FindMembers' parameters (filter and filterCriteria) to Nothing. The filter parameter takes an instance of the MemberFilter delegate. Defining a delegate allows you to define custom search logic. This delegate simply receives a MemberInfo object that represents a member that meets all the other search criteria.
The filterCriteria parameter can be any .NET object. You can use this parameter to define your custom search criteria. This can be as simple as a string or as involved as a custom object. Here is an example.
Image you have a class called SomeClass and that it defines three properties: Name, Id, and Type. Now suppose that you need a filter that searches classes to find only the properties Name and Id. We've provided a very simple example and of course you could return all properties, loop through them, and filter out those not named Name or Id. Take a look at the console application in Listing 1.
This application defines the delegate, MySearchDelegate, to customize the search. It creates the custom object, filterObject, with two fields that help define our search criteria. The application then calls FindMembers and indicates that you want all Property types. When a Property type is found the application raises the MySearchDelegate and passes the filterCriteria instance. The delegate then simply makes a decision based on the member name and returns True or False indicating whether or not the search passed the custom test.
Executing Discovered Code
Discovering types at runtime is great but being able to act on those types provides the real power behind reflection. With reflection you can write code that has no idea of a given object or assembly at design time. The code you write can then create an instance of a class within a discovered assembly, find a method on that class, get the method's parameters, and execute the method. This is the ultimate in late binding: locating and executing a type at runtime. In fact, when you use basic late binding in VB, the compiler uses reflection implicitly. Let's look at how to handle this explicitly.
The process for executing discovered code follows these basic steps:
Invoke the object's method and pass the proper parameters. You've already seen how to load an assembly and search for types in that assembly. Once you know the type you are after, you can use the System.Activator class to return an instance of the type. You'll use one of the CreateInstance methods of the Activator class. CreateInstance allows you to specify the object you want created and optionally the parameters used in the object's constructors. Here is a simple example where an object's default constructor takes no parameters:
Dim obj As Object = _
Activator.CreateInstance(myType)
Suppose that you want to create an instance of a given object with a constructor that took parameters. You can do so by passing these values as an array to CreateInstance. Each value needs to be of the same type and in the same order of the constructor's signature. Imagine that the type you were trying to create had the following constructor:
Public Sub New(ByVal someParam As String)
Your first job would be to query the constructor for parameters. Once you've identified the constructor, you can get its parameters using the GetParameters method of the ConstructorInfo class. GetParameters will return an array of ParameterInfo objects that will let you determine a parameter's order, its name, and its data type. You can then build your own array of parameter values and pass it to CreateInstance. Here is a basic example.
Suppose you have a class called SomeClass that has a constructor that takes one parameter that is of the type String. Let's also suppose you have a reference to SomeClass called myType. Finally, let's assume you have a reference (called ci) to SomeClass' constructor as a ConstructorInfo instance. To get a list of parameters for the given constructor (ci) you call GetParameters as follows:
Dim pi() As ParameterInfo
pi = ci.GetParameters()
Then you create your own array of the same size as the number of parameters returned by GetParameters.
Dim params(pi.GetUpperBound(0)) As Object
Values are set for every parameter in the array:
Dim i As Int16
For i = 0 To pi.GetUpperBound(0)
If pi(i).ParameterType.Name = "String" Then
params(i) = "Test"
End If
Next
Finally, you call CreateInstance passing in both your type and the values for the type's constructor's parameters.
Dim o As Object = _
Activator.CreateInstance(myType, params)
Now that you have an instance (o) for your object (SomeClass), let's look at how you might execute one of its methods. The same process of querying for parameters and passing them into a constructor works for methods. Let's suppose that SomeClass has a method called SomeMethod that you want to invoke. To keep this simple let's suppose that SomeMethod takes no parameters (as this is the same process outlined above). To invoke SomeMethod you need to get a reference to the method as a MethodInfo object. You can search for methods on your type with either GetMethod or GetMethods. Let's use GetMethod and pass the method name as a string:
Dim mi As MethodInfo = _
t.GetMethod("SomeMethod")
You have both an instance to SomeClass and a reference (mi) to the method you wish to call so you can use MethodInfo.Invoke to call your target method. You'll pass your object instance that contains your method and an array of parameters that the method takes (in this case Nothing). For example:
mi.Invoke(o, Nothing)
You've now successfully created an instance of an object not necessarily known at design time, found a method on that object, and invoked the method. You can easily extrapolate this example to create a utility such as a testing tool. Suppose that you allow a user to select an assembly. You could list all classes and methods in the given assembly. The user could select a method at run time. You could use discovered information about the class and method to present the user with a form to exercise the method. The user could then enter values for the given method and the testing tool would invoke the method and return the resultsall without knowing a thing about the assembly at design time.
Putting It All Together (A Simple Type Browser)
To show how easy it is to create a .NET type browser using reflection we put together a simple application called Discover that will allow you to browse a directory for any stored assembly (.DLL or .EXE). Once you select an assembly, you can click the Discover button and the application will fill a Tree control with information about that assembly. Figure 2 shows the Discover utility in action.
The information that the Discover application displays is all types in a given assembly, all the members of a given type, all the methods of a given type, and each method's parameters and data types. Displaying this information with reflection is remarkably easy. Listing 2 presents the code that loads the assembly and builds the tree. This code should be familiar to you, as it has been presented in previous sections of this article.
Summary
Hopefully the information in this article will allow you to add reflection as a new tool to your programmer's tool belt. It may not be the tool you'll reach for in typical programming situations, but given the right task, it can provide you with a lot of power.
Part 2 of our reflection series we'll explore using the System.Reflection.Emit namespace to generate code at runtime.
All of the code for this article can be downloaded from www.brilliantstorm.com/resources.
Mike Snell
Figure 2: Discover dialog in action.