Annotating Types and Members
Thus far we have covered the general benefits of attributes and explored global attributes. Attributes can be applied at the type, method, property, and field levels, too.
The general syntax for applying attributes at a lower level of granularity is similar to using global attributes. Non-global attributes do not use the Assembly: prefix. Let's take a look at some specific attributes that can be applied to types (like classes) and members of types.
Adding Type Attributes
Attributes can be applied to types as well as members of types. The AttributeUsageAttribute is used to define an attribute class and the AttributeTargets enumeration flag passed to that attribute determines how a particular attribute can be used and what it can be applied to. For example, to use a particular attribute on a class, that attribute must have been tagged with the <AttributeUsage(AttributeTargets.All)> or <AttributeUsage(AttributeTargets.Class)> attribute when the attribute was defined. (Refer to the section "Creating Custom Attributes" later in this chapter for more information.)
The application of an attribute is the same regardless of whether that attribute is applied to a type, property, method, field, or assembly. Whether you can use an attribute on a particular code element or not depends on the AttributeTargets enumerations applied to that attribute. If an attribute cannot be applied to a code element, you will get an error in the Task List. For instance, if you apply the <DebuggerHidden()> attribute on a form class, you will get the following error:
'DebuggerHiddenAttribute' cannot be applied to 'Form1' because the attribute is not valid on this declaration type.
If an attribute is suitable, the syntax and requirements of a particular attribute are the same whether the attribute is applied to a type or some other declaration type.
Basic Attribute Statement
The basic attribute statement uses the enclosing <> brackets with the attribute class name written as a method call followed by parentheses. Here is a syntactical example: < attributename ()>.
The HelpAttribute class is not part of the CLR. The sample class is demonstrated in the VS .NET help files and a close variation is defined later in this chapter in the section "Creating Custom Attributes."
Borrowing the custom HelpAttribute class from the VS .NET help files (see ms-help://MS.VSCC/MS.MSDNVS/vbls7/html/vblrfVBSpec4_10.htm), we can apply a variation of the HelpAttribute class that has an Optional constructor argument.
For our purposes, we will assume that HelpAttribute can be applied with no flags or parameters of any kind. Perhaps the default behavior is to point at your company's Web site. To apply HelpAttribute to a form class, we must add a reference to the assembly (think DLL!) containing the attribute class and add an imports statement or use the full name of the attribute class, including the namespace. To apply HelpAttribute to a class, assuming that we have added a reference to the assembly containing the attribute, use the following code as a guide.
Imports MyHelpAttribute <Help()> Public Class Form1 ...
The Imports statement imports the namespace containing the attribute class HelpAttribute, and <Help()> applies the attribute to the Form1 class in the example. (Recall that we can use the complete name of the attribute classwhich for the example would be HelpAttributebut we drop the attribute suffix by convention.)
Using Positional Parameters
When you add an attribute tag like <Help()>, you are actually constructing an instance of the HelpAttribute class, which is discernible by examining the disassembled IL (refer to the section "Viewing Attributes with the MSIL Disassembler").
I also indicated that the HelpAttribute class was defined to have an optional parameter. The HelpAttribute is defined as follows :
Public Sub New(Optional ByVal AUrn As String = "Add Help Reference Here") FUrn = AUrn End Sub
The implication is that you can pass a substitute argument to the optional parameter. Parameters passed between the attribute parentheses to supply information for constructor arguments are referred to as positional parameters. We could redefine the application of the HelpAttribute class by supplying an argument for the first (and only, in this instance) positional parameter.
Imports MyHelpAttribute <Help("http://www.softconcepts.com")> Public Class Form1 ...
In this example the FUrn field will contain the reference to http://www.softconcepts.com. Positional parameters are matched by position and type, identical to supplying parameters for any other methods .
Using Named Arguments
You can supply named arguments when you apply an attribute. Named arguments initialize public fields or public readable and writable properties. Suppose we have an additional field named Topic that is not expressed as an argument to the constructor. To initialize public fields or properties when you apply an attribute, append the name argument using the following syntax:
Name: = Value
Initializing the Topic field for the HelpAttribute using the named argument syntax, the HelpAttribute applied to the Form1 class would be revised as follows:
Imports MyHelpAttribute <Help("http://www.softconcepts.com"), Topic:="AttributeDemo"> Public Class Form1 ...
The word Help is understood to invoke the HelpAttribute. The first argument is the positional argument passed to the constructor, and the Topic:="AttributeDemo" argument is a named argument that would initialize the Topic field.
If you need multiple named arguments, type additional arguments in the form Name := Value , separating each named argument by inserting a comma between arguments.
Applying Multiple Attributes
Consider the case where you want to apply more than one attribute. Additional attributes applied to a single code element are inserted between the <> brackets, separating each attribute statement with a comma.
Thus, if you applied a DescriptionAttribute in conjunction with the default HelpAttribute, the attribute statement would be written as demonstrated.
<Help(), Description("This is my description.")>
The benefit of using attributes is to provide additional data, or metadata, that is beneficial to other developers or to support extensions to VS .NET.
Adding Method Attributes
After reading the previous section, you are aware of the general syntax for applying attributes. You know how to apply singular and multiple attributes and how to provide positional and named arguments. The technical application of attributes to other code elements is identical to their use as applied to classes. For this reason we will take a look at a couple of useful attributes that you can apply to methods.
The DebuggerHiddenAttribute class is used to hide methods from the debugger. This attribute requires no positional or named arguments and is defined in the System.Diagnostics namespace.
Suppose you have debugged code thoroughly and want to avoid stepping through tested code as your system evolves. You can add the <DebuggerHidden()> attribute to specific methods to skip over them as you are stepping and tracing your application. Skipping long or tested methods can be beneficial in speeding up white boxtracing and stepping through code in the IDEtesting.
ConditionalAttribute can be used to allow code to be emitted to IL or not based on the value of the positional argument passed to ConditionalAttribute.
VB6 supported operations similar to this using conditional compiler code. Consider the example where you want to write entries to the event log during testing and disable event logging when you have finished testing. Listing 12.7 demonstrates a reasonable technique for VB6.
Listing 12.7 Conditional code and event logging in VB6
1: Private Sub Log(ByVal Message As String) 2: #Const DebugMode = 1 3: #If DebugMode Then 4: Call App.LogEvent(Message, vbLogEventTypeInformation) 5: #End If 6: End Sub 7: 8: Private Sub Command1_Click() 9: Call Log("Command1_Click") 10: End Sub
Implementing the log procedure shown in the listing, we can wrap the VB6 call App.LogEvent in VB6 between the conditional compiler directives and use the pragma DebugMode to enable or disable logging. (Logging is enabled in the listing. As a result, all of our logging code can remain in place, allowing us to quickly turn on logging when we go into maintenance mode.)
Unfortunately, this technique is not wholly suitable. If #DebugMode = 0, logging will not occur, but we will have potentially dozens of empty procedure calls to Log.
ConditionalAttribute in Visual Basic .NET yields a better result. If we apply ConditionalAttribute to a method, the method and all statements calling it are removed if the conditional string variable is passed the name of a pragma variable that is equivalent to False. Listing 12.8 demonstrates using ConditionalAttribute with code equivalent to that in Listing 12.7.
Listing 12.8 Using the VB .NET ConditionalAttribute and the EventLog component
1: Public Class Form1 2: Inherits System.Windows.Forms.Form 3: 4: [ Windows Form Designer generated code ] 5: 6: #Const LogMode = True 7: 8: <Conditional("LogMode")> _ 9: Private Sub Log(ByVal Message As String) 10: EventLog1.WriteEntry("Application", Message) 11: End Sub 12: 13: Private Sub Button1_Click(ByVal sender As System.Object, _ 14: ByVal e As System.EventArgs) Handles Button1.Click 15: Log("Button1_Click") 16: End Sub 17: End Class
Line 6 defines the pragma constant LogMode and initializes it to True. (You can set the value of custom constants on the Build Property Page of the project's properties.) The Conditional on line 8 passes the name of the constant to the attribute. LogMode evaluates to True; consequently, the Log procedure is included in the emitted IL. Unlike what would happen in VB6, the statement on line 15 is omitted, too. Running the sample program, we can view the logged entry (see Figure 12.1).
Figure 12.1. The Event Viewer in Windows 2000 showing the logged event using the EventLog component.
If we change the LogMode constant to False, we can view the IL code and clearly note that all calls to the Conditional Log method have been removed from the IL. (Figure 12.2 shows the IL with LogMode = True and LogMode = False respectively.) Visual Basic .NET eliminates the overhead of empty method calls.
Figure 12.2. Emitted IL showing the "before and after" pictures when employing the Conditional Attribute.
Looking at IL code can provide interesting clues as to how idioms are implemented in .NET. (It is too bad we cannot get the complete CLR source code for our edification.) From the IL in Figure 12.2, it looks as if IL is similar to assembly language, and it also looks as if the compiler has a distinct C# slant on the way code is treated. Instance void ConditionalAttributeDemo::Form1::Log(string) looks suspiciously like C++ code.
A final note: the IL_0007 instruction callvirt sounds like a virtual method invocation, suggesting that ConditionalAttribute may use a pointer to a virtual methods table to implement ConditionalAttribute. Point at a specific method if the conditional argument is True and a null table entry if the conditional is False. This last part is supposition, but examining other developers' code provides insight.
You do not have to know how to read IL to program in .NET. However, if you can read a little assembly language, IL is similar, and you can tell what is going on. In the top half of Figure 12.2, you can see that LogMode = True. As a result, everywhere the call to Log occurs, IL will be emittednotice that instruction IL_0007 calls the Log method. The bottom half of the figure is the IL emitted when LogMode = False. The nop (no-op) instruction performs no operation; nop is filler code, probably to align code instruction boundaries. The important thing to note is that the compiler stripped the calls out for us, allowing us to leave our valuable , conditional code in place for future use.
Adding Field and Property Attributes
We already know that attributes are attributes regardless of the code elements they are applied to. In order to apply an attribute to a field or property, that attribute must have been defined with AttributeTargets including AttributeTargets.Field and AttributeTargets.Property flags.
DescriptionAttribute and CategoryAttribute are useful for organizing component properties and events (the latter only appear in the Properties window in C#). DescriptionAttribute allows you to provide a brief description for a property, and CategoryAttribute facilitates categorical grouping of properties and events. When a property is selected in the Properties window, the value provided to the Description attribute is displayed at the bottom of the Properties window (see Figure 12.3). When the Categorized button is clicked in the Properties window, properties are organized by category.
Figure 12.3. Description Attribute is read to display a description of component properties, as demonstrated for the Button.Text property.
To demonstrate using DescriptionAttribute and CategoryAttribute, we will take a slightly circuitous route. I will quickly demonstrate how to create a component, enabling you to see the end result: the description and categorization capabilities supported by the Properties window. (We will return to component building in Chapter 16, "Designing User Interfaces.")
Creating a Custom Component
Custom components are class library projects. To create the project for the custom component, select the Class Library project from the New Project dialog box.
The component we will create is a label that displays shadowy text. The effect is created by painting the text twice with a slightly offset contrast color. The component will need an additional color property, reflecting the contrasting color of the shadow.
To create the effect, we can overload the Paint method of a label control, painting the label's text at a slightly offset position using the shadow color. Listing 12.9 demonstrates how to subclass the System.Windows.Forms.Label control, adding the ShadowColor and the overloaded Paint method.
Listing 12.9 A custom control used to demonstrate DescriptionAttribute
[View full width]
1: Imports System.Windows.Forms 2: Imports System.Drawing 3: Imports System.ComponentModel 4: 5: Public Class EffectsLabel 6: Inherits Label 7: 8: Private FShadowColor As Color = Color.WhiteSmoke 9: 10: Private Const sDescription As String = "Background color used to create the shadow effect." 11: Private Const sCategory As String = "Appearance" 12: 13: 14: Public Property ShadowColor() As Color 15: Get 16: Return FShadowColor 17: End Get 18: Set(ByVal Value As Color) 19: If (Value.Equals(FShadowColor)) Then Exit Property 20: FShadowColor = Value 21: Invalidate() 22: End Set 23: End Property 24: 25: Private Function ShadowBrush() As Brush 26: Return New SolidBrush(FShadowColor) 27: End Function 28: 29: Protected Overrides Sub OnPaint(_ 30: ByVal e As PaintEventArgs) 31: 32: Dim Rect As New RectangleF(-2, -2, Width, Height) 33: 34: e.Graphics.DrawString(Text, Font, _ 35: ShadowBrush(), Rect, FormatObject()) 36: 37: MyBase.OnPaint(e) 38: 39: End Sub 40: 41: #Region "These functions are monolithic. Replace with a better algorithm." 42: 43: Private Function FormatObject() As StringFormat 44: ' Note: Uses the function name as a temporary variable name! 45: FormatObject = New StringFormat() 46: SetLineAlignment(FormatObject) 47: SetAlignment(FormatObject) 48: 49: If (RightToLeft) Then 50: FormatObject.FormatFlags = FormatObject.FormatFlags Or _ 51: StringFormatFlags.DirectionRightToLeft 52: End If 53: End Function 54: 55: Private Sub SetLineAlignment(ByVal FormatObject As StringFormat) 56: Select Case TextAlign ' Hideous algorithm! 57: Case ContentAlignment.BottomLeft To _ 58: ContentAlignment.BottomRight 59: 60: FormatObject.LineAlignment = StringAlignment.Far 61: 62: Case ContentAlignment.MiddleLeft To _ 63: ContentAlignment.MiddleRight 64: 65: FormatObject.LineAlignment = StringAlignment.Center 66: 67: Case ContentAlignment.TopLeft To _ 68: ContentAlignment.TopRight 69: 70: FormatObject.LineAlignment = StringAlignment.Near 71: End Select 72: End Sub 73: 74: Private Sub SetAlignment(ByVal FormatObject As StringFormat) 75: Select Case TextAlign ' Hideous algorithm! 76: Case ContentAlignment.BottomLeft, _ 77: ContentAlignment.TopLeft, _ 78: ContentAlignment.MiddleLeft 79: 80: FormatObject.Alignment = StringAlignment.Near 81: 82: Case ContentAlignment.MiddleCenter, _ 83: ContentAlignment.BottomCenter, _ 84: ContentAlignment.TopCenter 85: 86: FormatObject.Alignment = StringAlignment.Center 87: 88: Case ContentAlignment.TopRight, _ 89: ContentAlignment.BottomRight, _ 90: ContentAlignment.MiddleRight 91: 92: FormatObject.Alignment = StringAlignment.Far 93: End Select 94: End Sub 95: 96: #End Region 97: 98: End Class
The listing seems a little long; however, for the most part Listing 12.9 only introduces an overloaded Paint method and the ShadowColor property. ShadowColor is used to paint the background text. Unfortunately, lines 41 through 98 contain monolithic code to convert a ContentAlignment enumeration property describing both of the label's horizontal and vertical offsets to the individual horizontal StringFormat.Alignment and the vertical StringFormat.LineAlignment properties because a more convenient algorithm was not discovered .
The Paint method creates an offsetting rectangle on line 32. The rectangle and the StringFormat object returned by the FormatObject function on lines 43 to 53 are used as arguments to DrawString on lines 34 and 35. The call to Graphics.DrawString draws the string in the ShadowColor and then invokes the base methodMyBase.OnPaint, on line 37to draw the foreground label text.
Applying the Description and Category Attribute
You can apply the DescriptionAttribute to properties and events. Because events do not show up in the Properties window in Visual Basic .NET (they do in Visual C#), we will add a DescriptionAttribute to our only property, ShadowColor.
Modify line 13 which was blank in Listing 12.9 to apply the DescriptionAttribute to the ShadowColor property. The CategoryAttribute was used to enable categorical organization of the property.
The attribute statement is actually part of the property statement. Either add attributes to the same thing to which they are applied or add attributes on a preceding line and include the line continuation character, the underscore .
<Description(sDescription), Category(sCategory)> _
Notice that the attributes are followed by the line continuation character (_), which allows you to break long lines of code for formatting purposes. Attributes are required to be on the same line as the entity they describe.
Adding the Custom Component to the Toolbox
The component can be added to the Toolbox by compiling the Class Library project. When you have compiled the component's assembly (.DLL file), create a new Windows application to test the component.
Follow the numbered steps to add the control to the Toolbox and test the control.
Select the component from the Windows Forms Toolbox page and drop it on the form. The default effect should be an EffectLabel named EffectLabel1 by default. Press F4 to display the Properties window. Select the ShadowColor property and verify that the description is displayed (see Figure 12.4).
Figure 12.4. The result of adding Description Attribute to a component's property.