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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

404 C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

Figure 12-4. Reflecting on the external CarLibrary assembly

If you wish to make ExternalAssemblyReflector more flexible, 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).

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

Reflecting on Shared Assemblies

As you may suspect, Assembly.Load() has been overloaded a number of times. One variation of the Assembly.Load() method 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 (,Culture = culture token) (,Version = major.minor.build.revision) (,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;

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

405

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 simply prints out the names of the first 20 types:

using System;

using System.Reflection; using System.IO;

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);

Type[] types = a.GetTypes(); for(int i = 0; i < 20; i++)

Console.WriteLine("Type: {0}", types[i]);

}

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.ReadLine();

}

}

}

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

Sweet! At this point you should understand how to use some of the core items defined within the System.Reflection namespace to discover metadata at runtime. Of course, I realize despite the

406C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

“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 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, you may not understand 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.

The System.Activator Class

The System.Activator class is the key to .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

.NET remoting (see Chapter 18). 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 application named LateBinding, and update the Main() method as so (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)

{

//Try to load a local copy of CarLibrary.

Assembly a = null; try

{

a = Assembly.Load("CarLibrary");

}

catch(FileNotFoundException e)

{

Console.WriteLine(e.Message);

Console.ReadLine();

return;

}

//Get metadata for the Minivan type.

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

// Create the Minivan on the fly.

object obj = Activator.CreateInstance(miniVan);

}

}

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

407

Notice that the Activator.CreateInstance() method returns a generic 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!

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

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 so:

static void Main(string[] args)

{

//Try to load a local copy of CarLibrary.

...

//Get the MiniVan type.

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

// Create the MiniVan on the fly.

object obj = Activator.CreateInstance(miniVan);

// Get info for TurboBoost.

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

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

}

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

Figure 12-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 the MiniVan type defines a method named TellChildToBeQuiet():

408 C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

// Quiet down the troops...

public void TellChildToBeQuiet(string kidName, int shameIntensity)

{

for(int i = 0 ; i < shameIntensity; i++) MessageBox.Show("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, add the following code to your Main() method:

// Bind late to a method taking params. object[] paramArray = new object[2]; paramArray[0] = "Fred"; // Child name. paramArray[1] = 4; // Shame Intensity. mi = miniVan.GetMethod("TellChildToBeQuiet"); mi.Invoke(obj, paramArray);

If you run this program, you will see four message boxes pop up, shaming young Fred. Hopefully at this point you can see the relationships among reflection, dynamic loading, and late binding. Again, you still may wonder exactly when you might make use of these techniques in your own applications. The conclusion of this chapter should shed light on this question; however, the next topic under investigation is the role of .NET attributes.

Source Code The LateBinding project is included in the Chapter 12 subdirectory.

Understanding Attributed Programming

As illustrated at beginning of this chapter, one role of a .NET compiler is to generate metadata descriptions for all defined and referenced types. In addition to this standard metadata contained within any assembly, the .NET platform provides a way for programmers to embed additional metadata into an assembly using attributes. In a nutshell, attributes are nothing more than code annotations that can be applied to a given type (class, interface, structure, etc.), member (property, method, etc.), assembly, or module.

The idea of annotating code using attributes is not new. COM IDL provided numerous predefined attributes that allowed developers to describe the types contained within a given COM server. However, COM attributes were little more than a set of keywords. If a COM developer needed to create a custom attribute, they could do so, but it was referenced in code by a 128-bit number (GUID), which was cumbersome at best.

Unlike COM IDL attributes (which again were simply keywords), .NET attributes are class types that extend the abstract System.Attribute base class. As you explore the .NET namespaces, you will find many predefined attributes that you are able to make use of in your applications. Furthermore, you are free to build custom attributes to further qualify the behavior of your types by creating a new type deriving from Attribute.

Understand that when you apply attributes in your code, the embedded metadata is essentially useless until another piece of software explicitly reflects over the information. If this is not the case, the blurb of metadata embedded within the assembly is ignored and completely harmless.

Attribute Consumers

As you would guess, the .NET Framework 2.0 SDK ships with numerous utilities that are indeed on the lookout for various attributes. The C# compiler (csc.exe) itself has been preprogrammed to dis-

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

409

cover the presence of various attributes during the compilation cycle. For example, if the C# compiler encounters the [CLSCompilant] attribute, it will automatically check the attributed item to ensure it is exposing only CLS-compliant constructs. By way of another example, if the C# compiler discovers an item attributed with the [Obsolete] attribute, it will display a compiler warning in the Visual Studio 2005 Error List window.

In addition to development tools, numerous methods in the .NET base class libraries are preprogrammed to reflect over specific attributes. For example, if you wish to persist the state of an object to file, all you are required to do is annotate your class with the [Serializable] attribute. If the Serialize() method of the BinaryFormatter class encounters this attribute, the object is automatically persisted to file in a compact binary format.

The .NET CLR is also on the prowl for the presence of certain attributes. Perhaps the most famous

.NET attribute is [WebMethod]. If you wish to expose a method via HTTP requests and automatically encode the method return value as XML, simply apply [WebMethod] to the method and the CLR handles the details. Beyond web service development, attributes are critical to the operation of the .NET security system, .NET remoting layer, and COM/.NET interoperability (and so on).

Finally, you are free to build applications that are programmed to reflect over your own custom attributes as well as any attribute in the .NET base class libraries. By doing so, you are essentially able to create a set of “keywords” that are understood by a specific set of assemblies.

Applying Predefined Attributes in C#

As previously mentioned, the .NET base class library provides a number of attributes in various namespaces. Table 12-3 gives a snapshot of some—but by absolutely no means all—predefined attributes.

Table 12-3. A Tiny Sampling of Predefined Attributes

Attribute

Meaning in Life

[CLSCompliant]

Enforces the annotated item to conform to the rules of the Common

 

Language Specification (CLS). Recall that CLS-compliant types are

 

guaranteed to be used seamlessly across all .NET programming

 

languages.

[DllImport]

Allows .NET code to make calls to any unmanaged C- or C++-based code

 

library, including the API of the underlying operating system. Do note

 

that [DllImport] is not used when communicating with COM-based

 

software.

[Obsolete]

Marks a deprecated type or member. If other programmers attempt to use

 

such an item, they will receive a compiler warning describing the error of

 

their ways.

[Serializable]

Marks a class or structure as being “serializable.”

[NonSerialized]

Specifies that a given field in a class or structure should not be persisted

 

during the serialization process.

[WebMethod]

Marks a method as being invokable via HTTP requests and instructs the

 

CLR to serialize the method return value as XML (see Chapter 25 for

 

complete details).

 

 

To illustrate the process of applying attributes in C#, assume you wish build a class named Motorcycle that can be persisted in a binary format. To do so, simply apply the [Serializable] attribute to the class definition. If you have a field that should not be persisted, you may apply the

[NonSerialized] attribute:

410 C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

// This class can be saved to disk. [Serializable]

public class Motorcycle

{

//However this field will not be persisted. [NonSerialized]

float weightOfCurrentPassengers;

//These fields are still serializable. bool hasRadioSystem;

bool hasHeadSet; bool hasSissyBar;

}

Note An attribute only applies to the “very next” item. For example, the only nonserialized field of the Motorcycle class is weightOfCurrentPassengers. The remaining fields are serializable given that the entire class has been annotated with [Serializable].

At this point, don’t concern yourself with the actual process of object serialization (Chapter 17 examines the details). Just notice that when you wish to apply an attribute, the name of the attribute is sandwiched between square brackets.

Once this class has been compiled, you can view the extra metadata using ildasm.exe. Notice that these attributes are recorded using the serializable and notserialized tokens (see Figure 12-6).

Figure 12-6. Attributes shown in ildasm.exe

As you might guess, a single item can be attributed with multiple attributes. Assume you have a legacy C# class type (HorseAndBuggy) that was marked as serializable, but is now considered obsolete for current development. To apply multiple attributes to a single item, simply use

a comma-delimited list:

[Serializable,

Obsolete("This class is obsolete, use another vehicle!")] public class HorseAndBuggy

{

// ...

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

411

As an alternative, you can also apply multiple attributes on a single item by stacking each attribute as so (the end result is identical):

[Serializable]

[Obsolete("This class is obsolete, use another vehicle!")] public class HorseAndBuggy

{

// ...

}

Specifying Constructor Parameters for Attributes

Notice that the [Obsolete] attribute is able to accept what appears to be a constructor parameter. If you view the formal definition of the [Obsolete] attribute using the Code Definition window of Visual Studio 2005, you will find that this class indeed provides a constructor receiving a System.String:

public sealed class ObsoleteAttribute : System.Attribute

{

public bool IsError { get; } public string Message { get; }

public ObsoleteAttribute(string message, bool error); public ObsoleteAttribute(string message);

public ObsoleteAttribute();

}

Understand that when you supply constructor parameters to an attribute, the attribute is not allocated into memory until they parameters are reflected upon by another type or an external tool. The string data defined at the attribute level is simply stored within the assembly as a blurb of metadata.

The Obsolete Attribute in Action

Now that HorseAndBuggy has been marked as obsolete, if you were to allocate an instance of this type, you would find that the supplied string data is extracted and displayed within the Error List window of Visual Studio 2005 (see Figure 12-7).

Figure 12-7. Attributes in action

In this case, the “other piece of software” that is reflecting on the [Obsolete] attribute is the C# compiler.

C# Attribute Shorthand Notation

If you were reading closely, you may have noticed that the actual class name of the [Obsolete] attribute is ObsoleteAttribute, not Obsolete. As a naming convention, all .NET attributes (including custom attributes you may create yourself ) are suffixed with the “Attribute” token. However, to

412C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

simplify the process of applying attributes, the C# language does not require you to type in the Attribute suffix. Given this, the following iteration of the HorseAndBuggy type is identical to the previous (it just involves a few more keystrokes):

[SerializableAttribute]

[ObsoleteAttribute("This class is obsolete, use another vehicle!")] public class HorseAndBuggy

{

// ...

}

Be aware that this is a courtesy provided by C#. Not all .NET-enabled languages support this feature. In any case, at this point you should hopefully understand the following key points regarding

.NET attributes:

Attributes are classes that derive from System.Attribute.

Attributes result in embedded metadata.

Attributes are basically useless until another agent reflects upon them.

Attributes are applied in C# using square brackets.

Next up, let’s examine how you can build your own custom attributes and a piece of custom software that reflects over the embedded metadata.

Building Custom Attributes

The first step in building a custom attribute is to create a new class deriving from System.Attribute. Keeping in step with the automobile theme used throughout this book, assume you have created a brand new C# class library named AttributedCarLibrary. This assembly will define a handful of vehicles (some of which you have already seen in this text), each of which is described using a custom attribute named VehicleDescriptionAttribute:

// A custom attribute.

public sealed class VehicleDescriptionAttribute : System.Attribute

{

private string msgData;

public VehicleDescriptionAttribute(string description) { msgData = description;}

public VehicleDescriptionAttribute(){ }

public string Description

{

get { return msgData; } set { msgData = value; }

}

}

As you can see, VehicleDescriptionAttribute maintains a private internal string (msgData) that can be set using a custom constructor and manipulated using a type property (Description). Beyond the fact that this class derived from System.Attribute, there is nothing unique to this class definition.

C H A P T E R 1 2 T Y P E R E F L E C T I O N, L AT E B I N D I N G, A N D AT T R I B U T E - B A S E D P R O G R A M M I N G

413

Note For security reasons, it is considered a .NET best practice to design all custom attributes as sealed.

Applying Custom Attributes

Given that VehicleDescriptionAttribute is derived from System.Attribute, you are now able to annotate your vehicles as you see fit:

// Assign description using a 'named property'.

[Serializable,

VehicleDescription(Description = "My rocking Harley")] public class Motorcycle

{

// ...

}

[SerializableAttribute]

[ObsoleteAttribute("This class is obsolete, use another vehicle!"), VehicleDescription("The old gray mare, she ain't what she used to be...")] public class HorseAndBuggy

{

// ...

}

[VehicleDescription("A very long, slow, but feature-rich auto")] public class Winnebago

{

// ...

}

Notice that the description of the Motorcycle is assigned a description using a new bit of attributecentric syntax termed a named property. In the constructor of the first [VehicleDescription] attribute, you set the underlying System.String using a name/value pair. If this attribute is reflected upon by an external agent, the value is fed into the Description property (named property syntax is legal only if the attribute supplies a writable .NET property). In contrast, the HorseAndBuggy and Winnebago types are not making use of named property syntax and are simply passing the string data via the custom constructor.

Once you compile the AttributedCarLibrary assembly, you can make use of ildasm.exe to view the injected metadata descriptions for your type. For example, here is an embedded description of the Winnebago type (see Figure 12-8).

Figure 12-8. Embedded vehicle description data