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

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

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

414 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

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

}

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

//This time, we are using the AttributeUsage attribute

//to annotate our custom attribute.

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

public 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!

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

415

Assembly-Level (and Module-Level) Attributes

It is also possible to apply attributes on all types within a given module 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 jive with the CLS. public class Winnebago

{

public ulong notCompliant;

}

you are issued a compiler error.

The Visual Studio 2005 AssemblyInfo.cs File

By default, Visual Studio 2005 generates a file named AssemblyInfo.cs (see Figure 12-9).

Figure 12-9. The AssemblyInfo.cs file

This file is a handy place to put attributes that are to be applied at the assembly level. Table 12-4 lists some assembly-level attributes to be aware of.

Table 12-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

Continued

416 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

Table 12-4. (Continued)

Attribute

Meaning in Life

AssemblyKeyFileAttribute

Specifies the name of the file containing the key pair used

 

to sign the assembly (i.e., establish a shared 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 12 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 whatever course of action necessary. Now, like an 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();

}

}

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

417

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 application is included under the Chapter 12 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 so:

using System.Reflection;

namespace VehicleDescriptionAttributeReaderLateBinding

{

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Descriptions of Your Vehicles *****\n");

//Load the local copy of AttributedCarLibrary.

Assembly asm = Assembly.Load("AttributedCarLibrary");

//Get type info of VehicleDescriptionAttribute.

Type vehicleDesc = asm.GetType("AttributedCarLibrary.VehicleDescriptionAttribute");

//Get type info of the Description property.

PropertyInfo propDesc = vehicleDesc.GetProperty("Description");

//Get all types in the assembly.

Type[] types = asm.GetTypes();

//Iterate over each type and obtain any VehicleDescriptionAttributes. foreach (Type t in types)

{

object[] objs = t.GetCustomAttributes(vehicleDesc, false);

//Iterate over each VehicleDescriptionAttribute and print

//the description using late binding.

foreach (object o in objs)

{

Console.WriteLine("-> {0}: {1}\n", t.Name, propDesc.GetValue(o, null));

}

}

418 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

Console.ReadLine();

}

}

}

If you were able to follow along with the examples in this chapter, this Main() method should be (more or less) self-explanatory. The only point of interest is the use of the PropertyInfo.GetValue() method, which is used to trigger the property’s accessor. Figure 12-10 shows the output.

Source Code The VehicleDescriptionAttributeReaderLateBinding application is included under the Chapter 12 subdirectory.

Figure 12-10. Reflecting on attributes using late binding

Putting Reflection, Late Binding, and Custom

Attributes in Perspective

Even though you have seen numerous examples of these techniques in action, you may still be wondering when to make use of reflection, dynamic loading, late binding, and custom attributes in your programs. To be sure, these topics (while fascinating) can seem a bit on the academic side of programming (which may or may not be a bad thing, depending on your point of view). To help map these topics to a real-world situation, you need a solid example. Assume for the moment that you are on a programming team that is building an application with the following requirement:

• The product must be extendible by the use of additional third-party tools.

So, what exactly is meant by extendable? Consider Visual Studio 2005. When this application was developed, various “hooks” were inserted to allow other software vendors to snap custom modules into the IDE. Obviously, the Visual Studio 2005 team had no way to set references to external .NET assemblies it had not programmed (thus, no early binding), so how exactly would an application provide the required hooks?

First, an extendable application must provide some input vehicle to allow the user to specify the module to plug in (such as a dialog box or command-line flag). This requires dynamic loading.

Second, an extendable application must be able to determine if the module supports the correct functionality (such as a set of required interfaces) in order to be plugged into the environment. This requires reflection.

Finally, an extendable application must obtain a reference to the required infrastructure (e.g., the interface types) and invoke the members to trigger the underlying functionality. This often requires late binding.

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

419

Simply put, if the extendible application has been preprogrammed to query for specific interfaces, it is able to determine at runtime if the type can be activated. Once this verification test has been passed, the type in question may support additional interfaces that provide a polymorphic fabric to their functionality. This is the exact approach taken by the Visual Studio 2005 team, and despite what you may be thinking, is not at all difficult.

Building an Extendable Application

In the sections that follow, I will take you through a complete example that illustrates the process of building an extendible Windows Forms application that can be augmented by the functionality of external assemblies. What I will not do at this point is comment on the process of programming Windows Forms applications (Chapters 19, 20, and 21 will tend to that chore). So, if you are not familiar with the process of building Windows Forms applications, feel free to simply open up the supplied sample code and follow along (or build a console-based alternative). To serve as a road map, our extendible application entails the following assemblies:

CommonSnappableTypes.dll: This assembly contains type definitions that will be implemented by each snap-in as well as referenced by the extendible Windows Forms application.

CSharpSnapIn.dll: A snap-in written in C#, which leverages the types of

CommonSnappableTypes.dll.

VbNetSnapIn.dll: A snap-in written in Visual Basic .NET, which leverages the types of

CommonSnappableTypes.dll.

MyPluggableApp.exe: This Windows Forms application will be the entity that may be extended by the functionality of each snap-in. Again, this application will make use of dynamic loading, reflection, and late binding to dynamically gain the functionality of assemblies it has no prior knowledge of.

Building CommonSnappableTypes.dll

The first order of business is to create an assembly that contains the types that a given snap-in must leverage to be plugged into your expandable Windows Forms application. The CommonSnappableTypes class library project defines two types:

namespace CommonSnappableTypes

{

public interface IAppFunctionality

{

void DoIt();

}

[AttributeUsage(AttributeTargets.Class)]

public sealed class CompanyInfoAttribute : System.Attribute

{

private string companyName; private string companyUrl; public CompanyInfoAttribute(){}

public string Name

{

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

}

420 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

public string Url

{

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

}

}

}

The IAppFunctionality interface provides a polymorphic interface for all snap-ins that can be consumed by the extendible Windows Forms application. Of course, as this example is purely illustrative in nature, you supply a single method named DoIt(). To map this to a real-world example, imagine an interface (or a set of interfaces) that allows the snapper to generate scripting code, render an image onto the application’s toolbox, or integrate into the main menu of the hosting application.

The CompanyInfoAttribute type is a custom attribute that will be applied on any class type that wishes to be snapped in to the container. As you can tell by the definition of this class, [CompanyInfo] allows the developer of the snap-in to provide some basic details about the component’s point of origin.

Building the C# Snap-In

Next up, you need to create a type that implements the IAppFunctionality interface. Again, to focus on the overall design of an extendible application, a trivial type is in order. Assume a new C# code library named CSharpSnapIn that defines a class type named CSharpModule. Given that this class must make use of the types defined in CommonSnappableTypes, be sure to set a reference to this binary (as well as System.Windows.Forms.dll to display a noteworthy message). This being said, here is the code:

using System;

using CommonSnappableTypes; using System.Windows.Forms;

namespace CSharpSnapIn

{

[CompanyInfo(Name = "Intertech Training", Url = "www.intertechtraining.com")]

public class TheCSharpModule : IAppFunctionality

{

void IAppFunctionality.DoIt()

{

MessageBox.Show("You have just used the C# snap in!");

}

}

}

Notice that I choose to make use of explicit interface implementation when supporting the IAppFunctionality interface. This is not required; however, the idea is that the only part of the system that needs to directly interact with this interface type is the hosting Windows application.

Building the Visual Basic .NET Snap-In

Now, to simulate the role of a third-party vendor who prefers Visual Basic .NET over C#, create a new Visual Basic .NET code library (VbNetSnapIn) that references the same external assemblies as the previous CSharpSnapIn project. The code is (again) intentionally simple:

Imports System.Windows.Forms

Imports CommonSnappableTypes

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

421

<CompanyInfo(Name:="Chucky's Software", Url:="www.ChuckySoft.com")> _

Public Class VbNetSnapIn

Implements IAppFunctionality

Public Sub DoIt() Implements CommonSnappableTypes.IAppFunctionality.DoIt

MessageBox.Show("You have just used the VB .NET snap in!")

End Sub

End Class

Not too much to say here! Do notice, however, that applying attributes in the syntax of Visual Basic .NET requires angle brackets (< >) rather than square brackets ([ ]).

Building an Extendable Windows Forms Application

The final step is to create a new Windows Forms application (MyExtendableApp) that allows the user to select a snap-in using a standard Windows Open dialog box. Next, set a reference to the

CommonSnappableTypes.dll assembly, but not the CSharpSnapIn.dll or VbNetSnapIn.dll code libraries. Remember that the whole goal of this application is to make use of late binding and reflection to determine the “snapability” of independent binaries created by third-party vendors.

Again, I won’t bother to examine all the details of Windows Forms development at this point in the text. However, assuming you have placed a MenuStrip component onto the Form template, define a topmost menu item named Tools that provides a single submenu named Snap In Module (see Figure 12-11).

Figure 12-11. Initial GUI for MyExtendableApp

This Windows Form will also contain a ListBox type (which I renamed as lstLoadedSnapIns) that will be used to display the names of each snap-in loaded by the user. Figure 12-12 shows the final GUI.

422 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-12. Final GUI for MyExtendableApp

The code that handles the Tools Snap In Module menu item (which may be created simply by double-clicking the menu item from the design-time editor) displays a File Open dialog box and extracts the path to the selected file. This path is then sent into a helper function named LoadExternalModule() for processing. This method will return false when it is unable to find a class implementing IAppFunctionality:

private void snapInModuleToolStripMenuItem_Click(object sender, EventArgs e)

{

// Allow user to select an assembly to load.

OpenFileDialog dlg = new OpenFileDialog();

if (dlg.ShowDialog() == DialogResult.OK)

{

if (LoadExternalModule(dlg.FileName) == false) MessageBox.Show("Nothing implements IAppFunctionality!");

}

}

The LoadExternalModule() method performs the following tasks:

Dynamically loads the assembly into memory

Determines if the assembly contains a type implementing IAppFunctionality

If a type implementing IAppFunctionality is found, the DoIt() method is called, and the fully qualified name of the type is added to the ListBox (note that the for loop will iterate over all types in the assembly to account for the possibility that a single assembly has multiple snap-ins):

private bool LoadExternalModule(string path)

{

bool foundSnapIn = false; IAppFunctionality itfAppFx;

//Dynamically load the selected assembly.

Assembly theSnapInAsm = Assembly.LoadFrom(path);

//Get all types in assembly.

Type[] theTypes = theSnapInAsm.GetTypes();

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

423

// See if a type implement IAppFunctionality. for (int i = 0; i < theTypes.Length; i++)

{

Type t = theTypes[i].GetInterface("IAppFunctionality"); if (t != null)

{

foundSnapIn = true;

//Use late binding to create the type. object o =

theSnapInAsm.CreateInstance(theTypes[i].FullName);

//Call DoIt() off the interface.

itfAppFx = o as IAppFunctionality; itfAppFx.DoIt(); lstLoadedSnapIns.Items.Add(theTypes[i].FullName);

}

}

return foundSnapIn;

}

At this point, you can run your application. When you select the CSharpSnapIn.dll or VbNetSnapIn.dll assemblies, you should see the correct message displayed. Figure 12-13 shows one possible run.

Figure 12-13. Snapping in external assemblies

The final task is to display the metadata provided by the [CompanyInfo]. To do so, simply update LoadExternalModule() to call a new helper function named DisplayCompanyData() before exiting the if scope. Notice this method takes a single System.Type parameter.

private bool LoadExternalModule(string path)

{

...

if (t != null)

{

...

// Show company info. DisplayCompanyData(theTypes[i]);

}

}

return foundSnapIn;

}