Custom Attributes
Chapter 5 introduced the concept of attributes, which have already appeared in several examples. In this chapter we used the Serializable and Synchronization attributes, which are provided by .NET Framework classes. The .NET Framework makes the attribute mechanism entirely extensible, allowing you to define custom attributes, which can be added to the class's metadata. This custom metadata is available through reflection and can be used at runtime. To simplify the use of custom attributes, you may declare a base class to do the work of invoking the reflection API to obtain the metadata information.
The example CustomAttribute illustrates the custom attribute InitialDirectory. InitialDirectory controls the initial current directory where the program runs. By default, the current directory is the directory containing the solution, which in this case is C:\OI\NetCpp\Chap08\CustomAttribute.
Using a Custom Attribute
Before we discuss implementing the custom attribute, let us look at how the InitialDirectory attribute is used. To be able to control the initial directory for a class, we derive the class from the base class DirectoryContext. We may then apply to the class the attribute InitialDirectory, which takes a String* parameter giving a path to what the initial directory should be. The property DirectoryPath extracts the path from the metadata. If our class does not have the attribute applied, this path will be the default. Here is the code for our test program.
When you run this sample on your system, you can change the directory in the attribute to one that exists on your machine.
//AttributeDemo.h using namespace System; using namespace System::IO; __gc class Normal : public DirectoryContext { }; [InitialDirectory("C:\\OI\\NetCpp\\Chap08")] __gc class Special : public DirectoryContext { }; public __gc class AttributeDemo { public: static void Main() { Normal *objNormal = new Normal; Console::WriteLine( "path = {0}", objNormal->DirectoryPath); ShowDirectoryContents(objNormal->DirectoryPath); Special *objSpecial = new Special; Console::WriteLine( "path = {0}", objSpecial->DirectoryPath); ShowDirectoryContents(objSpecial->DirectoryPath); } private: static void ShowDirectoryContents(String *path) { DirectoryInfo *dir = new DirectoryInfo(path); FileInfo *files[] = dir->GetFiles(); Console::WriteLine("Files:"); IEnumerator *pEnum = files->GetEnumerator(); while (pEnum->MoveNext()) { FileInfo *f = dynamic_cast<FileInfo *>(pEnum->Current); Console::WriteLine(" {0}", f->Name); } DirectoryInfo *dirs [] = dir->GetDirectories(); Console::WriteLine("Directories:"); pEnum = dirs->GetEnumerator(); while (pEnum->MoveNext()) { DirectoryInfo *d = dynamic_cast<DirectoryInfo *>(pEnum->Current); Console::WriteLine(" {0}", d->Name); } } };
Here is the output:
path = c:\OI\NetCpp\Chap08\CustomAttribute Files: CustomAttribute.vcproj CustomAttribute.ncb ReadMe.txt CustomAttribute.cpp AssemblyInfo.cpp stdafx.cpp stdafx.h CustomAttribute.sln CustomAttribute.suo AttributeDemo.h DirectoryContext.h DirectoryAttribute.h Directories: Debug path = C:\OI\NetCpp\Chap08 Files: Directories: Reflection Dynamic FileIO Serialization Hotel ISerialization Threading PulseAll ThreadIsolation AppDomain Asynch AsynchThreading CustomAttribute MarshalByReference Remoting
Defining an Attribute Class
To create a custom attribute, you must define an attribute class, derived from the base class Attribute. By convention, give your class a name ending in "Attribute." The name of your class without the "Attribute" suffix will be the name of the custom attribute. In our example the class name is InitialDirectoryAttribute, so the attribute's name is InitialDirectory.
You may provide one or more constructors for your attribute class. The constructors define how to pass positional parameters to the attribute (provide a parameter list, separated by commas). It is also possible to provide "named parameters" for a custom attribute, where the parameter information will be passed using the name=value syntax.
You may also provide properties to read the parameter information. In our example, we have a property Path, which is initialized in the constructor.
//DirectoryAttribute.h using namespace System; public __gc class InitialDirectoryAttribute : public Attribute { private: String *path; public: InitialDirectoryAttribute(String *path) { this->path = path; } __property String *get_Path() { return path; } };
Defining a Base Class
The last step in working with custom attributes is to provide a means to extract the custom attribute information from the metadata using the reflection classes. You can obtain the Type of any object by calling the method GetType, which is provided in the root class Object. Using the class's method GetCustomAttributes you can read the custom attribute information.
To make the coding of the client program as simple as possible, it is often useful to provide a base class that does the work of reading the custom attribute information.18 We provide a base class DirectoryContext, which is used by a class wishing to take advantage of the InitialDirectory attribute. This base class provides the property DirectoryPath to return the path information stored in the metadata. Here is the code for the base class:
//DirectoryContext.h using namespace System; using namespace System::Reflection; using namespace System::IO; using namespace System::Collections; __gc class DirectoryContext { public: __property String *get_DirectoryPath() { Type *t = this->GetType(); IEnumerator *pEnum = t->GetCustomAttributes(true)->GetEnumerator(); while (pEnum->MoveNext()) { Attribute *a = dynamic_cast<Attribute *>(pEnum->Current); InitialDirectoryAttribute *da = dynamic_cast<InitialDirectoryAttribute *>(a); if (da != 0) { return da->Path; } } return Directory::GetCurrentDirectory(); } };
We must import the System::Reflection namespace. GetType returns the current Type object, and we can then use the GetCustomAttributes method to obtain a collection of Attribute objects from the metadata. Since this collection is heterogeneous, consisting of different types, the dynamic_cast operator is used to test if a given collection element is of the type InitialDirectoryAttribute. If we find such an element, we return the Path property. Otherwise, we return the default current directory, obtained from GetCurrentDirectory.