|
|
|
|
VB.NET Language in a Nutshell, 2nd EditionBy Steven Roman, Ron Petrusha, Paul LomaxSecond Edition April 2002 0-596-00308-0, Order Number: 3080 682 pages, $44.95 US $69.95 CA |
Chapter 8
AttributesAttributes are declarative tags that can be used to annotate types or class members, thereby modifying their meaning or customizing their behavior. This descriptive information provided by the attribute is stored as metadata in a .NET assembly and can be extracted either at design time or at runtime using reflection.
To see how attributes might be used, consider the
<WebMethod>attribute, which might appear in code as follows:<WebMethod(Description:="Indicates the number of visitors to a page")> _Public Function PageHitCount(strULR As String) As IntegerOrdinarily, public methods of a class can be invoked locally from an instance of that class; they are not treated as members of a web service. In contrast, the
<WebMethod>attribute marks a method as a function callable over the Internet as part of a web service. This<WebMethod>attribute also includes a single property, Description, which provides the text that will appear in the page describing the web service.You may wonder why attributes are used on the .NET platform and why they are not simply implemented as language elements. The answer comes from the fact that attributes are stored as metadata in an assembly, rather than as part of its executable code. As an item of metadata, the attribute describes the program element to which it applies and is available through reflection both at design time (if a graphical environment such as Visual Studio .NET is used), at compile time (when the compiler can use it to modify, customize, or extend the compiler's basic operation), and at runtime (when it can be used by the Common Language Runtime or by other executable code to modify the code's ordinary runtime behavior).
The behavior of interface objects (i.e., controls) in Visual Studio .NET illustrates the importance of attributes. Since Visual Studio offers drag-and-drop placement of controls on forms or web pages, it is necessary for controls to have a design time behavior in addition to their runtime behavior. For instance, when you double click on a control in a designer, you ordinarily want the code or the code template for its default event handler to be displayed. Note that the question posed here is not how the control should respond to a double-click event, since the DoubleClick event occurs at runtime and, if an event handler is present, causes that event handler's executable code to be executed. Because we're concerned with the standard behavior of a control in its design time environment, an attribute provides an excellent solution. Indeed, the .NET Framework provides the
<DefaultEvent>attribute, which allows you to define a control's default event. Since information on the attribute is stored in the assembly's metadata, Visual Studio can simply look to see whether a<DefaultEvent>attribute is attached to a particular control when it is double-clicked in a designer window.The attribute-based system of programming implemented in .NET is extensible. In addition to the attributes predefined by Visual Basic or by the .NET Framework, you can define custom attributes that you apply to program elements. For an attribute to be meaningful, there must also be code that attempts to detect the presence of the attribute at design time, at compile time, or at runtime, and accordingly that performs an action dictated by the attribute's presence.
This chapter discusses the syntax and use of attributes, and then shows how to define and use custom attributes.
Syntax and Use
In Visual Basic, an attribute appears within angle brackets (a less-than (<) and a greater-than symbol (>)). The attribute name is followed by parentheses, which are used to enclose arguments that might be passed to the attribute. For example, the
<Obsolete>attribute marks a type or type member as obsolete. We can apply<Obsolete>as a parameter-less attribute as follows:<Obsolete( )>If no arguments are assigned to the attribute, we can omit the trailing parentheses:
<Obsolete>If more than one attribute is applied to a single program element, the attributes are enclosed in a single set of angle brackets and delimited from one another by a comma. For example:
<Obsolete(), WebMethod( )> Public Function PageCount( _strURL As String) As IntegerEach attribute corresponds to a class derived from System.Attribute. (In fact, the VB.NET compiler actually treats an attribute as an instance of the attribute's class.) By convention, we drop the trailing string "Attribute" from the class name to form the attribute name, although the attribute name can also be identical to the class name. Thus, for example, the
<WebMethod>attribute corresponds to the WebMethodAttribute class in the System.Web.Services namespace, which in turn is found in System.Web.Services.dll. Alternately, you can also specify the attribute as<WebMethodAttribute>. If the namespace containing the attribute class is not automatically accessible to the Visual Basic compiler or to Visual Studio, theImportsdirective should be used, and a reference should be added to the project either using the References dialog in Visual Studio or the/rswitch in the command-line compiler.TIP: If the shortened attribute name is a Visual Basic .NET keyword, use an attribute name that's identical to the attribute's class name to prevent a compiler error. For example, the following declaration produces an error because
ParamArrayis a VB.NET keyword:
<ParamArray( )> lScores As Long)NOTE: However, the following code compiles correctly:
<ParamArrayAttribute( )> lScores As Long)The attribute class constructor or constructors determine whether any arguments are required. For example, the
<VBFixedString>attribute corresponds to the VBFixedStringAttribute class, which has the following constructor:New(ByVal Size As Integer)Hence, the
<VBFixedString>attribute can be used as follows:<VBFixedString(10)> Private sID As StringTIP: Attribute constructors can be overloaded. Any required arguments must correspond to those expected by one of the constructors in number and data type.
Required arguments must be supplied to the attribute as positional arguments only ; named arguments are not accepted. A comma separates all arguments, whether named or positional.
Optional arguments correspond to class properties and can be supplied to the attribute as named arguments. For example, in addition to its constructor, which indicates to what language elements the attribute applies, the
<AttributeUsage>attribute, which is used to define the language elements to which a custom attribute applies, has a Boolean property, Inherited, that indicates whether the attribute is inherited by derived classes and overridden members. Its default value isTrue. To set it toFalse, you could use the attribute as follows:<AttributeUsage(AttributeTargets.Class, Inherited:=False)> _Public Class MyCustomClassBe sure to recognize that attributes are evaluated at compile time, when their data is written to the assembly's metadata. This means that only literal values can be passed as arguments to the attribute's constructor.
Unless it has a modifier, an attribute immediately precedes the language element to which it applies and must be on the same logical line as that language element. If they are on different lines, the Visual Basic .NET line continuation character (the underscore, or
_) must be used. This syntax is valid for attributes applied to the following language elements:Class
Constructor
Delegate
Enum
Event
Field
Interface
Method
Parameter
Property
Return Value
StructureFor example, the following
Classstatement illustrates this general usage of an attribute:<AttributeUsage(AttributeTargets.All)> _Public Class MyCustomAttrAttributeThe following statement indicates how attributes are used with parameter declarations:
Public Sub MyFunction(strName As String, _<ParamArrayAttribute( )> lValues As Long)There are two exceptions to this rule. Some attributes must be prefaced with a modifier (either
Assembly:orModule:) indicating the program element to which the attribute applies. In that case, the attribute must be placed at the top of the source file (i.e., immediately following anyOptionandImportsstatements), along with any other attributes that require a modifier. This syntax is valid for an attribute applied to an assembly or a module only.Option Strict OnImports System.Data.SqlClient<Assembly: AssemblyDescription("Supplementary data access library")>Namespace SqlAccessDefining a Custom Attribute
An attribute is merely a class that inherits from System.Attribute, which makes it very easy to implement a custom attribute. In this section, we'll build a custom attribute called
<DeveloperNote>, which allows a developer to add assorted information (the developer's name, the date, a comment, and whether a code modification was a response to a bug) to code. The steps are as follows:
- Define a public class that inherits from System.Attribute or another attribute class derived from System.Attribute. For example:
Public Class DeveloperNoteAttributeInherits System.AttributeNote that, by convention, the name of the class ends with the substring "Attribute".
- Apply the
<AttributeUsage>attribute, which defines the language elements to which the custom attribute can be applied, to the class (as shown in the following code fragment). The attribute's only required argument is one of the following members of theAttributeTargetsenumeration:
AllAssemblyClassConstructorDelegateEnumEventFieldInterfaceMethodModuleParameterPropertyReturnValueStructIf an attribute applies to multiple programming elements, but not all elements, the relevant constants can be
ORed together. In the case of our<DeveloperNote>attribute, we want the attribute to apply to all program elements. In addition, we want to make the<DeveloperNote>attribute extensible through inheritance, so we set the<AttributeTarget>attribute's Inherited argument toTrue. Finally, we want to allow the application of multiple attributes to the same program element; hence, we want to set the AllowMultiple argument toTrueas well. In view of this setting, our code should look as follows:
<AttributeUsage(AttributeTargets.All, _Inherited:=True, _AllowMultiple:=True)> _Public Class DeveloperNoteAttributeInherits System.Attribute- Create the class constructor (the New subroutine), which is called when the attribute is applied to a particular language element. The class constructor defines the attribute's required or positional arguments. At a minimum, we'll want a developer to record his or her name, a comment, and the date. Our constructor appears as follows:
Public Sub New(Name As String, Comment As String, _DateRecorded As String)MyBase.New( )strName = NamestrComment = CommentdatDate = CDate(DateRecorded)End SubNote that the date is passed to the constructor as a String type. There is some restriction on the data types that can be used as attribute parameters. Parameters can be any integral data type (Byte, Short, Integer, Long) or floating point data type (Single and Double), as well as Char, String, Boolean, an enumerated type, or System.Type. Thus, Date, Decimal, Object, and structured types cannot be used as parameters.
Each required parameter also corresponds to a class property or field. These parameters are added to the class in the next step.
- Declare properties or fields. The attribute's public properties and fields correspond both to parameters required by the class constructor and to optional parameters supplied when the attribute is applied to a language element. In the case of our attribute, we'll want properties that correspond to each attribute, as well as an additional Bugs property that indicates whether or not the comment corresponds to a code modification that resulted from a bug. The code is:
Public Property Name As StringGetReturn strNameEnd GetSetstrName = ValueEnd SetEnd PropertyPublic Property Comment As StringGetReturn strCommentEnd GetSetstrComment = ValueEnd SetEnd PropertyPublic Property DateRecorded As DateGetReturn datDateEnd GetSetdatDate = ValueEnd SetEnd PropertyPublic Property Bug As BooleanGetReturn blnBugEnd GetSetblnBug = ValueEnd SetEnd PropertyThe complete code for the attribute class is shown in Example 8-1.
Example 8-1: The DeveloperNoteAttribute attribute class
Option Strict OnImports SystemNamespace Extensions.CustomAttributes<AttributeUsage(AttributeTargets.All, _Inherited:=True, _AllowMultiple:=True)> _Public Class DeveloperNoteAttributeInherits System.AttributeProtected strName, strComment As StringProtected blnBug As BooleanProtected datDate As DatePublic Sub New(Name As String, Comment As String, DateRecorded As String)MyBase.New( )strName = NamestrComment = CommentdatDate = CDate(DateRecorded)End SubPublic Property Name As StringGetReturn strNameEnd GetSetstrName = ValueEnd SetEnd PropertyPublic Property Comment As StringGetReturn strCommentEnd GetSetstrComment = ValueEnd SetEnd PropertyPublic Property DateRecorded As DateGetReturn datDateEnd GetSetdatDate = ValueEnd SetEnd PropertyPublic Property Bug As BooleanGetReturn blnBugEnd GetSetblnBug = ValueEnd SetEnd PropertyEnd ClassEnd NamespaceUsing a Custom Attribute
The Visual Basic compiler and .NET platform automatically recognize the meaning of the attributes based on attribute classes in the .NET Framework Class Library. This recognition isn't true, however, for custom attributes. Thus, not only must you define them, you must also develop a set of routines that will identify the presence of an attribute so your code can handle them.
NET assemblies are self-describing; when the compiler creates the .NET assembly, it writes metadata describing the assembly and its classes and methods to the assembly manifest. This metadata is then accessed programmatically at runtime by using the .NET Framework's reflection classes.
TIP: An assembly's metadata is similar to a COM type library. In addition to their greater accessibility through .NET Framework APIs, assembly metadata is always stored along with the assembly. In contrast, although a type library can be stored in the EXE or DLL containing the COM object (as did previous versions of Visual Basic), it is most commonly stored in a file different from the file containing the COM objects it describes.
The .NET Framework provides support for reflection in the Type class (in the System namespace) and in the types found in the System.Reflection namespace. The following code creates a console mode application that uses the reflection classes to extract information about the
<DeveloperNote>custom attribute and the program elements to which it is applied:Option Strict OnImports Microsoft.VisualBasicImports SystemImports System.ReflectionImports System.TextImports Extensions.CustomAttributesModule modCommentsPublic Sub Main( )Dim strFile As String = Command( )Dim sOutput As StringIf strFile = "" ThenConsole.WriteLine("Syntax is: " & vbCrLf & _" DevNotes <filename>")Exit SubEnd If' Load assemblyDim oAssem As System.Reflection.Assembly = _System.Reflection.Assembly.LoadFrom(strFile)' Get any assembly-level attributesDim oAttribs( ) As Attribute = Attribute.GetCustomAttributes(oAssem)if UBound(oAttribs) >= 0 ThensOutput = DisplayDeveloperNotes(oAttribs)if sOutput <> "" ThenConsole.WriteLine(oAssem.GetName.Name & _" Assembly Developer Notes:" & vbCrLf)Console.WriteLine(sOutput)End IfEnd If' Get any module-level attributesDim oMod As System.Reflection.ModuleDim oMods() As System.Reflection.Module = oAssem.GetModules( )For Each oMod in oModsoAttribs = Attribute.GetCustomAttributes(oMod)If UBound(oAttribs) >= 0 ThensOutput = DisplayDeveloperNotes(oAttribs)If sOutput <> "" ThenConsole.WriteLine(oMod.Name & " Module Developer Notes: " _& vbCrLf)Console.WriteLine(sOutput)End IfEnd IfNext' Enumerate typesEnumerateTypes(oAssem)End Sub' Show information about each attributePublic Function DisplayDeveloperNotes(oAttribs( ) As Object) As StringDim sMsg As New StringBuilderDim oAttrib As AttributeDim oNote As DeveloperNoteAttributeFor Each oAttrib in oAttribsTryoNote = CType(oAttrib, DeveloperNoteAttribute)sMsg.Append(" Developer: " & oNote.Name & vbCrLf)sMsg.Append(" Comment: " & oNote.Comment & vbCrLf)sMsg.Append(" Date: " & oNote.DateRecorded & vbCrLf)sMsg.Append(" Bug: " & oNote.Bug & vbCrLf)Catch' No need to do anythingEnd TryNextReturn sMsg.ToStringEnd FunctionPrivate Sub EnumerateTypes(oObj As Object)Dim sOutput As StringDim oType, oTypes( ) As TypeIf oObj.GetType.ToString = "System.Reflection.Assembly" ThenDim oAssem As System.Reflection.Assembly = CType(oObj, _System.Reflection.Assembly)oTypes = oAssem.GetTypes( )ElseoTypes.SetValue(oObj, 0)End IfFor each oType in oTypesDim strType, strTypeAttr, strMeth As StringIf oType.IsClass ThenstrType = "Class"ElseIf oType.IsValueType ThenstrType = "Structure"ElseIf oType.IsInterface ThenstrType = "Interface"ElseIf oType.IsEnum ThenstrType = "Enum"End IfsOutput = strType & " " & oType.Name & ":" & vbCrLf' Get any type-level attributesDim oCustAttribs( ) As Object = oType.GetCustomAttributes(False)If oCustAttribs.Length > 0 ThenstrTypeAttr = DisplayDeveloperNotes(oCustAttribs)End IfstrMeth = EnumerateTypeMembers(oType)' Display Type and Member InfoIf strMeth <> "" Or strTypeAttr <> "" ThenConsole.WriteLine(sOutput)If strTypeAttr <> "" ThenConsole.WriteLine(strTypeAttr)End IfIf strMeth <> "" ThenConsole.WriteLine(strMeth & vbCrLf)End IfEnd IfNextEnd SubPrivate Function EnumerateTypeMembers(oType As Type) As StringDim strMeth, strRetVal As StringDim oAttribs( ) As Object' Get members of typeDim oMembersInfo( ), oMemberInfo As MemberInfooMembersInfo = oType.GetMembersFor Each oMemberInfo in oMembersInfo' Determine if attribute is presentoAttribs = oMemberInfo.GetCustomAttributes(False)If oAttribs.Length > 0 Then' determine member typeSelect Case oMemberInfo.MemberTypeCase MemberTypes.AllstrMeth = " All "Case MemberTypes.ConstructorstrMeth = " Constructor "Case MemberTypes.CustomstrMeth = " Custom method "Case MemberTypes.EventstrMeth = " Event "Case MemberTypes.FieldstrMeth = " Field "Case MemberTypes.MethodstrMeth = " Method "Case MemberTypes.NestedTypestrMeth = " Nested type "Case MemberTypes.PropertystrMeth = " Property"Case MemberTypes.TypeInfostrMeth = " TypeInfo"End SelectIf oMemberInfo.Name = ".ctor" ThenstrMeth = "New " & strMethElsestrMeth = oMemberInfo.Name & strMethEnd IfstrMeth = strMeth & vbCrLf & DisplayDeveloperNotes(oAttribs) _& vbCrLfstrRetVal = strRetVal & strMethEnd IfNextReturn strRetValEnd FunctionEnd ModuleThe program's entry point, the Main routine, first instantiates an Assembly object (in the System.Reflection namespace) representing the assembly by calling the LoadFrom method and passing it the filename containing the assembly. It then calls the Attribute class' shared GetCustomAttributes method, passing it a reference to an Assembly object, which returns an array of Attribute objects representing each custom attribute, if any exist. These attributes are then displayed by calling the DisplayDeveloperNotes method.
The shared GetCustomAttributes method of the Attribute class has several overloads that allow you to retrieve custom attributes belonging to assemblies, modules, class members, and parameters. (Unfortunately, the method does not retrieve the custom attributes belonging to types.) Since derived classes call the base class implementation, you can also retrieve attributes of a specific custom type with the following code:
Dim oAttribs( ) As Attribute = _DeveloperNoteAttribute.GetCustomAttributes(oAssem)After listing any DeveloperNoteAttributes applied to the assembly, the code retrieves the modules in the assembly by calling the Assembly object's GetModules method, which returns an array of Module objects. The code then iterates these modules and again calls the Attribute class' shared GetCustomAttributes method, this time passing it a Module object (to retrieve an array of custom Attribute objects belonging to that module). These objects are also displayed by calling the DisplayDeveloperNotes method.
Finally, Main calls the EnumerateTypes method, a generic routine that it uses to iterate the types in the Assembly object. (The routine could also be called from a type to extract information about custom attributes in its nested types.) This iteration casts the generic object passed as a parameter to an Assembly object, and then calls the Assembly object's GetTypes method to return an array of Type objects (defined in the System namespace) containing information about each type (such as a class, interface, delegate, structure, or num) in the assembly. Each Type object's GetCustomAttributes method is then called and its custom attributes are displayed.
While iterating the type objects, the EnumerateTypes method also calls the EnumerateTypeMembers method, which is responsible for iterating the members of each type and extracting their custom DeveloperNoteAttribute attributes. The EnumerateTypeMembers method first extracts an array of MemberInfo objects corresponding to each member by calling the GetMembers method of oType, the Type object passed to it as a parameter. GetMembers returns an array of MemberInfo objects, each element of which corresponds to a member of the type. The method then calls the MemberInfo object's GetCustomAttributes method to extract information about any custom types. Instead, it could also have called the Attribute object's GetCustomAttributes method, passing it a MemberInfo object representing the member whose custom attribute information was to be retrieved.
The program can be easily extended by adding recursion (allowing it to retrieve information about custom attributes in a nested class and its members), as well as by retrieving information about custom attributes applied to parameters belonging to individual methods.
Back to: VB.NET Language in a Nutshell, 2nd Edition
© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com