Reflection

Reflection

You already know that managed applications are deployed as assemblies, that assemblies contain files that are usually (but not always) managed modules, and that managed modules contain types. You also know that every managed module has metadata inside it that fully describes the types defined in the module, and that assemblies carry additional metadata in their manifests identifying the files that make up the assembly and other pertinent information. And you ve seen how ILDASM can be used to inspect the contents of an assembly or managed module. Much of the information that ILDASM displays comes straight from the metadata.

The System.Reflection namespace contains types that you can use to access metadata without having to understand the binary metadata format. The term reflection means inspecting metadata to get information about an assembly, module, or type. The .NET Framework uses reflection to acquire important information at run time about the assemblies that it loads. Visual Studio .NET uses reflection to obtain IntelliSense data. The managed applications that you write can use reflection, too. Reflection makes the following operations possible:

  • Retrieving information about assemblies and modules and the types they contain

  • Reading information added to a compiled executable s metadata by custom attributes

  • Performing late binding by dynamically instantiating and invoking methods on types

Not every managed application uses reflection or has a need to use reflection, but reflection is something every developer should know about, for two reasons. First, learning about reflection deepens one s understanding of the .NET Framework. Second, reflection can be extraordinarily useful to certain types of applications. While far from exhaustive, the next few sections provide a working introduction to reflection and should at least enable you to hold your own when the conversation turns to reflection at a .NET party.

Retrieving Information About Assemblies, Modules, and Types

One use for reflection is to gather information at run time about assemblies, managed modules, and the types that assemblies and modules contain. The key classes that expose the functionality of the framework s reflection engine are

  • System.Reflection.Assembly, which represents assemblies

  • System.Reflection.Module, which represents managed modules

  • System.Type, which represents types

The first step in acquiring information from an assembly s metadata is to load the assembly. The following statement uses the static Assembly.LoadFrom method to load the assembly whose manifest is stored in Math.dll:

Assembly a = Assembly.LoadFrom ("Math.dll");

LoadFrom returns a reference to an Assembly object representing the loaded assembly. A related method named Load takes an assembly name rather than a file name as input. Once an assembly is loaded, you can use Assembly methods to retrieve all sorts of interesting information about it. For example, the GetModules method returns an array of Module objects representing the modules in the assembly. GetExportedTypes returns an array of Type objects representing the types exported from the assembly (in other words, the assembly s public types). GetReferencedAssemblies returns an array of AssemblyName objects identifying assemblies used by this assembly. And the GetName method returns an AssemblyName object that serves as a gateway to still more information encoded in the assembly manifest.

Figure 3-9 contains the source code listing for a console application named AsmInfo that, given the name of a file containing an assembly manifest, uses reflection to display information about the assembly. Included in the output is information indicating whether the assembly is strongly or weakly named, the assembly s version number, the managed modules that it consists of, the types exported from the assembly, and other assemblies containing types that this assembly references. When run on the weakly named version of the Math assembly (Math.dll) presented in Chapter 2, AsmInfo produces the following output:

Naming: Weak Version: 0.0.0.0 Modules math.dll simple.netmodule complex.netmodule Exported Types SimpleMath ComplexMath Referenced Assemblies mscorlib Microsoft.VisualBasic

You can plainly see the two types exported from the Math assembly (SimpleMath and ComplexMath) and the modules that make up the assembly (Math.dll, Simple.netmodule, and Complex.netmodule). Mscorlib appears in the list of referenced assemblies because it contains the core data types used by virtually all managed applications. Microsoft.VisualBasic shows up also because one of the modules in the assembly, Simple.netmodule, was written in Visual Basic .NET.

AsmInfo.cs

using System; using System.Reflection; class MyApp { static void Main (string[] args) { if (args.Length == 0) { Console.WriteLine ("Error: Missing file name"); return; } try { // Load the assembly identified on the command line Assembly a = Assembly.LoadFrom (args[0]); AssemblyName an = a.GetName (); // Indicate whether the assembly is strongly or // weakly named byte[] bytes = an.GetPublicKeyToken (); if (bytes == null) Console.WriteLine ("Naming: Weak"); else Console.WriteLine ("Naming: Strong"); // Display the assembly's version number
Figure 3-9

AsmInfo source code.

 Version ver = an.Version; Console.WriteLine ("Version: {0}.{1}.{2}.{3}", ver.Major, ver.Minor, ver.Build, ver.Revision); // List the modules that make up the assembly Console.WriteLine ("\nModules"); Module[] modules = a.GetModules (); foreach (Module module in modules) Console.WriteLine ("  " + module.Name); // List the types exported from the assembly Console.WriteLine ("\nExported Types"); Type[] types = a.GetExportedTypes (); foreach (Type type in types) Console.WriteLine ("  " + type.Name); // List assemblies referenced by the assembly Console.WriteLine ("\nReferenced Assemblies"); AssemblyName[] names = a.GetReferencedAssemblies (); foreach (AssemblyName name in names) Console.WriteLine ("  " + name.Name); } catch (Exception e) { Console.WriteLine (e.Message); } } }

If you d like to know even more about an assembly specifically, about the modules that it contains you can use the Module objects returned by Assembly.GetModules. Calling GetTypes on a Module object retrieves a list of types defined in the module all types, not just exported types. The following code sample writes the names of all the types defined in module to a console window:

Type[] types = module.GetTypes (); foreach (Type type in types) Console.WriteLine (type.FullName);

To learn even more about a given type, you can call GetMembers on a Type object returned by GetTypes. GetMembers returns an array of MemberInfo objects representing the type s individual members. MemberInfo.MemberType tells you what kind of member a MemberInfo object represents. MemberTypes.Field, for example, identifies the member as a field, while MemberTypes.Method identifies it as a method. A MemberInfo object s Name property exposes the member s name. Using these and other Type members, you can drill down as deeply as you want to into a type, even identifying the parameters passed to individual methods (and the methods return types) if you care to.

Using reflection to inspect the contents of managed executables is probably only interesting if you plan to write diagnostic utilities. But the fact that reflection exists at all leads to some other interesting possibilities, one of which is discussed in the next section.

Custom Attributes

One of the ground-breaking new language features supported by CLR-compliant compilers is the attribute. Attributes are a declarative means for adding information to metadata. For example, if you attribute a method this way and compile it without a DEBUG symbol defined, the compiler emits a token into the module s metadata noting that DoValidityCheck can t be called:

[Conditional ("DEBUG")] public DoValidityCheck () { ... }

If you later compile a module that calls DoValidityCheck, the compiler reads the metadata, sees that DoValidityCheck can t be called, and ignores statements that call it.

Attributes are instances of classes derived from System.Attribute. The FCL defines several attribute classes, including System.Diagnostics.ConditionalAttribute, which defines the behavior of the Conditional attribute. You can write attributes of your own by deriving from Attribute. The canonical example of a custom attribute is a CodeRevision attribute for documenting source code revisions. Revisions noted with source code comments a traditional method for documenting code revisions appear only in the source code. Revisions noted with attributes, however, are written into the compiled executable s metadata and can be retrieved through reflection.

Here s the source code for a custom attribute named CodeRevisionAttribute:

[AttributeUsage (AttributeTargets.All, AllowMultiple=true)] class CodeRevisionAttribute : Attribute { public string Author; public string Date; public string Comment; public CodeRevisionAttribute (string Author, string Date) { this.Author = Author; this.Date = Date; } }

The first statement, AttributeUsage, is itself an attribute. The first parameter passed to it, AttributeTargets.All, indicates that CodeRevisionAttribute can be applied to any element of the source code to classes, methods, fields, and so on. The second parameter permits multiple CodeRevisionAttributes to be applied to a single element. The remainder of the code is a rather ordinary class declaration. The class constructor defines CodeRevisionAttribute s required parameters. Public fields and properties in an attribute class can be used as optional parameters. Because CodeRevisionAttribute defines a public field named Comment, for example, you can include a comment string in a code revision attribute simply by prefacing the string with Comment=.

Here s an example demonstrating how CodeRevisionAttribute might be used:

[CodeRevision ("billg", "07-19-2001")] [CodeRevision ("steveb", "09-30-2001", Comment="Fixed Bill's bugs")] struct Point { public int x; public int y; public int z; }

Get the picture? You can attribute any element of your source code by simply declaring a CodeRevisionAttribute in square brackets. You don t have to include the word Attribute in the attribute name because the compiler is smart enough to do it for you.

Reflection is important to developers who write (or use) custom attributes because it is through reflection that an application reads information added to its (or someone else s) metadata via custom attributes. The following code sample enumerates the code revision attributes attached to type Point and writes them to the console window. Enumeration is made possible by MemberInfo.GetCustomAttributes, which reads the custom attributes associated with any element that can be identified with a MemberInfo object:

MemberInfo info = typeof (Point); object[] attributes = info.GetCustomAttributes (false); if (attributes.Length > 0) { Console.WriteLine ("Code revisions for Point struct"); foreach (CodeRevisionAttribute attribute in attributes) { Console.WriteLine ("\nAuthor: {0}", attribute.Author); Console.WriteLine ("Date: {0}", attribute.Date); if (attribute.Comment != null) Console.WriteLine ("Comment: {0}", attribute.Comment); } }

And here s the output when this code is run against the Point struct shown above:

Code revisions for Point struct Author: billg Date: 07-19-2001 Author: steveb Date: 09-30-2001 Comment: Fixed Bill's bugs

Writing a reporting utility that lists all the code revisions in a compiled executable wouldn t be difficult because types and type members are easily enumerated using the reflection techniques described in the previous section.

Dynamically Loading Types (Late Binding)

A final use for reflection is to dynamically load types and invoke methods on them. Dynamic loading means binding to a type at run time rather than compile time. Let s say your source code references a type in another assembly, like this:

Rectangle rect = new Rectangle ();

Here you re practicing early binding because your compiler inserts data into the resulting executable, noting that a type named Rectangle is imported from another assembly. Late binding gives you the ability to use a type without embedding references to it in your metadata. Late binding is accomplished by using reflection.

One use for late binding is to facilitate plug-ins. Suppose you want to enable third-party developers to extend your application by contributing images to the splash screen your app displays when it starts up. Because you don t know in advance what plug-ins might be present at startup, you can t early bind to classes in the plug-ins. But you can late bind to them. If you instruct third-party developers to build classes named PlugIn, and if each Plug In class contains a method named GetImage that returns an image to the caller, the following code calls GetImage on each plug-in represented in the list of assembly names in the names array:

ArrayList images = new ArrayList (); foreach (string name in names) { Assembly a = Assembly.Load (name); Type type = a.GetType ("PlugIn"); MethodInfo method = type.GetMethod ("GetImage"); Object obj = Activator.CreateInstance (type); Image image = (Image) method.Invoke (obj, null); images.Add (image); }

At the end, the ArrayList named images holds an array of Image objects representing the images obtained from the plug-ins.

Visual Basic .NET uses late binding to interact with variables whose declared type is Object. Late binding is an important part of the .NET Framework architecture and something you should be aware of even if you don t use it.



Programming Microsoft  .NET
Applied MicrosoftNET Framework Programming in Microsoft Visual BasicNET
ISBN: B000MUD834
EAN: N/A
Year: 2002
Pages: 101

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net