Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

532 CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

Displaying Various Odds and Ends

Last but not least, you have one final helper method that will simply display various statistics (indicating whether the type is generic, what the base class is, whether the type is sealed, and so forth) regarding the incoming type:

// Just for good measure.

static void ListVariousStats(Type t)

{

Console.WriteLine("***** Various Statistics *****");

Console.WriteLine("Base class is: {0}", t.BaseType); Console.WriteLine("Is type abstract? {0}", t.IsAbstract); Console.WriteLine("Is type sealed? {0}", t.IsSealed); Console.WriteLine("Is type generic? {0}", t.IsGenericTypeDefinition); Console.WriteLine("Is type a class type? {0}", t.IsClass); Console.WriteLine();

}

Implementing Main()

The Main() method of the Program class prompts the user for the fully qualified name of a type. Once you obtain this string data, you pass it into the Type.GetType() method and send the extracted System.Type into each of your helper methods. This process repeats until the user enters Q to terminate the application:

static void Main(string[] args)

{

Console.WriteLine("***** Welcome to MyTypeViewer *****");

string typeName = ""; bool userIsDone = false;

do

{

Console.WriteLine("\nEnter a type name to evaluate"); Console.Write("or enter Q to quit: ");

// Get name of type.

typeName = Console.ReadLine();

// Does user want to quit?

if (typeName.ToUpper() == "Q")

{

userIsDone = true; break;

}

// Try to display type. try

{

Type t = Type.GetType(typeName); Console.WriteLine(""); ListVariousStats(t);

CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

533

ListFields(t);

ListProps(t);

ListMethods(t);

ListInterfaces(t);

}

catch

{

Console.WriteLine("Sorry, can't find type");

}

} while (!userIsDone);

}

At this point, MyTypeViewer.exe is ready to take out for a test drive. For example, run your application and enter the following fully qualified names (be aware that the manner in which you invoked Type.GetType() requires case-sensitive string names):

System.Int32

System.Collections.ArrayList

System.Threading.Thread

System.Void

System.IO.BinaryWriter

System.Math

System.Console

MyTypeViewer.Program

Figure 16-2 shows the partial output when specifying System.Math.

Figure 16-2. Reflecting on System.Math

534 CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

Reflecting on Generic Types

When you call Type.GetType() in order to obtain metadata descriptions of generic types, you must make use of a special syntax involving a “back tick” character (`) followed by a numerical value that represents the number of type parameters the type supports. For example, if you wish to print out the metadata description of List<T>, you would need to pass the following string into your application:

System.Collections.Generic.List`1

Here, we are using the numerical value of 1, given that List<T> has only one type parameter. However, if you wish to reflect over Dictionary<TKey, TValue>, you would supply the value 2:

System.Collections.Generic.Dictionary`2

Reflecting on Method Parameters and Return Values

So far, so good! Let’s make one minor enhancement to the current application. Specifically, you will update the ListMethods() helper function to list not only the name of a given method, but also the return value and incoming parameters. The MethodInfo type provides the ReturnType property and GetParameters() method for these very tasks. In the following code, notice that you are building a string type that contains the type and name of each parameter using a nested foreach loop:

static void ListMethods(Type t)

{

Console.WriteLine("***** Methods *****");

MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi)

{

// Get return value.

string retVal = m.ReturnType.FullName; string paramInfo = "(";

// Get params.

foreach (ParameterInfo pi in m.GetParameters())

{

paramInfo += string.Format("{0} {1} ", pi.ParameterType, pi.Name);

}

paramInfo += ")";

// Now display the basic method sig.

Console.WriteLine("->{0} {1} {2}", retVal, m.Name, paramInfo);

}

Console.WriteLine();

}

If you now run this updated application, you will find that the methods of a given type are much more detailed. Figure 16-3 shows the method metadata of the System.Globalization. GregorianCalendar type.

CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

535

Figure 16-3. Method details of System.Globalization.GregorianCalendar

The current implementation of ListMethods() is helpful, in that you can directly investigate each parameter and method return value using the System.Reflection object model. As an extreme shortcut, be aware that each of the XXXInfo types (MethodInfo, PropertyInfo, EventInfo, etc.) have overridden ToString() to display the signature of the item requested. Thus, we could also implement ListMethods() as follows:

public static void ListMethods(Type t)

{

Console.WriteLine("***** Methods *****");

MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi)

{

//Could also simply say "Console.WriteLine(m)" as well,

//as ToString() is called automatically by WriteLine().

Console.WriteLine(m.ToString());

}

Console.WriteLine();

}

Interesting stuff, huh? Clearly the System.Reflection namespace and System.Type class allow you to reflect over many other aspects of a type beyond what MyTypeViewer is currently displaying. As you would hope, you can obtain a type’s events, get the list of any generic parameters for a given member, and glean dozens of other details.

Nevertheless, at this point you have created a (somewhat capable) object browser. The major limitation, of course, is that you have no way to reflect beyond the current assembly (MyTypeViewer) or the always accessible mscorlib.dll. This begs the question, “How can I build applications that can load (and reflect over) assemblies not referenced at compile time?”

Source Code The MyTypeViewer project can be found under the Chapter 16 subdirectory.

536 CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

Dynamically Loading Assemblies

In the previous chapter, you learned all about how the CLR consults the assembly manifest when probing for an externally referenced assembly. However, there will be many times when you need to load assemblies on the fly programmatically, even if there is no record of said assembly in the manifest. Formally speaking, the act of loading external assemblies on demand is known as a dynamic load.

System.Reflection defines a class named Assembly. Using this type, you are able to dynamically load an assembly as well as discover properties about the assembly itself. Using the Assembly type, you are able to dynamically load private or shared assemblies, as well as load an assembly located at an arbitrary location. In essence, the Assembly class provides methods (Load() and LoadFrom() in particular) that allow you to programmatically supply the same sort of information found in a client-side *.config file.

To illustrate dynamic loading, create a brand-new Console Application named External AssemblyReflector. Your task is to construct a Main() method that prompts for the friendly name of an assembly to load dynamically. You will pass the Assembly reference into a helper method named DisplayTypes(), which will simply print the names of each class, interface, structure, enumeration, and delegate it contains. The code is refreshingly simple:

using System;

using System.Reflection;

using System.IO; // For FileNotFoundException definition.

namespace ExternalAssemblyReflector

{

class Program

{

static void DisplayTypesInAsm(Assembly asm)

{

Console.WriteLine("\n***** Types in Assembly *****");

Console.WriteLine("->{0}", asm.FullName); Type[] types = asm.GetTypes();

foreach (Type t in types) Console.WriteLine("Type: {0}", t);

Console.WriteLine("");

}

static void Main(string[] args)

{

Console.WriteLine("***** External Assembly Viewer *****");

string asmName = ""; bool userIsDone = false; Assembly asm = null;

do

{

Console.WriteLine("\nEnter an assembly to evaluate"); Console.Write("or enter Q to quit: ");

// Get name of assembly. asmName = Console.ReadLine();

CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

537

//Does user want to quit? if (asmName.ToUpper() == "Q")

{

userIsDone = true; break;

}

//Try to load assembly.

try

{

asm = Assembly.Load(asmName); DisplayTypesInAsm(asm);

}

catch

{

Console.WriteLine("Sorry, can't find assembly.");

}

} while (!userIsDone);

}

}

}

Notice that the static Assembly.Load() method has been passed only the friendly name of the assembly you are interested in loading into memory. Thus, if you wish to reflect over CarLibrary. dll, you will need to copy the CarLibrary.dll binary to the \bin\Debug directory of the External AssemblyReflector application to run this program. Once you do, you will find output similar to Figure 16-4.

Figure 16-4. Reflecting on the external CarLibrary assembly

If you wish to make ExternalAssemblyReflector more flexible, you can update your code to load the external assembly using Assembly.LoadFrom() rather than Assembly.Load(). By doing so, you can enter an absolute path to the assembly you wish to view (e.g., C:\MyApp\MyAsm.dll). Essentially, Assembly.LoadFrom() allows you to programmatically supply a <codeBase> value.

Source Code The ExternalAssemblyReflector project is included in the Chapter 16 subdirectory.

538 CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

Reflecting on Shared Assemblies

The Assembly.Load() method has been overloaded a number of times. One variation allows you to specify a culture value (for localized assemblies) as well as a version number and public key token value (for shared assemblies). Collectively speaking, the set of items identifying an assembly is termed the display name. The format of a display name is a comma-delimited string of name/value pairs that begins with the friendly name of the assembly, followed by optional qualifiers (that may appear in any order). Here is the template to follow (optional items appear in parentheses):

Name (,Version = major.minor.build.revision) (,Culture = culture token) (,PublicKeyToken= public key token)

When you’re crafting a display name, the convention PublicKeyToken=null indicates that binding and matching against a non–strongly named assembly is required. Additionally, Culture="" indicates matching against the default culture of the target machine, for example:

// Load version 1.0.982.23972 of CarLibrary using the default culture.

Assembly a = Assembly.Load(

@"CarLibrary, Version=1.0.982.23972, PublicKeyToken=null, Culture=""");

Also be aware that the System.Reflection namespace supplies the AssemblyName type, which allows you to represent the preceding string information in a handy object variable. Typically, this class is used in conjunction with System.Version, which is an OO wrapper around an assembly’s version number. Once you have established the display name, it can then be passed into the overloaded Assembly.Load() method:

// Make use of AssemblyName to define the display name.

AssemblyName asmName; asmName = new AssemblyName(); asmName.Name = "CarLibrary";

Version v = new Version("1.0.982.23972"); asmName.Version = v;

Assembly a = Assembly.Load(asmName);

To load a shared assembly from the GAC, the Assembly.Load() parameter must specify a PublicKeyToken value. For example, assume you wish to load version 2.0.0.0 of the System.Windows. Forms.dll assembly provided by the .NET base class libraries. Given that the number of types in this assembly is quite large, the following application only prints out the names of public enums, using a simple LINQ query:

using System;

using System.Reflection; using System.IO;

using System.Linq;

namespace SharedAsmReflector

{

public class SharedAsmReflector

{

private static void DisplayInfo(Assembly a)

{

Console.WriteLine("***** Info about Assembly *****");

Console.WriteLine("Loaded from GAC? {0}", a.GlobalAssemblyCache); Console.WriteLine("Asm Name: {0}", a.GetName().Name); Console.WriteLine("Asm Version: {0}", a.GetName().Version); Console.WriteLine("Asm Culture: {0}",

a.GetName().CultureInfo.DisplayName); Console.WriteLine("\nHere are the public enums:");

CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

539

// Use a LINQ query to find the public enums.

Type[] types = a.GetTypes();

var publicEnums = from pe in types where pe.IsEnum && pe.IsPublic select pe;

foreach (var pe in publicEnums)

{

Console.WriteLine(pe);

}

}

static void Main(string[] args)

{

Console.WriteLine("***** The Shared Asm Reflector App *****\n");

// Load System.Windows.Forms.dll from GAC. string displayName = null;

displayName = "System.Windows.Forms," + "Version=2.0.0.0," + "PublicKeyToken=b77a5c561934e089," + @"Culture=""";

Assembly asm = Assembly.Load(displayName); DisplayInfo(asm); Console.WriteLine("Done!"); Console.ReadLine();

}

}

}

Source Code The SharedAsmReflector project is included in the Chapter 16 subdirectory.

At this point you should understand how to use some of the core types defined within the System.Reflection namespace to discover metadata at runtime. Of course, I realize despite the “cool factor,” you likely will not need to build custom object browsers at your place of employment. Do recall, however, that reflection services are the foundation for a number of very common programming activities, including late binding.

Understanding Late Binding

Simply put, late binding is a technique in which you are able to create an instance of a given type and invoke its members at runtime without having hard-coded compile-time knowledge of its existence. When you are building an application that binds late to a type in an external assembly, you have no reason to set a reference to the assembly; therefore, the caller’s manifest has no direct listing of the assembly.

At first glance, it is not easy to see the value of late binding. It is true that if you can “bind early” to a type (e.g., set an assembly reference and allocate the type using the C# new keyword), you should opt to do so. For one reason, early binding allows you to determine errors at compile time, rather than at runtime. Nevertheless, late binding does have a critical role in any extendable application you may be building. You will have a chance to build such an “extendable” program at the end of this chapter in the section “Building an Extendable Application”; until then, we need to examine the role of the Activator type.

540 CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

The System.Activator Class

The System.Activator class is the key to the .NET late binding process. Beyond the methods inherited from System.Object, Activator defines only a small set of members, many of which have to do with the .NET remoting API. For our current example, we are only interested in the Activator.

CreateInstance() method, which is used to create an instance of a type à la late binding.

This method has been overloaded numerous times to provide a good deal of flexibility. The simplest variation of the CreateInstance() member takes a valid Type object that describes the entity you wish to allocate on the fly. Create a new Console Application named LateBindingApp, and update the Main() method as follows (be sure to place a copy of CarLibrary.dll in the project’s \bin\Debug directory):

// Create a type dynamically. public class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Late Binding *****");

//Try to load a local copy of CarLibrary.

Assembly a = null; try

{

a = Assembly.Load("CarLibrary");

}

catch(FileNotFoundException e)

{

Console.WriteLine(e.Message);

return;

}

//Get metadata for the Minivan type.

Type miniVan = a.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly.

object obj = Activator.CreateInstance(miniVan); Console.WriteLine("Created a {0} using late binding!", obj); Console.ReadLine();

}

}

Notice that the Activator.CreateInstance() method returns a System.Object rather than a strongly typed MiniVan. Therefore, if you apply the dot operator on the obj variable, you will fail to see any members of the MiniVan type. At first glance, you may assume you can remedy this problem with an explicit cast; however, this program has no clue what a MiniVan is in the first place (and if you did, why use late binding at all)!

Remember that the whole point of late binding is to create instances of objects for which there is no compile-time knowledge. Given this, how can you invoke the underlying methods of the MiniVan object stored in the System.Object variable? The answer, of course, is by using reflection.

Invoking Methods with No Parameters

Assume you wish to invoke the TurboBoost() method of the MiniVan. As you recall, this method will set the state of the engine to “dead” and display an informational message box. The first step is to obtain a MethodInfo type for the TurboBoost() method using Type.GetMethod(). From the resulting

CHAPTER 16 TYPE REFLECTION, LATE BINDING, AND ATTRIBUTE-BASED PROGRAMMING

541

MethodInfo, you are then able to call MiniVan.TurboBoost using Invoke(). MethodInfo.Invoke() requires you to send in all parameters that are to be given to the method represented by MethodInfo. These parameters are represented by an array of System.Object types (as the parameters for a given method could be any number of various entities).

Given that TurboBoost() does not require any parameters, you can simply pass null (meaning “this method has no parameters”). Update your Main() method as follows:

static void Main(string[] args)

{

...

// Get metadata for the Minivan type.

Type miniVan = a.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly.

object obj = Activator.CreateInstance(miniVan); Console.WriteLine("Created a {0} using late binding!", obj);

// Get info for TurboBoost.

MethodInfo mi = miniVan.GetMethod("TurboBoost");

// Invoke method ('null' for no parameters). mi.Invoke(obj, null);

Console.ReadLine();

}

At this point you are happy to see the message box in Figure 16-5.

Figure 16-5. Late-bound method invocation

Invoking Methods with Parameters

To illustrate how to dynamically invoke a method that does take some number of parameters, assume you have updated the MiniVan type created in the previous chapter with a new method named TellChildToBeQuiet():

// Quiet down the troops...

public void TellChildToBeQuiet(string kidName, int shameIntensity)

{

for(int i = 0 ; i < shameIntensity; i++) MessageBox.Show(string.Format("Be quiet {0} !!", kidName));

}

TellChildToBeQuiet() takes two parameters: a string representing the child’s name and an integer representing your current level of frustration. When using late binding, parameters are packaged as an array of System.Objects. To invoke the new method, update the Main() method as follows: