Designer Integration





Designer Integration

Many controls are created with reuse in mind, whether it is across multiple applications within one company or perhaps for sale and distribution. Reusable controls should take advantage of designer integration so that they integrate into the Visual Studio .NET design environment. With designer integration you can control

  • How your control appears in the Toolbox

  • The appearance of the control's tag on the page

  • The appearance of the control's @Register directive on the page

  • How properties are categorized in the designer

  • Editable string formats for any complex property types

  • Custom GUIs for editing properties, if necessary

  • The designer view of your control

Adding designer integration to your control is the final, critical step in making your control accessible to developers who want to use it.

1 Properties and Appearance

When a control you build is used in Visual Studio .NET, the first thing users of your control will see is the cool 16-by-16 graphic that represents your control in the Toolbox—or the lack thereof. To associate a Toolbox image with your control, you must create a bitmap that is 16 by 16 pixels. The lower left pixel of the bitmap is used as the transparent color when the control is rendered in the Toolbox. The file name of the bitmap must be the full class name of the control class with which it is associated. For example, if you have defined a control class named Control1 in the MyControls namespace, the bitmap file for that control must be named MyControls.Control1.bmp. Finally, the bitmap needs to be compiled into the control assembly as an embedded resource. You can do this in a Visual Studio .NET project by setting the Build Action of the bitmap file to Embedded Resource. Be aware that Visual Studio .NET prepends the default namespace associated with your project to any embedded resources, so you must either take that into consideration when naming your bitmap file to ensure that it matches the control class name, or remove the default namespace from your project altogether (accessible through the project properties dialog). In a command-line compilation, you can embed a bitmap as a resource by using the /resource switch referencing the bitmap file. The entire process of associating a Toolbox bitmap with your control is shown in Figure.

Setting the Toolbox Bitmap for Your Control

graphics/08fig06.gif

The second thing users of your control will notice is the tag that is placed into the .aspx page. You can control what this tag looks like through the class-level attribute ToolboxData. This attribute takes a string as a parameter that is the tag you would like rendered for your control, using the placeholder {0} wherever a tag prefix is used (which is also controllable through an assembly-level attribute). Listing 8-39 shows a custom control that specifies the tag that should be used through the TooboxData attribute. Note that you can specify any initial attributes or content you would like the tags for your control to have when they are first placed on a form.

-39 Specifying a Client Tag for a Custom Control
<ToolboxData("<{0}:BarGraphControl runat='server'/>")> _
Public Class BarGraphControl
  Inherits WebControl
 '...
End Class

In addition to the tag for your control, you can specify how the @Register directive should appear for your control when users place an instance of your control on their form for the first time. To do this, you specify an assembly-level attribute called TagPrefix, which takes two strings as parameters: the namespace of your control and the tag prefix to use when placing tags for your control on a form. Listing 8-40 shows an example of using the TagPrefix attribute to customize the @Register directive generated by Visual Studio .NET for a custom control.

-40 Customizing the @Register Directive for a Custom Control
<assembly: TagPrefix("EssentialAspDotNet.CustomControls", _
                     "eadn")>

A number of attributes can be applied to properties of a control to influence their appearance and usage from within a designer. Figure shows a complete list of the attributes that apply to control properties. The two that should most often be applied are the Category and Browsable attributes, which control under which category a property should appear and whether it should appear at all.

Control Property Attributes

Attribute

Values

Description

Bindable

BindableSupport.[Yes, No, Default]

Should this property be displayed in the DataBindings dialog?

Category

Any string

Property category this property should appear under in the designer (Appearance, Behavior, ...)

DefaultValue

Any value

The default value this property should take when the control is first created

PersistenceMode

PersistenceMode. [Attribute, Default, EncodedInnerDefaultProperty, InnerDefaultProperty]

Whether changes made to the value of this property are persisted (and how)

Browsable

true, false

Whether this property is displayed in the designer

TypeConverter

Type of a class that derives from TypeConverter

Class to use to convert the string in the designer to the type required by this property

Editor

Type of a class that derives from UITypeEditor

Class to provide a custom interface for editing this property

2 Type Converters

You can also define custom type converters if your control has complex properties that may need special UI support in the designer. Because properties for controls can be specified as attributes (by default) within a control tag on a form, all property types need a way to be converted to and from a string representation. For many properties, this happens automatically because there are several built-in converter classes, as shown in Figure.

Built-in Type Converters

Converter Class

ArrayConverter

BooleanConverter

ByteConverter

CharConverter

CollectionConverter

ComponentConverter

CultureInfoConverter

DateTimeConverter

DecimalConverter

DoubleConverter

EnumConverter

Int16Converter

Int32Converter

Int64Converter

ReferenceConverter

UInt16Converter

UInt32Converter

UInt64Converter

ColorConverter

WebColorConverter

FontConverter

For other property types, however, you need to provide a custom converter to allow clients to specify property values within a control tag. To provide a custom converter for a property in your control, you must create a new class deriving from System.ComponentModel.TypeConverter to perform the conversions, and you must associate that type converter class with your property by using the TypeConverter attribute. For example, suppose we built a control that exposed a composite property to set the attributes of a Dog, including its name, age, and breed. These attributes could be encapsulated into a structure and exposed as subproperties, as shown in Listing 8-41.

-41 The Dog Structure
Public Enum eBreed
  Dalmation
  Labrador
  GoldenRetriever
  Mutt
  BlackLabradorRetriever
End Enum

<TypeConverter(GetType(DogConverter)), _
 Editor(GetType(DogEditor), GetType(UITypeEditor))> _
Public Structure Dog
  Private _name As String
  Private _breed As eBreed
  Private _age As Integer
  Public Sub New(ByVal name As String, _
                 ByVal breed As eBreed, _
                 ByVal age As Integer)
    _name = name
    _breed = breed
    _age = age
  End Sub

  Public Property Name() As String
    Get
      Return _name
    End Get
    Set(ByVal Value As String)
      _name = Value
    End Set
  End Property
  Public Property Breed() As eBreed
    Get
      Return _breed
    End Get
    Set(ByVal Value As eBreed)
      _breed = Value
    End Set
  End Property
  Public Property Age() As Integer
    Get
      Return _age
    End Get
    Set(ByVal Value As Integer)
      _age = Value
    End Set
  End Property
End Structure

If a custom control exposed the Dog structure as a property without adding a TypeConverter, users of this control would only be able to set the value of the dog property programmatically. For users to be able to manipulate the subproperties of the Dog structure through the designer, we must provide a way to convert the structure to and from a string representation. Listing 8-42 shows the implementation of DogConverter, a class that provides conversion between the Dog data type and its equivalent string representation (which, in this case, we have chosen to be "name, breed, age").

-42 A Custom Type Converter for Dog
Public Class DogConverter
       Inherits ExpandableObjectConverter

  Public Overloads Overrides Function _
    CanConvertFrom(context As ITypeDescriptorContext, _
                   destType As Type) As Boolean
    If destType Is GetType(String) Then
       Return True
    Else
       Return MyBase.CanConvertFrom(context, destType)
    End If
  End Function

  Public Overloads Overrides Function _
    CanConvertTo(context As ITypeDescriptorContext, _
                 destType As Type) As Boolean
    If destType Is GetType(InstanceDescriptor) Or _
       destType Is GetType(String) Then
       Return True
    End If
    Return MyBase.CanConvertTo(context, destType)
  End Function

  Public Overloads Overrides Function _
    ConvertFrom(context As ITypeDescriptorContext, _
                culture As CultureInfo, value As Object) _
                As Object
    ' Parse the string format, which is: name,breed,age
    Dim sValue As String = CType(value, String)
    If Not sValue = Nothing Then
       Dim v As String() = sValue.Split(New Char() {","})
       Return New Dog(v(0), [Enum].Parse(GetType(eBreed), _
                      v(1)), [Integer].Parse(v(2)))
     Else
       Return MyBase.ConvertFrom(context, culture, value)
     End If
   End Function

   Public Overloads Overrides Function _
     ConvertTo(context As ITypeDescriptorContext, _
               culture As CultureInfo, value As Object, _
               destinationType As Type) As Object
     Dim dg As Dog = value

     If destinationType Is GetType(InstanceDescriptor) Then
        Return New InstanceDescriptor( _
           GetType(Dog).GetConstructor( _
            New Type() {GetType(String), GetType(eBreed), _
                        GetType(Integer)}), _
            New Object() {dg.Name, dg.Breed, dg.Age})
     Else
        Return String.Format("{0},{1},{2}", dg.Name, _
                             dg.Breed, dg.Age)
     End If

     Return MyBase.ConvertTo(context, culture, value, _
                             destinationType)
  End Function
End Class

Note that four core methods must be overridden in a type converter. First, the CanConvertFrom and CanConvertTo methods indicate what types this class can convert from and to. In our case, the purpose of providing this class is to convert from a Dog to a string and back again. Second, the ConvertFrom method requests that the class perform a conversion from a given type to the real type, which in our case will always be from a string to a Dog. Finally, the ConvertTo method involves taking a reference to the real type and returning a conversion of it into the target type, which in our case will always be a string. The one additional requirement of the designer is that it be able to generate code to create an instance of your type. To provide this information, we must also support conversion to an InstanceDescriptor, which is a class that stores information on how to create an instance of a particular class so that the designer can correctly rehydrate your class from its persistent store. In our case, we need to provide an InstanceDescriptor that describes how to invoke the three-parameter constructor of our Dog structure, shown in our implementation of ConvertTo.

The last step is to apply this custom type converter to our Dog structure, which we do by using the TypeConverter attribute, as shown in Listing 8-43.

-43 Applying a Type Converter to a Type
<TypeConverter(GetType(DogConverter))> _
Public Structure Dog
'...
End Structure

Once a property has a valid converter associated with it, the designer lets the user modify the property value through the property window, taking whatever string is passed in, running it through the converter, and assigning it to the control's property. If you would like the user to be able to edit the subproperties of your type individually, you can derive your type converter from a special derivative of TypeConverter called ExpandableObjectTypeConverter. Figure shows what the property editor looks like when the DogConverter class is derived from Expandable ObjectTypeConverter.

Expandable Properties

graphics/08fig07.gif

3 Property Editors

Some control authors may want to take this one step further and provide their own custom editors for users to edit properties with. Instead of having users type a formatted string in the property editor, they can associate an editor with a property that can launch a form or dialog to edit the property. Several built-in editors are available, as shown in Figure.

Built-in Editors

Editor Class

DataBindingCollectionEditor

ImageUrlEditor

UrlEditor

XmlFileEditor

XmlUrlEditor

XslUrlEditor

For an example of using a custom editor, consider a class that maintains a URL property. If would be nice if instead of having users type in any random string for this property, there were a selection dialog that helped them construct a proper URL. By using the Editor attribute on the string property used to access and set the URL, the control builder can associate the built-in UrlEditor class to do just this, as shown in Figure. Note that the Editor takes two parameters, the type of the editor class and the base class from which it inherits, which currently should always be System.Drawing.Design.UITypeEditor.

Associating a Custom Editor with a Control Property

graphics/08fig08.gif

To create your own custom editor for a property, you derive a new class from System.Drawing.Design.UITypeEditor and override the EditValue and GetEditStyle methods. The GetEditStyle returns an enumeration indicating what type of user interface the editor is going to use: modal dialog, drop-down box from within the property list, or none. EditValue takes a reference to the object it is editing and returns a new object with new values. What happens inside EditValue is completely up to you, but most often it invokes a modal dialog box with controls on it to edit the values for the object. Listing 8-44 shows an example of a custom editor for editing the Dog structure shown earlier and how to hook it up to the Dog structure using the Editor attribute.

-44 A Custom Editor Example
Public Class DogEditor
       Inherits UITypeEditor

  Public Overloads Overrides Function _
    EditValue(tdc As ITypeDescriptorContext, _
              sp As IServiceProvider, obj As Object) _
              As Object
    Dim dg As Dog = obj
    Dim dlg As DogEditorDlg = New DogEditorDlg()
    dlg.DogName.Text = dg.Name
    dlg.DogAge.Text = dg.Age.ToString()
    dlg.DogBreed.SelectedIndex = _
            dlg.DogBreed.Items.IndexOf(dg.Breed.ToString())

    If dlg.ShowDialog() = DialogResult.OK Then
       Return New Dog(dlg.DogName.Text, _
                   [Enum].Parse(GetType(eBreed), _
                   dlg.DogBreed.SelectedItem.ToString()), _
                   dlg.DogAge.Text)
    Else
       Return obj ' no change
    End If

  End Function

  Public Overloads Overrides Function _
    GetEditStyle(tdc As ITypeDescriptorContext) _
    As UITypeEditorEditStyle

    Return UITypeEditorEditStyle.Modal
  End Function
End Class

' To hook up this editor to the Dog structure:
< Editor(typeof(DogEditor), typeof(UITypeEditor))> _
Public Structure Dog
'...
End Structure

4 Designers

For each control you create, you can create an accompanying designer class that determines how that control appears and behaves within the designer. For many controls, this is unnecessary because the Visual Studio .NET designer creates an instance of your control when it is placed on a form and asks it to render itself. For controls whose rendering depends on runtime data population or that have no visual presence on a form, however, it makes sense to have something show up when such controls are dropped on a form in the designer. Whether this means artificially populating a control with initial data or providing a "stub" rendering indicating that it is a placeholder depends on the type of control.

Creating a designer involves creating a new class derived from System.Web.UI.Design.ControlDesigner and overriding its GetDesignTimeHtml method. This method should return the HTML you want to represent your control at design time. The last step is to associate the designer with your control class by using the Designer attribute. Listing 8-45 shows a custom control class with an associated designer. This listing also demonstrates the common practice of placing the designer in a new namespace called Design within the control class's namespace.

-45 A Custom Control with an Associated Designer Class
Namespace Design
  Public Class MyControlDesigner
    Inherits ControlDesigner

    Public Overrides Function GetDesignTimeHtml() As String
      Return "<h3>MyControl in design mode!</h3>"
    End Function
  End Class
End Namespace

<Designer(GetType(Design.MyControlDesigner))>_
Public Class MyControl
  Inherits Control
'...
End Class

Providing a designer for a control does not change the fact that an instance of your control is created in design mode; it merely changes how that control renders itself in a designer. For controls that want to use the standard control rendering in their designer, it is possible to reference the control instance from within the designer via the Component property of the ControlDesigner class. A good example of when this would be appropriate is in the BarGraph control presented earlier in this chapter. This control renders a bar graph with data populated at runtime, so at design time there is nothing to render. To give it a visual presence in the designer, we can access the allocated instance of the control, populate it with some artificial data, and return the result of invoking the control's render function (by calling the base class implementation of GetDesignTimeHtml). This technique is shown in Listing 8-46.

-46 Using a Designer to Artificially Populate the BarGraph Control
Public Class BarGraphDesigner
 Inherits ControlDesigner

  Private bGetDesignTimeHtmlCalled As Boolean = False

  Public Overrides Function GetDesignTimeHtml() As String
    ' populate with data the first time only
    If Not bGetDesignTimeHtmlCalled Then
      Dim ctrl As BarGraphControl = _
          CType(Component, BarGraphControl)
      ' add some artifical data
      ctrl.AddValue("Value 1", 10.0)
      ctrl.AddValue("Value 2", 20.0)
      ctrl.AddValue("Value 3", 30.0)
      ctrl.AddValue("Value 4", 40.0)
      bGetDesignTimeHtmlCalled = True
    End If
    Return NewBase.GetDesignTimeHtml()
  End Function
End Class

The other type of controls that require designers are those that have no runtime rendering or whose rendering depends on too many runtime elements, making rendering in design mode impossible. For these controls, a helper function called CreatePlaceHolderDesignTimeHtml creates a standard placeholder for a control. This method takes a string and renders a gray box with the string displayed at design time, and is used by controls such as the Xml control and the Repeater control in the base class libraries. Listing 8-47 shows a sample designer that renders a placeholder.

-47 Rendering a Placeholder with a Designer
Public Class MyControlDesigner
  Inherits ControlDesigner

  Public Overrides Function GetDesignTimeHtml() As String
    Return CreatePlaceHolderDesignTimeHtml("see me later")
  End Function
End Class

     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows