Search the Catalog
VB.NET Language in a Nutshell, 2nd Edition

VB.NET Language in a Nutshell, 2nd Edition

By Steven Roman, Ron Petrusha, Paul Lomax
Second Edition April 2002
0-596-00308-0, Order Number: 3080
682 pages, $44.95 US $69.95 CA

Chapter 8
Attributes

Attributes 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 Integer

Ordinarily, 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 Integer

Each 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, the Imports directive should be used, and a reference should be added to the project either using the References dialog in Visual Studio or the /r switch 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 ParamArray is 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 String

TIP:   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 is True. To set it to False, you could use the attribute as follows:

<AttributeUsage(AttributeTargets.Class, Inherited:=False)> _
Public Class MyCustomClass

Be 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
Structure

For example, the following Class statement illustrates this general usage of an attribute:

<AttributeUsage(AttributeTargets.All)> _
Public Class MyCustomAttrAttribute

The 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: or Module:) 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 any Option and Imports statements), along with any other attributes that require a modifier. This syntax is valid for an attribute applied to an assembly or a module only.

For example:

Option Strict On
Imports System.Data.SqlClient
<Assembly: AssemblyDescription("Supplementary data access library")>
 
Namespace SqlAccess

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

  1. Define a public class that inherits from System.Attribute or another attribute class derived from System.Attribute. For example:
  2. Public Class DeveloperNoteAttribute
     Inherits System.Attribute
    
  3. Note that, by convention, the name of the class ends with the substring "Attribute".

  4. 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 the AttributeTargets enumeration:
  5. All
    Assembly
    Class
    Constructor
    Delegate
    Enum
    Event
    Field
    Interface
    Method
    Module
    Parameter
    Property
    ReturnValue
    Struct
    
  6. If 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 to True. Finally, we want to allow the application of multiple attributes to the same program element; hence, we want to set the AllowMultiple argument to True as well. In view of this setting, our code should look as follows:

  7. <AttributeUsage(AttributeTargets.All, _
     Inherited:=True, _ 
     AllowMultiple:=True)> _
    Public Class DeveloperNoteAttribute
     Inherits System.Attribute
    
  8. 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:
  9. Public Sub New(Name As String, Comment As String, _
     DateRecorded As String)
     MyBase.New(  )
     strName = Name
     strComment = Comment
     datDate = CDate(DateRecorded)
    End Sub
    
  10. Note 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.

  11. 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:
  12. Public Property Name As String
       Get
          Return strName
       End Get
       Set
          strName = Value
       End Set
    End Property   
     
    Public Property Comment As String
       Get
          Return strComment
       End Get
       Set
          strComment = Value
       End Set
    End Property
     
    Public Property DateRecorded As Date
       Get
          Return datDate
       End Get
       Set
          datDate = Value
       End Set
    End Property
     
    Public Property Bug As Boolean
       Get
          Return blnBug
       End Get
       Set
          blnBug = Value
       End Set
    End Property
    

The complete code for the attribute class is shown in Example 8-1.

Example 8-1: The DeveloperNoteAttribute attribute class

Option Strict On
Imports System
 
Namespace Extensions.CustomAttributes
 
<AttributeUsage(AttributeTargets.All, _
                Inherited:=True, _ 
                AllowMultiple:=True)> _
Public Class DeveloperNoteAttribute
   Inherits System.Attribute
 
Protected strName, strComment As String
Protected blnBug As Boolean
Protected datDate As Date
 
Public Sub New(Name As String, Comment As String, DateRecorded As String)
   MyBase.New(  )
   strName = Name
   strComment = Comment
   datDate = CDate(DateRecorded)
End Sub
 
Public Property Name As String
   Get
      Return strName
   End Get
   Set
      strName = Value
   End Set
End Property   
 
Public Property Comment As String
   Get
      Return strComment
   End Get
   Set
      strComment = Value
   End Set
End Property
 
Public Property DateRecorded As Date
   Get
      Return datDate
   End Get
   Set
      datDate = Value
   End Set
End Property
 
Public Property Bug As Boolean
   Get
      Return blnBug
   End Get
   Set
      blnBug = Value
   End Set
End Property
 
End Class
 
End Namespace

Using 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 On
 
Imports Microsoft.VisualBasic
Imports System
Imports System.Reflection
Imports System.Text
Imports Extensions.CustomAttributes
 
Module modComments
 
Public Sub Main(  )
 
   Dim strFile As String = Command(  )
   Dim sOutput As String
 
   If strFile = "" Then 
      Console.WriteLine("Syntax is: " & vbCrLf & _
                        "   DevNotes <filename>")
      Exit Sub
   End If
 
   ' Load assembly
   Dim oAssem As System.Reflection.Assembly = _
                 System.Reflection.Assembly.LoadFrom(strFile)
 
   ' Get any assembly-level attributes
   Dim oAttribs(  ) As Attribute = Attribute.GetCustomAttributes(oAssem)
   if UBound(oAttribs) >= 0 Then
      sOutput = DisplayDeveloperNotes(oAttribs)
      if sOutput <> "" Then
         Console.WriteLine(oAssem.GetName.Name & _
                           " Assembly Developer Notes:" & vbCrLf)
         Console.WriteLine(sOutput)
      End If
   End If
 
   ' Get any module-level attributes
   Dim oMod As System.Reflection.Module
   Dim oMods() As System.Reflection.Module = oAssem.GetModules(  )
   For Each oMod in oMods
      oAttribs = Attribute.GetCustomAttributes(oMod)
      If UBound(oAttribs) >= 0 Then
         sOutput = DisplayDeveloperNotes(oAttribs)
         If sOutput <> "" Then
            Console.WriteLine(oMod.Name & " Module Developer Notes: " _
                              & vbCrLf)
            Console.WriteLine(sOutput)
         End If
      End If
   Next
   ' Enumerate types
   EnumerateTypes(oAssem)
 
End Sub
 
' Show information about each attribute
Public Function DisplayDeveloperNotes(oAttribs(  ) As Object) As String
 
   Dim sMsg As New StringBuilder
   Dim oAttrib As Attribute
   Dim oNote As DeveloperNoteAttribute
 
   For Each oAttrib in oAttribs
      Try
         oNote = 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 anything
      End Try
   Next
   Return sMsg.ToString
End Function
 
Private Sub EnumerateTypes(oObj As Object)
   Dim sOutput As String
   Dim oType, oTypes(  ) As Type
   If oObj.GetType.ToString = "System.Reflection.Assembly" Then
      Dim oAssem As System.Reflection.Assembly = CType(oObj, _
                    System.Reflection.Assembly)
      oTypes = oAssem.GetTypes(  )
   Else
      oTypes.SetValue(oObj, 0)
   End If
   For each oType in oTypes
      Dim strType, strTypeAttr, strMeth As String
      If oType.IsClass Then 
         strType = "Class"
      ElseIf oType.IsValueType Then
         strType = "Structure"
      ElseIf oType.IsInterface Then
         strType =  "Interface"
      ElseIf oType.IsEnum Then
         strType = "Enum"
      End If
      sOutput = strType & " " & oType.Name & ":" & vbCrLf
 
      ' Get any type-level attributes
      Dim oCustAttribs(  ) As Object = oType.GetCustomAttributes(False)
      If oCustAttribs.Length > 0 Then
         strTypeAttr = DisplayDeveloperNotes(oCustAttribs)
      End If
 
      strMeth = EnumerateTypeMembers(oType)
 
      ' Display Type and Member Info
      If strMeth <> "" Or strTypeAttr <> "" Then
         Console.WriteLine(sOutput)
         If strTypeAttr <> "" Then
            Console.WriteLine(strTypeAttr)
         End If
         If strMeth <> "" Then
            Console.WriteLine(strMeth & vbCrLf)
         End If
      End If
   Next
End Sub
 
Private Function EnumerateTypeMembers(oType As Type) As String
   Dim strMeth, strRetVal As String
   Dim oAttribs(  ) As Object
   ' Get members of type
   Dim oMembersInfo(  ), oMemberInfo As MemberInfo
   oMembersInfo = oType.GetMembers
   For Each oMemberInfo in oMembersInfo
      ' Determine if attribute is present
      oAttribs = oMemberInfo.GetCustomAttributes(False)
      If oAttribs.Length > 0 Then 
         ' determine member type
         Select Case oMemberInfo.MemberType
            Case MemberTypes.All
               strMeth = " All "  
            Case MemberTypes.Constructor
               strMeth = " Constructor "
            Case MemberTypes.Custom
               strMeth = " Custom method "  
            Case MemberTypes.Event
               strMeth = " Event " 
            Case MemberTypes.Field
               strMeth = " Field "
            Case MemberTypes.Method
               strMeth = " Method "
            Case MemberTypes.NestedType
               strMeth = " Nested type "
            Case MemberTypes.Property
               strMeth = " Property" 
            Case MemberTypes.TypeInfo
               strMeth = " TypeInfo"
         End Select
         If oMemberInfo.Name = ".ctor" Then
            strMeth = "New " & strMeth
         Else
            strMeth = oMemberInfo.Name & strMeth
         End If
         strMeth = strMeth & vbCrLf & DisplayDeveloperNotes(oAttribs) _
                   & vbCrLf
         strRetVal = strRetVal & strMeth 
      End If
   Next
   Return strRetVal
End Function
 
End Module

The 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


oreilly.com Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com