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

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

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

522CHAPTER 15 INTRODUCING .NET ASSEMBLIES

the *.config file, you can do so using the System.Configuration.Configuration class type. Consult the .NET Framework 3.5 SDK documentation for complete details.

Source Code The AppConfigReaderApp application can be found under the Chapter 15 subdirectory.

The Machine Configuration File

The configuration files you’ve examined in this chapter have a common theme: they apply only to a specific application (that is why they have the same name as the launching application). In addition, each .NET-aware machine has a file named machine.config that contains a vast number of configuration details (many of which have nothing to do with resolving external assemblies) that control how a specific version of the .NET platform operates.

The .NET platform maintains a separate *.config file for each version of the framework installed on the local machine. For example, the machine.config file for .NET 2.0 can be found under the C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG directory (your version may differ). If you were to open this file, you would find numerous XML elements that control ASP.NET settings, various security details, debugging support, and so forth. However, if you wish to update the machine.config file with machinewide application settings (via an <appSettings> element), you are free to do so.

Although this file can be directly edited using Notepad, be warned that if you alter this file incorrectly, you may cripple the ability of a specific version of the runtime to function correctly. This scenario can be far more painful than a malformed application *.config file, given that XML errors in an application configuration file affect only a single application, but erroneous XML in the machine.config file can break a specific version of the .NET platform.

Summary

This chapter drilled down into the details of how the CLR resolves the location of externally referenced assemblies. You began by examining the content within an assembly: headers, metadata, manifests, and CIL. Then you constructed single-file and multifile assemblies and a handful of client applications (written in a language-agnostic manner).

As you have seen, assemblies may be private or shared. Private assemblies are copied to the client’s subdirectory, whereas shared assemblies are deployed to the GAC, provided they have been assigned a strong name. Finally, as you have seen, private and shared assemblies can be configured using a client-side XML configuration file or, alternatively, via a publisher policy assembly.

C H A P T E R 1 6

Type Reflection, Late Binding, and

Attribute-Based Programming

As shown in the previous chapter, assemblies are the basic unit of deployment in the .NET universe. Using the integrated object browsers of Visual Studio 2008 (and numerous other IDEs), you are able to examine the types within a project’s referenced set of assemblies. Furthermore, external tools such as ildasm.exe and reflector.exe allow you to peek into the underlying CIL code, type metadata, and assembly manifest for a given .NET binary. In addition to this design-time investigation of .NET assemblies, you are also able to programmatically obtain this same information using the System.Reflection namespace. To this end, the first task of this chapter is to define the role of reflection and the necessity of .NET metadata.

The remainder of the chapter examines a number of closely related topics, all of which hinge upon reflection services. For example, you’ll learn how a .NET client may employ dynamic loading and late binding to activate types it has no compile-time knowledge of. You’ll also learn how to insert custom metadata into your .NET assemblies through the use of system-supplied and custom attributes. To put all of these (seemingly esoteric) topics into perspective, the chapter closes by demonstrating how to build several “snap-in objects” that you can plug into an extendable Windows Forms application.

The Necessity of Type Metadata

The ability to fully describe types (classes, interfaces, structures, enumerations, and delegates) using metadata is a key element of the .NET platform. Numerous .NET technologies, such as object serialization, .NET remoting, XML web services, and Windows Communication Foundation (WCF), require the ability to discover the format of types at runtime. Furthermore, cross-language interoperability, numerous compiler services, and an IDE’s IntelliSense capabilities all rely on a concrete description of type.

Regardless of (or perhaps due to) its importance, metadata is not a new idea supplied by the

.NET Framework. Java, CORBA, and COM all have similar concepts. For example, COM type libraries (which are little more than compiled IDL code) are used to describe the types contained within a COM server. Like COM, .NET code libraries also support type metadata. Of course, .NET metadata has no syntactic similarities to COM IDL.

Recall that the ildasm.exe utility allows you to view an assembly’s type metadata using the Ctrl+M keyboard option (see Chapter 1). Thus, if you were to open any of the *.dll or *.exe assemblies created over the course of this book (such as the CarLibrary.dll created in the previous chapter) using ildasm.exe and press Ctrl+M, you would find the relevant type metadata (see Figure 16-1).

523

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

Figure 16-1. Viewing an assembly’s metadata

As you can see, ildasm.exe’s display of .NET type metadata is very verbose (the actual binary format is much more compact). In fact, if I were to list the entire metadata description representing the CarLibrary.dll assembly, it would span several pages. Given that this act would be a woeful waste of paper, let’s just glimpse into some key metadata descriptions of the CarLibrary.dll assembly.

Viewing (Partial) Metadata for the EngineState Enumeration

Each type defined within the current assembly is documented using a TypeDef #n token (where TypeDef is short for type definition). If the type being described uses a type defined within a separate

.NET assembly, the referenced type is documented using a TypeRef #n token (where TypeRef is short for type reference). A TypeRef token is a pointer (if you will) to the referenced type’s full metadata definition in an external library. In a nutshell, .NET metadata is a set of tables that clearly mark all type definitions (TypeDefs) and referenced entities (TypeRefs), all of which can be viewed using ildasm.exe’s metadata window.

As far as CarLibrary.dll goes, one TypeDef we encounter is the metadata description of the CarLibrary.EngineState enumeration (your number may differ; TypeDef numbering is based on the order in which the C# compiler processes the file):

TypeDef

#1

 

 

-------------------------------------------------------

TypDefName: CarLibrary.EngineState (02000002)

Flags

 

: [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] (00000101)

Extends

:

01000001 [TypeRef] System.Enum

...

 

 

 

Field

#2

 

 

-------------------------------------------------------

Field

Name: engineAlive (04000002)

Flags

 

: [Public] [Static] [Literal] [HasDefault] (00008056)

DefltValue: (I4) 0

CallCnvntn: [FIELD]

Field

type:

ValueClass CarLibrary.EngineState

...

 

 

 

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

525

Here, the TypDefName token is used to establish the name of the given type. The Extends metadata token is used to document the base class of a given .NET type (in this case, the referenced type, System.Enum). Each field of an enumeration is marked using the Field #n token. For brevity, I have simply listed the metadata for EngineState.engineAlive.

Viewing (Partial) Metadata for the Car Type

Here is a partial dump of the Car type that illustrates the following:

How fields are defined in terms of .NET metadata

How methods are documented via .NET metadata

How a single type property is mapped to two discrete member functions

TypeDef #3

-------------------------------------------------------

TypDefName: CarLibrary.Car (02000004)

Flags

: [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] (00100081)

Extends

: 01000002 [TypeRef] System.Object

Field #1

 

-------------------------------------------------------

Field

Name: petName (04000008)

Flags

: [Family] (00000004)

CallCnvntn: [FIELD]

Field

type: String

...

 

Method #1

-------------------------------------------------------

MethodName: .ctor (06000001)

Flags

: [Public] [HideBySig] [ReuseSlot] [SpecialName]

[RTSpecialName] [.ctor] (00001886)

RVA

: 0x00002050

ImplFlags : [IL] [Managed] (00000000)

CallCnvntn: [DEFAULT]

hasThis

 

ReturnType: Void

No arguments.

...

 

Property #1

-------------------------------------------------------

Prop.Name

: PetName (17000001)

Flags

: [none] (00000000)

CallCnvntn: [PROPERTY]

hasThis

 

ReturnType: String

No arguments.

 

DefltValue:

 

Setter

: (06000004)

set_PetName

Getter

: (06000003)

get_PetName

0 Others

 

 

...

First, note that the Car class metadata marks the type’s base class and includes various flags that describe how this type was constructed (e.g., [public], [abstract], and whatnot). Methods (such as our Car’s constructor) are described in regard to their parameters, return value, and name.

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

Finally, note how properties are mapped to their internal get/set methods using the .NET metadata Setter/Getter tokens. As you would expect, the derived Car types (SportsCar and MiniVan) are described in a similar manner.

Examining a TypeRef

Recall that an assembly’s metadata will describe not only the set of internal types (Car, EngineState, etc.), but also any external types the internal types reference. For example, given that CarLibrary. dll has defined two enumerations, you find a TypeRef block for the System.Enum type:

TypeRef #1 (01000001)

-------------------------------------------------------

Token: 0x01000001

ResolutionScope: 0x23000001

TypeRefName: System.Enum MemberRef #1

-------------------------------------------------------

Member: (0a00000f) ToString: CallCnvntn: [DEFAULT] hasThis

ReturnType: String No arguments.

Documenting the Defining Assembly

The ildasm.exe metadata window also allows you to view the .NET metadata that describes the assembly itself using the Assembly token. As you can see from the following (partial) listing, information documented within the Assembly table is (surprise, surprise!) the same information that can be viewable via the MANIFEST icon. Here is a partial dump of the manifest of CarLibrary.dll (version 2.0.0.0):

Assembly

-------------------------------------------------------

Token: 0x20000001

Name : CarLibrary

Public Key : 00 24 00 00 04 80 00 00 // Etc...

Hash Algorithm : 0x00008004

Major Version: 0x00000002

Minor Version: 0x00000000

Build Number: 0x00000000

Revision Number: 0x00000000

Locale: <null>

Flags : [SideBySideCompatible] (00000000)

Documenting Referenced Assemblies

In addition to the Assembly token and the set of TypeDef and TypeRef blocks, .NET metadata also makes use of AssemblyRef #n tokens to document each external assembly. Given that the

CarLibrary.dll makes use of the MessageBox type, you find an AssemblyRef for System.Windows. Forms, for example:

 

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

527

AssemblyRef #2

 

 

-------------------------------------------------------

 

Token:

0x23000002

 

Public

Key or

Token: b7 7a 5c 56 19 34 e0 89

 

Name: System.Windows.Forms

 

Version: 2.0.3600.0

 

Major Version: 0x00000002

 

Minor Version: 0x00000000

 

Build Number: 0x00000e10

 

Revision Number: 0x00000000

 

Locale: <null>

 

 

HashValue Blob:

 

Flags: [none]

(00000000)

 

Documenting String Literals

 

The final point of interest regarding .NET metadata is the fact that each and every string literal in

 

your code base is documented under the User Strings token, for example:

 

User Strings

 

 

-------------------------------------------------------

 

70000001

: (11)

L"Jamming {0}"

 

70000019

: (13)

L"Quiet time..."

 

70000035

: (23)

L"CarLibrary Version 2.0!"

 

70000065

: (14)

L"Ramming speed!"

 

70000083

: (19)

L"Faster is better..."

 

700000ab

: (16)

L"Time to call AAA"

 

700000cd

: (16)

L"Your car is dead"

 

Now, don’t be too concerned with the exact syntax of each and every piece of .NET metadata. The bigger point to absorb is that .NET metadata is very descriptive and lists each internally defined (and externally referenced) type found within a given code base.

The next question on your mind may be (in the best-case scenario) “How can I leverage this information in my applications?” or (in the worst-case scenario) “Why should I care about metadata?” To address both points of view, allow me to introduce .NET reflection services. Be aware that the usefulness of the topics presented over the pages that follow may be a bit of a head-scratcher until this chapter’s endgame. So hang tight.

Note You will also find a number of CustomAttribute tokens displayed by the MetaInfo window, which documents the attributes applied within the code base. You’ll learn about the role of .NET attributes later in this chapter.

Understanding Reflection

In the .NET universe, reflection is the process of runtime type discovery. Using reflection services, you are able to programmatically obtain the same metadata information displayed by ildasm.exe using a friendly object model. For example, through reflection, you can obtain a list of all types contained within a given assembly (or *.netmodule), including the methods, fields, properties, and events defined by a given type. You can also dynamically discover the set of interfaces supported by a given type, the parameters of a method, and other related details (base classes, namespace information, manifest data, and so forth).

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

Like any namespace, System.Reflection contains a number of related types. Table 16-1 lists some of the core items you should be familiar with.

Table 16-1. A Sampling of Members of the System.Reflection Namespace

Type

Meaning in Life

Assembly

This class contains a number of methods that allow you to load, investigate,

 

and manipulate an assembly.

AssemblyName

This class allows you to discover numerous details behind an assembly’s

 

identity (version information, culture information, and so forth).

EventInfo

This class holds information for a given event.

FieldInfo

This class holds information for a given field.

MemberInfo

This is the abstract base class that defines common behaviors for the

 

EventInfo, FieldInfo, MethodInfo, and PropertyInfo types.

MethodInfo

This class contains information for a given method.

Module

This class allows you to access a given module within a multifile assembly.

ParameterInfo

This class holds information for a given parameter.

PropertyInfo

This class holds information for a given property.

 

 

To understand how to leverage the System.Reflection namespace to programmatically read

.NET metadata, you need to first come to terms with the System.Type class.

The System.Type Class

The System.Type class defines a number of members that can be used to examine a type’s metadata, a great number of which return types from the System.Reflection namespace. For example, Type. GetMethods() returns an array of MethodInfo types, Type.GetFields() returns an array of FieldInfo types, and so on. The complete set of members exposed by System.Type is quite expansive; however, Table 16-2 offers a partial snapshot of the members supported by System.Type (see the .NET Framework 3.5 SDK documentation for full details).

Table 16-2. Select Members of System.Type

Type

Meaning in Life

IsAbstract

These properties (among others) allow you to discover a number of

IsArray

basic traits about the Type you are referring to (e.g., if it is an

IsClass

abstract method, an array, a nested class, and so forth).

IsCOMObject

 

IsEnum

 

IsGenericTypeDefinition

 

IsGenericParameter

 

IsInterface

 

IsPrimitive

 

IsNestedPrivate

 

IsNestedPublic

 

IsSealedIsValueType

 

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

529

 

 

 

 

Type

Meaning in Life

 

 

GetConstructors()

These methods (among others) allow you to obtain an array

 

GetEvents()

representing the items (interface, method, property, etc.) you are

 

GetFields()

interested in. Each method returns a related array (e.g., GetFields()

 

GetInterfaces()

returns a FieldInfo array, GetMethods() returns a MethodInfo array,

 

GetMembers()

etc.). Be aware that each of these methods has a singular form (e.g.,

 

GetMethods()

GetMethod(), GetProperty(), etc.) that allows you to retrieve a

 

GetNestedTypes()

specific item by name, rather than an array of all related items.

 

GetProperties()

 

 

 

FindMembers()

This method returns an array of MemberInfo types based on search

 

 

criteria.

 

GetType()

This static method returns a Type instance given a string name.

 

InvokeMember()

This method allows late binding to a given item.

 

 

 

 

 

Obtaining a Type Reference Using System.Object.GetType()

You can obtain an instance of the Type class in a variety of ways. However, the one thing you cannot do is directly create a Type object using the new keyword, as Type is an abstract class. Regarding your first choice, recall that System.Object defines a method named GetType(), which returns an instance of the Type class that represents the metadata for the current object:

// Obtain type information using a SportsCar instance.

SportsCar sc = new SportsCar(); Type t = sc.GetType();

Obviously, this approach will only work if you have compile-time knowledge of the type (SportsCar in this case) and currently have an instance of the type in memory. Given this restriction, it should make sense that tools such as ildasm.exe do not obtain type information by directly calling System.Object.GetType() for each type, given the ildasm.exe was not compiled against your custom assemblies!

Obtaining a Type Reference Using System.Type.GetType()

To obtain type information in a more flexible manner, you may call the static GetType() member of the System.Type class and specify the fully qualified string name of the type you are interested in examining. Using this approach, you do not need to have compile-time knowledge of the type you are extracting metadata from, given that Type.GetType() takes an instance of the omnipresent

System.String.

Note When I say you do not need compile-time knowledge when calling Type.GetType(), I am referring to the fact that this method can take any string value whatsoever (rather than a strongly typed variable). Of course, you would still need to know the name of the type in a stringified format!

The Type.GetType() method has been overloaded to allow you to specify two Boolean parameters, one of which controls whether an exception should be thrown if the type cannot be found, and the other of which establishes the case sensitivity of the string. To illustrate, ponder the following:

//Obtain type information using the static Type.GetType() method

//(don't throw an exception if SportsCar cannot be found and ignore case).

Type t = Type.GetType("CarLibrary.SportsCar", false, true);

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

In the previous example, notice that the string you are passing into GetType() makes no mention of the assembly containing the type. In this case, the assumption is that the type is defined within the currently executing assembly. However, when you wish to obtain metadata for a type within an external private assembly, the string parameter is formatted using the type’s fully qualified name, followed by the friendly name of the assembly containing the type (each of which is separated by a comma):

// Obtain type information for a type within an external assembly.

Type t = Type.GetType("CarLibrary.SportsCar, CarLibrary");

As well, do know that the string passed into Type.GetType() may specify a plus token (+) to denote a nested type. Assume you wish to obtain type information for an enumeration (SpyOptions) nested within a class named JamesBondCar. To do so, you would write the following:

//Obtain type information for a nested enumeration

//within the current assembly.

Type t = Type.GetType("CarLibrary.JamesBondCar+SpyOptions");

Obtaining a Type Reference Using typeof()

The final way to obtain type information is using the C# typeof operator:

// Get the Type using typeof.

Type t = typeof(SportsCar);

Like Type.GetType(), the typeof operator is helpful in that you do not need to first create an object instance to extract type information. However, your code base must still have compile-time knowledge of the type you are interested in examining, as typeof expects the strongly typed name of the type, rather than a textual representation of the type.

Building a Custom Metadata Viewer

To illustrate the basic process of reflection (and the usefulness of System.Type), let’s create a Console Application named MyTypeViewer. This program will display details of the methods, properties, fields, and supported interfaces (in addition to some other points of interest) for any type within mscorlib.dll (recall all .NET applications have automatic access to this core framework class library) or a type within MyTypeViewer itself. Once the application has been created, be sure to import the System.Reflection namespace.

using System.Reflection;

Reflecting on Methods

The Program class will be updated to define a number of static methods, each of which takes a single System.Type parameter and returns void. First you have ListMethods(), which (as you might guess) prints the name of each method defined by the incoming type. Notice how Type.GetMethods() returns an array of System.Reflection.MethodInfo types:

// Display method names of type. static void ListMethods(Type t)

{

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

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

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

531

Console.WriteLine("->{0}", m.Name); Console.WriteLine();

}

Here, you are simply printing the name of the method using the MethodInfo.Name property. As you might guess, MethodInfo has many additional members that allow you to determine whether the method is static, virtual, generic, or abstract. As well, the MethodInfo type allows you to obtain the method’s return value and parameter set. You’ll spruce up the implementation of ListMethods() in just a bit.

Reflecting on Fields and Properties

The implementation of ListFields() is similar. The only notable difference is the call to Type. GetFields() and the resulting FieldInfo array. Again, to keep things simple, you are printing out only the name of each field.

// Display field names of type. static void ListFields(Type t)

{

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

FieldInfo[] fi = t.GetFields(); foreach(FieldInfo field in fi)

Console.WriteLine("->{0}", field.Name); Console.WriteLine();

}

The logic to display a type’s properties is similar:

// Display property names of type. static void ListProps(Type t)

{

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

PropertyInfo[] pi = t.GetProperties(); foreach(PropertyInfo prop in pi)

Console.WriteLine("->{0}", prop.Name); Console.WriteLine();

}

Reflecting on Implemented Interfaces

Next, you will author a method named ListInterfaces() that will print out the names of any interfaces supported on the incoming type. The only point of interest here is that the call to GetInterfaces() returns an array of System.Types! This should make sense given that interfaces are, indeed, types:

// Display implemented interfaces. static void ListInterfaces(Type t)

{

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

Type[] ifaces = t.GetInterfaces(); foreach(Type i in ifaces)

Console.WriteLine("->{0}", i.Name);

}