Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1
.pdfC H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G |
739 |
as declarative tags that are written to an assembly at compile time to annotate or mark up a class and/or its members so that class and/or its members can be later extracted at runtime, possibly to change its normal behavior.
To add an attribute to a class or its members, you add code in front of the element you want to annotate with the following syntax:
[AttributeName(ConstructorArguments, optionalpropertyname=value)]
If you want to add more than one attribute, you simply add more than one attribute within the square brackets, delimited by commas:
[Attribute1(), Attribute2()]
An important feature to you (other than the changed behavior caused by the .NET Framework attributes) is that you can access attributes using reflection. A more important feature is that you can create your own custom attributes.
Creating a Custom Attribute
According to the Microsoft documentation, a custom attribute is just a class that is derived from the System::Attribute class with a few minor additional criteria.
The additional criteria are as follows:
•The custom attribute class needs to be public.
•By convention, the attribute name should end in “Attribute”. A neat thing is that when you implement the attribute, you don’t have to add the trailing “Attribute”, as it’s automatically added. In other words, as you saw in Chapter 15, WebMethod and WebMethodAttribute are the same.
•There’s an additional AttributeUsageAttribute that you can apply to your custom attribute.
•All properties that will be written to the metadata need to be public.
•The properties available to be written to the metadata are restricted to Integer type (Byte, Int32, and so on), floating point (Single or Double), Char, String, Boolean, or Enum. Note that this means the very common DateTime data type isn’t supported. (I show you how to get around this limitation later in this chapter.)
Of all the additional criteria, the only one you need to look at in more detail is the AttributeUsageAttribute attribute. This attribute controls the manner in which the custom attribute is used. To be more accurate, it defines three behaviors: which data types the custom attribute is valid on, if the custom attribute is inherited, and whether more than one of the custom attributes can be applied to a single data type.
You can specify that the custom attribute can be applied to any assembly entity (see Table 18-4) by giving the AttributeUsageAttribute attribute an AttributeTargets::All value. On the other hand, if you want to restrict the custom attribute to a specific type or a combination of types, then you would specify one or a combination (by ORing) of the AttributeTargets enumerations in Table 18-4.
Table 18-4. AttributeTargets Enumeration
All |
Assembly |
Class |
Constructor |
Delegate |
Enum |
Event |
Field |
Interface |
Method |
Module |
Parameter |
Property |
ReturnValue |
Struct |
|
|
|
|
|
740 C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G
The second parameter of the AttributeUsageAttribute attribute specifies whether any class that inherits from a class that implements the custom attribute inherits that custom attribute. The default is that a class does inherit the custom attribute.
The final parameter allows a custom attribute to be applied more than one time to a single type. The default is that only a single custom attribute can be applied.
There are three ways that you can have data passed into the attribute when implementing. The first is by the custom attribute’s construction. The second is by a public property. The third is by a public member variable.
Listing 18-3 and Listing 18-4 show the creation of two custom documentation attributes. The first is the description of the element within the class, and the second is a change history. By nature you should be able to apply both of these attributes to any type within a class and you should also have the attributes inherited. These attributes mostly differ in that a description can be applied only once to an element in a class, whereas the change history will be used repeatedly.
Listing 18-3. Documentation Custom Attributes Definition
using namespace System::Reflection;
namespace Documentation
{
[AttributeUsage(AttributeTargets::All, Inherited=true, AllowMultiple=false)] public ref class DescriptionAttribute : public Attribute
{
String ^mAuthor; DateTime mCompileDate; String ^mDescription;
public:
DescriptionAttribute(String ^Author, String ^Description);
property String^ Author { String^ get(); } property String^ Description { String^ get(); } property String^ CompileDate { String^ get(); }
};
[AttributeUsage(AttributeTargets::All, Inherited=true, AllowMultiple=true)] public ref class HistoryAttribute : public Attribute
{
String ^mAuthor; DateTime mModifyDate; String ^mDescription;
public:
HistoryAttribute(String ^Author, String ^Description);
property String^ Author { String^ get(); } property String^ Description { String^ get(); } property String^ ModifyDate
{
String^ get();
void set(String^ value);
}
};
}
C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G |
741 |
Listing 18-4. Documentation Custom Attributes Implemenation
#include "Documentation.h"
namespace Documentation
{
// ------------- |
DescriptionAttribute ------------------- |
DescriptionAttribute::DescriptionAttribute(String ^Author, String ^Description)
{
mAuthor = Author; mDescription = Description; mCompileDate = DateTime::Now;
}
String^ DescriptionAttribute::Author::get()
{
return mAuthor;
}
String^ DescriptionAttribute::Description::get()
{
return mDescription;
}
String^ DescriptionAttribute::CompileDate::get()
{
return mCompileDate.ToShortDateString();
}
// ------------- |
HistoryAttribute ------------------- |
HistoryAttribute::HistoryAttribute(String ^Author, String ^Description)
{
mAuthor = Author; mDescription = Description; mModifyDate = DateTime::Now;
}
String^ HistoryAttribute::Author::get()
{
return mAuthor;
}
String^ HistoryAttribute::Description::get()
{
return mDescription;
}
String^ HistoryAttribute::ModifyDate::get()
{
return mModifyDate.ToShortDateString();
}
742 C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G
void HistoryAttribute::ModifyDate::set(String ^value)
{
mModifyDate = Convert::ToDateTime(value);
}
}
As you can see by the code, other than the [AttributeUsage] attribute (which is inherited from System::Attribute), there is nothing special about these classes. They are simply classes with a constructor and a few public properties and private member variables.
The only thing to note is the passing of dates in the form of a string, which are then converted to DateTime structure. Attributes are not allowed to pass the DateTime structure as pointed out previously, so this simple trick fixes this problem.
Implementing a Custom Attribute
As you can see in the example shown in Listing 18-5, you implement custom attributes in the same way as you do .NET Framework attributes. In this example, the DescriptionAttribute attribute you created earlier is applied to two classes, a constructor, a member method, and a property. Also, the HistoryAttribute attribute is applied twice to the first class and then later to the property.
Listing 18-5. Implementing the Description and History Attributes
using namespace System;
using namespace Documentation;
namespace DocTestLib
{
[Description("Stephen Fraser",
"This is TestClass1 to test the documentation Attribute.")] [History("Stephen Fraser", "Original Version.", ModifyDate="11/27/02")] [History("Stephen Fraser", "Added DoesNothing Method to do nothing.")] public ref class TestClass1
{
public:
[Description("Stephen Fraser",
"This is default constructor for TextClass1.")] TestClass1() {}
[Description("Stephen Fraser",
"This is method does nothing for TestClass1.")] void DoesNothing() {}
[Description("Stephen Fraser", "Added Variable property.")] [History("Stephen Fraser", "Removed extra CodeDoc Attribute")] property String^ Variable;
};
[Description("Stephen Fraser",
"This is TestClass2 to test the documentation Attribute.")] public ref class TestClass2
{
};
}
C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G |
743 |
Notice in Listing 18-5 that “Attribute” is stripped off the end of the attributes. This is optional, and it is perfectly legal to keep “Attribute” on the attribute name.
Another thing that you might want to note is how to implement a named property to an attribute. This is done in the first use of the History attribute where I specify the date that the change was made:
[History("Stephen Fraser", "Original Version.", ModifyDate="11/27/02")]
The modified date is also a string and not a DateTime as you would expect. This is because (as I pointed out previously) it is not legal to pass a DateTime to an attribute.
Using a Custom Attribute
You looked at how to use custom attributes when you learned about reflection. Custom attributes are just placed as metadata onto the assembly and, as you learned in reflection, it is possible to examine an assembly’s metadata.
The only new thing about assembly reflection and custom attributes is that you need to call the
GetCustomAttribute() method to get a specific custom attribute or the GetCustomAttributes() method to get all custom attributes for a specific type.
The tricky part with either of these two methods is that you have to typecast them to their appropriate type, as both return an Object type. What makes this tricky is that you need to use the full name of the attribute or, in other words, unlike when you implemented it, you need the “Attribute” suffix added. If you created a custom attribute that doesn’t end in “Attribute” (which is perfectly legal, I might add), then this won’t be an issue.
Both of these methods have a few overloads, but they basically break down to one of three syntaxes. To get all custom attributes:
public: Object ^GetCustomAttributes(Boolean useInhertiance); // For example:
array <Object^>^ CustAttr = info->GetCustomAttributes(true);
To get all of a specific type of custom attribute:
public: Object ^GetCustomAttributes(Type ^type, Boolean useInhertiance); // For example:
array <Object^>^CustAttr = info->GetCustomAttributes(HistoryAttribute::typeid, true);
Or to get a specific attribute for a specific type reference:
public: static Attribute^ GetCustomAttribute(ReflectionReference^, Type^); // For Example
Attribute ^attribute =
Attribute::GetCustomAttribute(methodInfo, DescriptionAttribute::typeid);
■Caution If the type allows multiple custom attributes of a single type to be added to itself, then the
GetCustomAttribute() method returns an Array and not an Attribute.
Listing 18-6 is really nothing more than another example of assembly reflection, except this time it uses an additional GetCustomAttribute() and GetCustomAttributes() method. The example simply walks through an assembly that you passed to it and displays information about any class, constructor, method, or property that is found within it. Plus, it shows any custom Description or History attributes that you may have added.
744 C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G
Listing 18-6. Using Custom Attributes to Document Classes
using namespace System; using namespace Reflection;
using namespace Documentation;
void DisplayDescription(Attribute ^attr)
{
if (attr != nullptr)
{
DescriptionAttribute ^cd = (DescriptionAttribute^)attr;
Console::WriteLine(" |
Author: {0} -- Compiled: {1}", |
cd->Author, cd->CompileDate); |
|
Console::WriteLine(" Description: {0}", cd->Description); |
|
Console::WriteLine(" |
---- Change History ----"); |
} |
|
else |
|
Console::WriteLine(" |
No Documentation"); |
}
void DisplayHistory(array<Object^>^ attr)
{
if (attr->Length > 0)
{
for each (HistoryAttribute^ cd in attr)
{
Console::WriteLine(" Author: {0} -- Modified: {1}", cd->Author, cd->ModifyDate);
Console::WriteLine(" Description: {0}", cd->Description);
}
}
else
Console::WriteLine(" No changes");
}
void DisplayAttributes(MemberInfo ^info)
{
DisplayDescription(Attribute::GetCustomAttribute(info,
DescriptionAttribute::typeid)); DisplayHistory(info->GetCustomAttributes(HistoryAttribute::typeid, true));
}
void PrintClassInfo(Type ^type)
{
Console::WriteLine("Class: {0}", type->ToString()); DisplayAttributes(type);
array<ConstructorInfo^>^ constructors = type->GetConstructors(); for (int i = 0; i < constructors->Length; i++)
{
Console::WriteLine("Constructor: {0}", constructors[i]->ToString()); DisplayAttributes(constructors[i]);
}
C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G |
745 |
array <MethodInfo^>^ methods = type->GetMethods((BindingFlags) (BindingFlags::Public|BindingFlags::Instance|BindingFlags::DeclaredOnly));
for (int i = 0; i < methods->Length; i++)
{
Console::WriteLine("Method: {0}", methods[i]->ToString()); DisplayAttributes(methods[i]);
}
array<PropertyInfo^>^ properties = type->GetProperties((BindingFlags) (BindingFlags::Public|BindingFlags::Instance|BindingFlags::DeclaredOnly));
for (int i = 0; i < properties->Length; i++)
{
Console::WriteLine("Property: {0}", properties[i]->ToString()); DisplayAttributes(properties[i]);
}
}
void main(array<System::String ^> ^args)
{
try
{
Assembly ^assembly = Assembly::LoadFrom(args[0]);
array<Type^>^ types = assembly->GetTypes();
for (int i = 0; i < types->Length; i++)
{
PrintClassInfo(types[i]);
Console::WriteLine();
}
}
catch(System::IO::FileNotFoundException^)
{
Console::WriteLine("Can't find assembly: {0}\n", args[0]);
}
}
One thing that this example has that the previous reflection example doesn’t is the use of the BindingFlags enumeration. The BindingFlags enum specifies the way in which the search for members and types within an assembly is managed by reflection. In the preceding example I used the following flags:
BindingFlags::Public | BindingFlags::Instance | BindingFlags::DeclaredOnly
This combination of flags specified that only public instance members that have only been declared at the current level (in other words, not inherited) will be considered in the search.
Also notice that even though the DisplayAttributes() method is called with a parameter of type Type, ConstructorInfo, MethodInfo, or PropertyInfo, it is declared using a parameter of type MemberInfo. The reason this is possible is because all the previously mentioned classes inherit from the MemberInfo class.
Figure 18-3 shows DocumentationWriter.exe in action. The dates in Figure 18-3 are based on when I compiled the assembly and most likely will differ from your results.
746 C H A P T E R 1 8 ■ A S S E M B L Y P R O G R A M M I N G
Figure 18-3. The DocumentationWriter program in action
Shared Assemblies
Up until now you have been developing only private assemblies. In other words, you have been developing assemblies that are local to the application and that can be accessed only by the application. In most cases, private assemblies will be all you really need to develop. But what happens if you have multiple applications that share a common assembly? You could make a copy of the assembly and copy it to each application’s directory. Or you could use the second type of assembly, a shared assembly.
Shared assemblies are accessible to any program that is run on the same machine where the assembly resides. By the way, you work with shared assemblies whenever you use any of the classes or any other data type of the .NET Framework. This seems logical, as every .NET application shares these assemblies.
The Global Assembly Cache
Unlike private assemblies, shared assemblies are placed in a common directory structure known as the global assembly cache (GAC). If and when you go looking for the GAC, you will find it off of your <WINDIR> (Windows or Windows NT) directory, in a subdirectory aptly called assembly.
When you open the assembly directory in Windows Explorer, it has the appearance of being one big directory made up of many different assemblies (see Figure 18-4). In reality, the assembly directory has a complex directory structure that gets hidden (thankfully) by Windows Explorer.