asp:feature
LANGUAGES: Visual Basic .NET
TECHNOLOGIES: Web Controls
Great-Looking Controls
Add sophistication with attributes.
By Jon Henning
If you've ever placed a control on a form within Visual Studio .NET and set a property within the property browser, you might have noticed that some properties are more sophisticated than others. For example, the Font property expands into a series of sub-properties, and the BackColor property provides a dropdown list of available colors and a visual sample of each color choice. You render this rich UI using attributes.
To jumpstart your understanding of how to use attributes in control development, I'll walk you through some of the attributes commonly used to develop an ASP.NET Web control. Specifically, I'll show you the source code from an ASP.NET hierarchical menu control that is publicly available free of charge.
Probably the most common property exposed for any control is a color. One way to assign a color is to have the user type in the color's hexadecimal value, but that's not user-friendly and not consistent with the way other .NET controls expose their color properties. When you specify the return value for the ShadowColor property as System.Drawing.Color, however, the appropriate color pop-up displays within the property browser (see Figure 1).
Figure 1. The standard way to display color properties within Visual
Studio .NET is to provide a pop-up that displays a dropdown list of available
colors. This pop-up provides a visual sample of each color choice.
Unfortunately, the property browser does not store the value as a hexadecimal number as a Web control requires. The runtime needs an additional set of information stating that the ShadowColor property is used for Web colors. You do this by adding the TypeConverter attribute to the property. This code demonstrates how to use four commonly used attributes for a control's property:
Category("Appearance"), DefaultValue("gray"),
_ Description("Border color for shadow effect")> _ Public Property ShadowColor() As System.Drawing.Color Get Return
IIf(m_objShadowColor.IsEmpty, m_objShadowColor.Gray, m_objShadowColor) End Get Set(ByVal Value As
System.Drawing.Color) m_objShadowColor =
Value End Set End Property The next few examples demonstrate how you can use
attributes to create expandable properties, enumerators, and images. The
MenuControl provides a property named MenuEffects. This property exposes the
MenuEffects class, which has four main properties: ShadowColor,
ShadowDirection, ShadowStrength, and UseShadow. If metadata is not provided for
this property, the only way to interact with the MenuEffects properties is
through code - you can't change these values through Visual Studio's property
dialog (see the sidebar, "The Need For Metadata"). To get the property viewer to interact with the
MenuEffects object, the property viewer must be able to convert the object into
a data type the viewer can understand - in this case, a string. Also, you could
allow the property to be expanded in the same way as the menu's built-in Font
property, which lets you modify each property independently. To handle both
scenarios, you must use a custom TypeConverter. As seen with the WebColorConverter, TypeConverters assist
in the conversion of one type - an object - into another type - a string. To
create a custom TypeConverter, the new class must be inherited from the
TypeConverter class. Then, the class needs to override three of the methods:
CanConvertFrom, ConvertFrom, and ConvertTo. The CanConvertFrom method returns a
Boolean that dictates whether a source of a given type - such as a string or
integer - can be converted. The ConvertTo method does the actual work of
converting the MenuEffects object to a string. (Note that you don't need to
override the CanConvertTo method because it defaults to true when converting to
a string.) The last overridden method is ConvertFrom. This method
converts the string into a MenuEffects object. The code in Figure 2 overrides
the ConvertTo and ConvertFrom functions. Public Overloads Overrides Function
ConvertTo( _ ByVal context As
ITypeDescriptorContext, _ ByVal culture As
CultureInfo, ByVal value As Object, _ ByVal destinationType
As Type) As Object If TypeOf value Is MenuEffects And _ destinationType Is
GetType(String) Then Dim obj As
MenuEffects = CType(value, MenuEffects) '- GetColor is a
custom function that translates '- a color object
into a string Return
GetColor(obj.ShadowColor) & "," & _ obj.ShadowDirection
& "," & _ obj.ShadowStrength
& "," & _ obj.UseShadow End If End Function Public Overloads Overrides Function
ConvertFrom( _ ByVal context As
ITypeDescriptorContext, _ ByVal culture As
CultureInfo, _ ByVal value As Object)
As Object If value Is GetType(String) Then Dim arr As String()
= Split(CType(value, String), _ ",") If UBound(arr) >= 3 Then Dim objEffect As
MenuEffects = New MenuEffects() objEffect.ShadowColor = GetColor(arr(0)) objEffect.ShadowDirection = CStr(arr(1)) objEffect.ShadowStrength = CInt(arr(2)) objEffect.UseShadow = CBool(arr(3)) End If End If End Function Figure 2. When creating a custom TypeConverter, two
methods must be overridden: ConvertTo and ConvertFrom. The ConvertTo function
packs the sub-properties into a string, and ConvertFrom unpacks the string into
MenuEffects' sub-properties. Use Expandable Properties The TypeConverter object also exposes two more methods:
GetProperties and GetPropertiesSupported. Overriding these methods allows the
MenuEffects property to become expandable like the Font property. The
GetPropertiesSupported method tells the caller - in this case, the property
browser - that the GetProperties method is supported. The GetProperties method
returns to the caller the names of the properties in a PropertyCollection
object. This is a simple step, but you can bypass it because Microsoft provides
a class to handle this task in VS .NET automatically. Inheriting from the
ExpandableObjectConverter class takes care of the code behind these two
methods. The MenuEffects property definition needs to specify the
TypeConverter attribute in the same way as the ShadowColor property. This code
shows the application of the custom TypeConverter class MenuEffectsConverter: Public Property MenuEffects() As MenuEffects ... Now that the properties dialog knows how to display the
MenuEffects property, you must tell it how to persist the values to the page
when they are modified. The Font property handles persistence correctly, so
looking at it in the object browser can provide helpful information. It is defined
with the attribute DesignerSerializationVisibilityAttribute(2). When defining
this attribute, Visual Studio .NET gives an enumerated list of choices:
.Content, .Hidden, and .Visible. Because the value of .Content equals 2, it is
added to the MenuEffects property: DesignerSerializationVisibilityAttribute( _ DesignerSerializationVisibility.Content)> _ Public Property MenuEffects() As MenuEffects ... This yields better results. When the value of the concatenated
string is modified, the changes are persisted; the changing of the individual
properties is still, however, not written to the page. Once again, you need an
attribute to tell the runtime that the child properties must notify their
parent property when they are modified. By placing the NotifyParentProperty
attribute before each MenuEffects property, the values are persisted correctly: Public Property ShadowStrength() As Integer One of the properties in the MenuEffects object is
ShadowDirection. To an end user, seeing this might cause confusion because
ShadowDirection is defined as an integer. It's better to provide the end user a
dropdown list such as Upper Left, Upper Right, or Lower Right. To accomplish
this, you need another custom TypeConverter. Because this class is not an expandable property, it can
be inherited from the TypeConverter class directly. This time, the methods
GetStandardValuesSupported, GetStandardValuesExclusive, and GetStandardValues
are overridden. The GetStandardValuesSupported method returns a Boolean that
tells the caller whether this property contains a list of standard values. The
GetStandardValuesExclusive method returns a Boolean that tells the caller if
the only choices allowed are contained in the list; otherwise, the user is free
to type in a custom value. The last method, GetStandardValues, returns a list
of values to be displayed in the dropdown: Public Overloads Overrides Function
GetStandardValues( _ ByVal context As
ITypeDescriptorContext) _ As
TypeConverter.StandardValuesCollection Dim s As String() = {"Top", "Upper Right",
"Right", _ "Lower
Right", "Bottom", "Lower Left", _ "Left",
"Upper Left"} Return New
TypeConverter.StandardValuesCollection(s) End Function Adding this TypeConverter to the ShadowDirection property
causes the property browser to display a dropdown list of choices (see Figure
3). Display Property Images One of the property viewer's nicer features is the display
of an image next to the property value. This display commonly is seen in a
control's Font and Color properties, and it provides you with a visual
representation of what the property will look like. For the menu's
ShadowDirection property to contain its own set of images, you need to create a
class that inherits from the UITypeEditor object. This class overrides the
GetPaintValueSupported and PaintValue methods: Public Overloads Overrides _ Function GetPaintValueSupported(
_ ByVal context As
ITypeDescriptorContext) As Boolean 'let property browser
know we are doing custom painting Return True End Function GetPaintValueSupported returns a Boolean that tells the
caller whether the property supports images: Public Overloads Overrides Sub
PaintValue( _ ByVal e As
Design.PaintValueEventArgs) Dim sImageName As String = "spshadow" & _ CType(e.Value,
String).ToLower '--- draw image from
resource file ---' e.Graphics.DrawImage(CType( _ m_objRes.GetObject(sImageName), Bitmap), e.Bounds) '- could have
specified many ways to free-draw image -' '-
e.Graphics.DrawEllipse(New Pen(Color.Blue), _ '- New RectangleF(5, 5, 10, 10)) End Sub The PaintValue subroutine is passed a reference to the
property dialog's Property object. By interrogating the Value property, the
code can determine what the property is set to. Using this value, the code can
render the appropriate image to display using the Graphics.DrawImage method.
Associating the MenuShadowDirEditor to the ShadowDirection property, as shown
in the following code, gives the results displayed in Figure 3: TypeConverter(GetType(MenuEffectsShadowDirConverter))> _ Public Property ShadowDirection() As String ... One way that controls can simplify some of the more
complex properties is by providing a design-time interface. The
SolpartMenuControl provides its own GUI to assign the XML for its MenuData
property. To accomplish this, the property must be associated with an editor
that inherits from System.Drawing.Design.UITypeEditor. This class requires that
two methods be overridden. First, you must override GetEditStyle, which allows
the pop-up dialog box to be defined as modal by returning
UITypeEditorEditStyle.Modal. Next, override EditValue. This function is called
when the ellipsis is pressed (see Figure 4). EditValue is passed the current property's value, which is
given to the designer form as a constructor argument. When the custom dialog is
displayed, it is representative of the current MenuData value. After the
changes are made and the dialog is closed, the value is returned through the
form's XML property and is passed back as a return value to the EditValue
function (see Figure 5). Public Overloads Overrides Function
EditValue( _ ByVal context As _ System.ComponentModel.ITypeDescriptorContext,
_ ByVal provider As
System.IServiceProvider, _ ByVal value As
Object) As Object If Not context Is Nothing And _ Not
context.Instance Is Nothing And _ Not provider Is
Nothing Then m_objEditorSvc =
CType(provider.GetService( _ GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService) End If If Not m_objEditorSvc
Is Nothing Then 'instantiate form
object 'passing value
from property m_objEditor = New
SPMenuDesignForm(CStr(value)) m_objEditor.ShowDialog() 'assign property
xml from form value =
m_objEditor.xml End If Return value End Function Figure 5. The EditValue function is responsible for
showing the designer for the class inheriting from UITypeEditor. When you pass
the function the property's current value through the value parameter, the
function returns the form's modifications. Adding the Editor attribute to the MenuData property
allows the property to be associated with the Editor class: (GetType(MyDesign.ControlDesigners.MenuDataDesigner), GetType(System.Drawing.Design.UITypeEditor)) Public Property
MenuData() As String Control Design-Time Appearance One feature of a good control is to have its design-time
representation look like its runtime interface. By default, a custom Web
control has an interface that renders like Figure 6. What would be better is to have a representation that
changes as you modify the various properties within the control. For example,
if you change the font name to Arial, the fore color to Yellow, and the
background color to a shade of blue, the control should render that way at
design time. Figure 7 shows a much better design-time representation. Once again, the .NET Framework makes this task simple.
First, you must define a class that inherits from
System.Web.UI.Design.ControlDesigner. Once you define this class, it needs to
override two methods: AllowResize and GetDesignTimeHtml. AllowResize returns a
Boolean that lets Visual Studio know whether the control should be allowed to
be resized; GetDesignTimeHtml returns a string that contains the HTML necessary
to render the control. Figure 8 demonstrates a simple implementation of this
concept. Public Overrides Function
GetDesignTimeHtml() As String '--- Component is the instance of the component ---' '--- or control that this designer object is ---' '--- associated with. This property is inherited ---' '--- from System.ComponentModel.ComponentDesigner.---' Dim objM As SolpartMenu =
CType(Component, SolpartMenu) If objM.ID.Length > 0 Then Dim sw As New
StringWriter() Dim tw As New
HtmlTextWriter(sw) Dim objLbl As Label
= New Label() objLbl.BackColor =
objM.BackColor objLbl.Font.CopyFrom(objM.Font) objLbl.Text =
objM.ID objLbl.ForeColor =
objM.ForeColor '--- make border
look 3D ---' objLbl.BorderStyle =
BorderStyle.Outset '--- only show
border when property is set ---' objLbl.BorderWidth =
New Unit(objM.MenuBorderWidth) If
objM.MenuAlignment <> "Justify" Then '--- Align Menu
Text accordingly ---' objLbl.Style.Add("text-align", objM.MenuAlignment) End If '--- Determine if
displayed horiz. or vert. ---' If objM.Display =
"Horizontal" Then objLbl.Width =
New Unit("100%") objLbl.Height =
New Unit(objM.MenuBarHeight) Else objLbl.Height =
New Unit(500) objLbl.Width =
Unit.Empty End If '--- Render HTML to
HTMLTextWriter Object ---' objLbl.RenderControl(tw) '--- Return HTML
String ---' Return sw.ToString() End If End Function Figure 8. This method creates HTML controls,
interrogates the menu's applicable properties, and returns the control's
rendered HTML string. The only difficult part is realizing that the class
inherited an object called Component that you can cast into the SolpartMenu
object. You can find information regarding the Component object in the .NET
Framework Developers Guide section of the Visual Studio .NET help file. The last thing you need to do is specify the class in the
Designer attribute of the SolpartMenu class definition: ("<{0}:SolpartMenu
runat=server>{0}:SolpartMenu>"), _ Description("Solution Partners ASP.NET Menu Control"),
_ Designer _ (GetType(MyDesign.ControlDesigners.MenuDesigner)), _ ParseChildren(False)> _ Public Class SolpartMenu : Inherits WebControl : _ Implements
System.Web.UI.IPostBackEventHandler You will be pleased to find out that the full source code
from the menu is free and available for download. The sample code in this
article is available for download. Jon Henning is a senior consultant with Solution
Partners Inc., a consulting company specializing in Microsoft technologies (http://www.solpart.com).
He is an MCSD who has been working with Visual Studio .NET since the PDC
release. He has written several articles dealing with all aspects of
programming, but his current love is developing Web controls for ASP.NET.
E-mail Jon at mailto:[email protected]. One shortcoming of programming COM components was the lack
of extensibility within COM's type information. Although you could associate
metadata with an interface, this technique was not used often because it was
not possible to do it within Visual Basic. Probably the most recognizable
implementation of metadata added to an interface came with the Transaction
attribute set for MTS objects. VB6 allowed this one attribute to be defined
within its development environment, but other attributes could be set only by
the person installing the program - if at all. In contrast, the .NET platform allows you to define the
metadata for an object's interface - such as methods and properties - through
well known or custom attributes by prefixing the definition with the attribute
information enclosed in brackets: < > for VB, [ ] for C#. These
attributes give you the ability to offer more meaningful information to the
runtime on its intended use. For more information, see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconmetadataoverview.asp. One of the main goals of the Menu control is to make it
stand on its own. To do this, the control should not require any references to
external files. If you're familiar with coding DHTML, you know this usually
involves a lot of JavaScript. Usually this script is separated out in its own
.js file. Although the menu easily could have followed this model, it is not
advantageous for a Web control because the user is required to install the
different .js files on the Web server. Embedding the script in a hard-coded
string within the Web control is another option, but it's not a very
maintainable one because you must take great care to ensure the strings' quotation
marks don't get lost. Even when this is done correctly, the end script still is
not very readable. Ordinarily, resource files are used in building
multilanguage applications. Resource files are also, however, an excellent way
to store your script; the design-time format of the information stored in a
resource file is XML. The code in Figure A shows how to reference the resource
file, retrieve the data, and send it down to the client. Private m_objRes As
System.Resources.ResourceManager = _ New System.Resources.ResourceManager( _ "SolpartWebControls.solpartwc-script", _ GetType(SolpartMenu).Assembly) Protected Overrides Sub OnPreRender( _ ByVal e As
System.EventArgs) Dim sScript As String If Not Page.IsClientScriptBlockRegistered( _ "solpartmenuscript")
Then Select Case
Request.Browser.Browser Case
"IE" sScript =
m_objRes.GetString("spmenu-ie") Case
"Netscape" If
BrowserType.MajorVersion >= 6 Then sScript =
m_objRes.GetString("spmenu-ns6") Else sScript =
m_objRes.GetString("spmenu-all") End If End Select Page.RegisterClientScriptBlock("solpartmenuscript", _ "") End If End Sub Figure A. This code verifies that the script has not
yet been sent, detects the browser type and version, pulls the appropriate
JavaScript out of the resource file, and sends it down to the requesting page. Although it's good to minimize the hassles involved with
installing the control, there's a trade-off when it comes to performance. If
separate .js files are used, the browser can cache the files, which could lead
to improved performance. For this reason, the Menu control optionally allows
the files to be sent down as .js files through the assignment of the
SystemScriptPath property. Tell us what you think! Please send any comments about
this article to [email protected].
Please include the article title and author.
Figure 3. The end result of your efforts is an expandable MenuEffects
property handled by the MenuEffectsConverter TypeConverter, and an enumerated
list of choices for the ShadowDirection property for which the
MenuEffectsShadowDirConverter TypeConverter is responsible. Also, an image is
associated with each enumerated item.
Figure 4. The property dialog now displays an ellipsis next to the
property value. Clicking on the ellipsis causes the custom menu designer form
to display.
Figure 6. The default design-time interface for a custom Web control
does not look professional. It consists of the control's class name and ID.
Figure 7. Here, the control's design-time interface changes
automatically when you change a property.