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

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

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

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

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

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

}

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 LateBindingApp project is included in the Chapter 16 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, he or she 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.

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

543

Attribute Consumers

As you would guess, the .NET 3.5 Framework SDK ships with numerous utilities that are indeed on the lookout for various attributes. The C# compiler (csc.exe) itself has been preprogrammed to discover the presence of various attributes during the compilation cycle. For example, if the C# compiler encounters the [CLSCompliant] 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 2008 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, Windows Communication Foundation, 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 Attributes in C#

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

Table 16-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,” meaning it is able to persist

 

its current state into a stream.

[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.

 

 

To illustrate the process of applying attributes in C#, assume you wish to 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:
// 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 21 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 16-6).
Figure 16-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. Rather than deleting the class definition from your code base (and risk breaking existing software), you can mark the class with the [Obsolete] attribute. To apply multiple attributes to a single item, simply use a comma-delimited list:

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

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

545

[Serializable, Obsolete("Use another vehicle!")] public class HorseAndBuggy

{

// ...

}

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

[Serializable]

[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 2008, 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 the 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:

static void Main(string[] args)

{

HorseAndBuggy mule = new HorseAndBuggy();

}

you would find that the supplied string data is extracted and displayed within the Error List window of Visual Studio 2008 (see Figure 16-7).

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

Figure 16-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 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("Use another vehicle!")] public class HorseAndBuggy

{

// ...

}

Be aware that this is a courtesy provided by C#. Not all .NET-enabled languages support this shorthand attribute syntax. 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:

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

547

// 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.

Note For security reasons, it is considered a .NET best practice to design all custom attributes as sealed. In fact, Visual Studio 2008 provides a code snippet named Attribute that will dump out a new System. Attribute-derived class into your code window. See Chapter 2 for an explication of using code snippets.

Applying Custom Attributes

Given that VehicleDescriptionAttribute is derived from System.Attribute, you are now able to annotate your vehicles as you see fit. For testing purposes, add the following class definitions to your new class library:

// Assign description using a "named property."

[Serializable]

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

{

}

[SerializableAttribute] [ObsoleteAttribute("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

{

}

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

Named Property Syntax

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. In any case, once you compile the AttributedCarLibrary assembly, you can make use of ildasm.exe to view the injected metadata descriptions for your type. For example, Figure 16-8 shows an embedded description of the

Winnebago type.

Figure 16-8. Embedded vehicle description data

Restricting Attribute Usage

By default, custom attributes can be applied to just about any aspect of your code (methods, classes, properties, and so on). Thus, if it made sense to do so, you could use VehicleDescription to qualify methods, properties, or fields (among other things):

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

{

[VehicleDescription("My rocking CD player")] public void PlayMusic(bool On)

{

...

}

}

In some cases, this is exactly the behavior you require. Other times, however, you may want to build a custom attribute that can be applied only to select code elements. If you wish to constrain the scope of a custom attribute, you will need to apply the [AttributeUsage] attribute on the definition of your custom attribute. The [AttributeUsage] attribute allows you to supply any combination of values (via an OR operation) from the AttributeTargets enumeration:

// This enumeration defines the possible targets of an attribute. public enum AttributeTargets

{

All, Assembly, Class, Constructor, Delegate, Enum, Event, Field, Interface, Method, Module, Parameter, Property, ReturnValue, Struct

}

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

549

Furthermore, [AttributeUsage] also allows you to optionally set a named property (AllowMultiple) that specifies whether the attribute can be applied more than once on the same item. As well, [AttributeUsage] allows you to establish whether the attribute should be inherited by derived classes using the Inherited named property.

To establish that the [VehicleDescription] attribute can be applied only once on a class or structure (and the value is not inherited by derived types), you can update the

VehicleDescriptionAttribute definition as follows:

//This time, we are using the AttributeUsage attribute

//to annotate our custom attribute.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]

public sealed class VehicleDescriptionAttribute : System.Attribute

{

...

}

With this, if a developer attempted to apply the [VehicleDescription] attribute on anything other than a class or structure, he or she is issued a compile-time error.

Tip Always get in the habit of explicitly marking the usage flags for any custom attribute you may create, as not all .NET programming languages honor the use of unqualified attributes!

Assembly-Level (and Module-Level) Attributes

It is also possible to apply attributes on all types within a given module (for a multifile assembly) or all modules within a given assembly using the [module:] and [assembly:] tags, respectively. For example, assume you wish to ensure that every public type defined within your assembly is CLS compliant. To do so, simply add the following line in any one of your C# source code files (do note that assembly-level attributes must be outside the scope of a namespace definition):

// Enforce CLS compliance for all public types in this assembly.

[assembly:System.CLSCompliantAttribute(true)]

If you now add a bit of code that falls outside the CLS specification (such as an exposed point of unsigned data):

// Ulong types don't jibe with the CLS. public class Winnebago

{

public ulong notCompliant;

}

you are issued a compiler warning.

The Visual Studio 2008 AssemblyInfo.cs File

By default, Visual Studio 2008 projects receive a file named AssemblyInfo.cs, which can be viewed by expanding the Properties icon of the Solution Explorer (see Figure 16-9).

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

Figure 16-9. The AssemblyInfo.cs file

This file is a handy place to put attributes that are to be applied at the assembly level. You may recall from Chapter 15, during our examination of .NET assemblies, that the manifest contains assembly-level metadata, much of which comes from the assembly-level attributes shown in Table 16-4.

Table 16-4. Select Assembly-Level Attributes

Attribute

Meaning in Life

AssemblyCompanyAttribute

Holds basic company information

AssemblyCopyrightAttribute

Holds any copyright information for the product or

 

assembly

AssemblyCultureAttribute

Provides information on what cultures or languages the

 

assembly supports

AssemblyDescriptionAttribute

Holds a friendly description of the product or modules

 

that make up the assembly

AssemblyKeyFileAttribute

Specifies the name of the file containing the key pair used

 

to sign the assembly (i.e., establish a strong name)

AssemblyOperatingSystemAttribute

Provides information on which operating system the

 

assembly was built to support

AssemblyProcessorAttribute

Provides information on which processors the assembly

 

was built to support

AssemblyProductAttribute

Provides product information

AssemblyTrademarkAttribute

Provides trademark information

AssemblyVersionAttribute

Specifies the assembly’s version information, in the

 

format <major.minor.build.revision>

 

 

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

Reflecting on Attributes Using Early Binding

As mentioned in this chapter, an attribute is quite useless until some piece of software reflects over its values. Once a given attribute has been discovered, that piece of software can take

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

551

whatever course of action necessary. Now, like any application, this “other piece of software” could discover the presence of a custom attribute using either early binding or late binding. If you wish to make use of early binding, you’ll require the client application to have a compile-time definition of the attribute in question (VehicleDescriptionAttribute in this case). Given that the AttributedCarLibrary assembly has defined this custom attribute as a public class, early binding is the best option.

To illustrate the process of reflecting on custom attributes, create a new C# Console Application named VehicleDescriptionAttributeReader. Next, set a reference to the AttributedCarLibrary assembly. Finally, update your initial *.cs file with the following code:

// Reflecting on custom attributes using early binding. using System;

using AttributedCarLibrary;

public class Program

{

static void Main(string[] args)

{

//Get a Type representing the Winnebago.

Type t = typeof(Winnebago);

//Get all attributes on the Winnebago.

object[] customAtts = t.GetCustomAttributes(false);

// Print the description.

Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n");

foreach(VehicleDescriptionAttribute v in customAtts) Console.WriteLine("-> {0}\n", v.Description);

Console.ReadLine();

}

}

As the name implies, Type.GetCustomAttributes() returns an object array that represents all the attributes applied to the member represented by the Type (the Boolean parameter controls whether the search should extend up the inheritance chain). Once you have obtained the list of attributes, iterate over each VehicleDescriptionAttribute class and print out the value obtained by the Description property.

Source Code The VehicleDescriptionAttributeReader project is included under the Chapter 16 subdirectory.

Reflecting on Attributes Using Late Binding

The previous example made use of early binding to print out the vehicle description data for the Winnebago type. This was possible due to the fact that the VehicleDescriptionAttribute class type was defined as a public member in the AttributedCarLibrary assembly. It is also possible to make use of dynamic loading and late binding to reflect over attributes.

Create a new project called VehicleDescriptionAttributeReaderLateBinding and copy AttributedCarLibrary.dll to the project’s \bin\Debug directory. Now, update your Main() method as follows:

using System.Reflection;