Great-Looking Controls

Add sophistication with attributes.

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).

 


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.

 

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).

 


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.

 

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.

 


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.

 

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.

 


Figure 7. Here, the control's design-time interface changes automatically when you change a property.

 

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>"), _

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].

 

The Need For Metadata

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.

 

Code Should Stand Alone

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.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish