Monday, March 24, 2008

Custom Controls

Introduction(Design Web Control)

Post back data is not the only thing that you may want to deal with in your custom controls. You may want your control to cause a post back and raise some server side event such as Click or SelectedIndexChanged. Handling a post back event requires that your control implements IPostBackEventHandler interface. In this article I explain how that can be done.
IPostBackEventHandler Interface

The IPostBackEventHandler interface must be implemented by your custom control class if it requires to handle post back event. This interface contains a single method as shown below:

void RaisePostBackEvent(string eventArgument)

The PaisePostBackEvent() method gives you a chance to raise a server side post back event (e.g. Click or SelectedIndexChanged). The method receives an event argument parameter of type string which may contain event argument data if any.
Example

Let's create a simple custom control that implements IPostBackEventHandler interface and raises Click event upon post back. Begin by creating a new web site in Visual Studio.

Add App_Code to your web site and further add a new class named GraphicButton. The following code shows the class definition.

namespace BinaryIntellect.UI
{
public class GraphicButton : WebControl, IPostBackEventHandler
{
public GraphicButton(): base(HtmlTextWriterTag.Span)
{

}
...

Here, you create a class named GraphicButton that resides in BinaryIntellect.UI namespace. The GraphicButton class inherits from WebControl base class and implements IPostBackEventHandler interface. The constructor of GraphicButton class calls its base class constructor by passing HtmlTextWriterTag. This indicates that your control builds over tag.

Now add two public properties viz. ImageUrl and Text to the GraphicButton class.

public string ImageUrl
{
get { return ViewState["imgurl"] as string; }
set { ViewState["imgurl"] = value; }
}

public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

The ImageUrl property specifies the image URL to be displayed and Text property specifies the text to be rendered. Both of the property values are stored in ViewState. Then declare Click event as shown below:

public event EventHandler Click;

The Click event is raised when user clicks on the image rendered by the GraphicButton control. Now override the RenderContents() method of the WebControl base class and emit the required markup.

protected override void RenderContents(HtmlTextWriter writer)
{
base.RenderContents(writer);
writer.WriteFullBeginTag("center");
writer.WriteBeginTag("img");
writer.WriteAttribute("name", this.UniqueID);
writer.WriteAttribute("src", ImageUrl);
writer.WriteAttribute("onclick",
Page.ClientScript.GetPostBackEventReference
(this, string.Empty));
writer.Write("/>");
writer.Write("
");
writer.Write(Text);
writer.WriteEndTag("center");
}

Here, we render an image tag and set its name and src attributes. It also renders the Text property value below the image. In order that the image tag triggers a post back event you need to handle its client side onclick event handler. Notice the call to GetPostBackEventReference() method. This method returns a javascript function (__doPostBack()) that actually causes the post back. The GetPostBackEventReference() method accepts two parameters viz. the control that will receive the post back and event arguments if any. If you supply some event arguments then they will be passed on to RaisePostBackEvent() method.

Finally, implement the IPostBackEventHandler interface by writing RaisePostBackEvent() method.

public void RaisePostBackEvent(string eventArgument)
{
Click(this, EventArgs.Empty);
}

Here, you simply raise the Click event of GraphicButton control.

This completes the control. In order to use it on a web form register it with the page framework as shown below:

<%@ Register
Namespace="BinaryIntellect.UI"
TagPrefix="mycontrols" %>

Also create an instance of the GraphicButton control on the web form.

Text="Save changes to database"
ImageUrl="SaveButton.gif"
onclick="button1_Click"
Font-Bold="True" />

Set the ImageUrl and Text properties to some image URL and text respectively and run the web form. Handle the Click event of the GraphicButton as follows:

protected void button1_Click
(object sender, EventArgs e)
{
Label1.Text = "Button clicked";
}

The following figure shows a sample run of the web form.




Introduction(Enhance Web Control)

Developing a nice custom control is just one part of the story. As a control author you should also pay attention about the experience of other developers who will be using your control. In most of the real world cases developers use Visual Studio as the IDE for developing .NET applications. You can enhance the experience of other developers using your control by providing proper designer support. For example, you can control how your control properties and events are displayed in property window and toolbox. A set of attributes often called as Design Time Attributes allow you to accomplish this.
Common Design Time Attributes

The following sections explain the common design time attributes that allow you to change how the control behaves in the property window and toolbox. Most of these attributes reside in System.ComponentModel namespace. In the following sections we use the GraphicButton control that we created earlier in this series.
Deciding whether a property will be visible in the property window

By default all the public properties of a custom control are displayed in the property window. Sometimes you may want that developers should set a property only via code and not via property window. The [Browsable] attribute allows you to control just that. The usage of this attribute is as follows:

[Browsable(false)]
public string ImageUrl
{
get { return ViewState["imgurl"] as string; }
set { ViewState["imgurl"] = value; }
}

The value of false indicates that the ImageUrl property will not be displayed in the property window.
Adding description to a property

Have a look at the screen capture below:

Can you notice a one line description of the Text property at the bottom portion of the property window? Such description can be added using [Description] attribute.

[Description("Specifies the caption of the graphic button")]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

Grouping related properties

The property windows allows you list properties either alphabetically or grouped in categories. Grouping properties by category makes it simple to locate them as per their functionality. Have a look below where the ImageUrl and Text properties are grouped together under Control Values category.

The [Category] attribute allows you to specify category for your property.

[Category("Control Value")]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

Enabling data binding for a property

You may want to use your custom control as a child control of some data bound control (say DataList). Further, you may want to data bind some of the control properties. In other words you want to list your control properties in the data bindings editor as shown below:

The [Bindable] attribute enables this behavior for you.

[Bindable(true)]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

Making a property read only

At times you may want that a control property be listed in the property window but it should be read only. This behavior can enabled using [ReadOnly] attribute.

[ReadOnly(true)]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

Setting a property of multiple control instances

When you select multiple instance of the same control and set a property in the property window all the instances bear the same property value. You may not always this default behavior. You can use [MergableProperty] attribute to indicate this to the IDE.

[MergableProperty(true)]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

Refreshing other properties when a property value changes

Some times your properties are interdependent in that if value of one property changes then it causes value of some other property to change. In such cases you would like to refresh the property window when a property value changes. The [RefreshProperties] attribute allows you to indicate such a behavior.

[RefreshProperties(RefreshProperties.All)]
public string Text
{
get { return ViewState["text"] as string; }
set { ViewState["text"] = value; }
}

The [RefreshProperties] attribute accepts a parameter of type RefreshProperties enumeration. The three possible enumeration values are None, All and Repaint.
Setting a default property and event

When yu drag and drop a control instance on the web form you may want to show a particular property (or event) selected by default in the property window. This can be accomplished with the help of [DefaultProperty] and [DefaultEvent] attributes respectively.

[DefaultEvent("Click")]
[DefaultProperty("Text")]
public class GraphicButton :
WebControl, IPostBackEventHandler
...

Notice that unlike other attributes we discussed so far the [DefaultProperty] and [DefaultEvent] attributes are class level attributes. Both of these attributes take a string parameter indicating the name of the default property and event respectively.
Changing icon displayed in the toolbox

By default a newly created custom control is shown in the toolbox as shown below (see the gear icon):

You may want to change the default icon displayed along with your control. You do this by adding a bitmap file to your custom control project and naming it same as the custom control class. For example, for our BinaryIntellect.UI.GraphicButton control you would add a bitmap with name BinaryIntellect.UI.GraphicButton.bmp. The bitmap must be 16x16 pixels and should use no more than 16 colors.

There is another way to set toolbox bitmap - the [ToolboxBitmap] class level attribute.

[ToolboxBitmap(@"C:\BinaryIntellect.UI.GraphicButton.bmp")]

The [ToolboxBitmap] attribute takes the full path of the bitmap file. Remember that the [ToolboxBitmap] attribute resides in System.Drawing namespace.

The following figure shows how a custom bitmap looks like in the toolbox.


Introduction(Custom Control Complex Type Code Serialization)

Whenever you set any property of a control in the property window, the property window needs to save this property value in the .aspx file. This process is known as code serialization. For properties that are of simple types (such as integer and string) this code serialization happens automatically. However, when property data types are user defined complex types then you need to do that work yourself. This is done via what is called as Type Converters. This article is going to examine what type converters are and how to create one for your custom control.
Type Converters

A type converter is a class that converts values entered in the property window to the actual data type of the property and vice a versa. The type converter class is inherited from TypeConverter or ExpandableObjectConverter base class. If you inherit from TypeConverter base class then you need to supply a delimited string in the property window where each part of the string corresponds to a property of the underlying complex type. If you inherit from ExpandableObjectConverter base class then Visual Studio makes your job easy by providing an expandable tree to enter the individual property values. The following figure shows how this expandable region looks like:

Creating an Expandable Type Converter

As an example of creating a type converter let's assume that you have a custom control that displays full name in the web form. The Name property of the control allows you to specify the full name to be displayed. The Name property is of type FullName. The FullName class consists of two public properties namely FirstName and LastName.
Creating FullName class

To begin developing this example first of all create a new Web Control project in Visual Studio. Add a new class to the project and name it as FullName. Code the FullName class as shown below:

[Serializable]
public class FullName
{
private string strFName;
private string strLName;

public string FirstName
{
get
{
return strFName;
}
set
{
strFName = value;
}
}

public string LastName
{
get
{
return strLName;
}
set
{
strLName = value;
}
}

public FullName()
{
}

public FullName(string fname, string lname)
{
strFName = fname;
strLName = lname;
}
}

The FullName class simply consists of two public properties namely FirstName and LastName. Notice that the FullName class is marked as [Serializable]
Creating FullNameConverter class

Now add another class to the project and name it as FullNameConvertor. Inherit the FullNameConvertor class from ExpandableObjectConverter base class. As a convention the type converter class names should have name of the class they convert attached with "Converter" at the end. Once created you need to override certain methods of the base class. These methods are explained next.

* bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
The CanConvertFrom method tells the property window whether the source type can be converted to the property data type. Most of the cases you will ensure that if the source type is string then the method returns true; false otherwise.
* bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
The CanConvertTo method tells the property window whether a property value can be converted to the destination data type. Most of the cases you will ensure that if the destination type is string then the method returns true; false otherwise.
* object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
The actual task of converting a source value (string) into the destination type (FullName) is done inside ConvertFrom method
* object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
The actual task of converting a property value (FullName) to the destination type (string) is done inside ConvertTo method.

The following code shows all these methods for FullNameConverter class.

public override bool CanConvertFrom
(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
else
{
return base.CanConvertFrom(context, sourceType);
}
}

public override bool CanConvertTo
(ITypeDescriptorContext context,
Type destinationType)
{
if (destinationType == typeof(string))
{
return true;
}
else
{
return base.CanConvertTo(context, destinationType);
}
}

public override object ConvertFrom
(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value)
{
if(value is string)
{
string[] names = ((string)value).Split(' ');
if (names.Length == 2)
{
return new FullName(names[0],names[1]);
}
else
{
return new FullName();
}
}
else
{
return base.ConvertFrom(context,culture,value);
}
}

public override object ConvertTo
(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value, Type destinationType)
{
if (value is string)
{
FullName name = value as FullName;
return name.FirstName + " " + name.LastName;
}
else
{
return base.ConvertTo(context, culture, value,
destinationType);
}
}

The CanConvertFrom() method checks if the data type of proposed value is string. If so it returns true otherwise base class version of CanConvertFrom() method is called. Similar job is done inside CanConvertTo() method. Remember that these two methods though sound similar are called at different times. The CanConvertFrom() method is called when you enter a value in the property window whereas CanConvertTo() method is called when property window reads previously serialized property value.

The ConvertFrom() method converts a supplied string value into an instance of type FullName. It does so by splitting the source string (e.g. Nancy Davalio) at the occurrence of a white space. An instance of FullName class is returned based on the supplied FirstName and LastName values.

The ConvertTo() method does reverse of ConvertFrom() method. It converts a FullName instance into its string representation. This is done by simply concatenating FirstName property, a white space and the LastName property. The resultant string is returned from the ConvertTo() method.

Note that in the above example we inherited FullNameConverter class from ExpandableObjectConverter base class. Even if you inherit from TypeConverter base class the process of overriding the methods remains the same.
Attaching type converter to FullName class

Now that you have completed the FullNameConverter class it's time to attach it to the FullName class. This is done as follows:

[TypeConverter(typeof(FullNameConvertor))]
[Serializable]
public class FullName
...

You need to decorate the FullName class with [TypeConverter] attribute. The TypeConverter attribute accepts the type information of a class that is acting as a type converter for this class.
Synchronizing markup and property window

Whenever you make any change in the property window immediately the new values should be saved to the .aspx file. To enable this behavior you need to mark the FirstName and LastName properties with the following additional attributes.

[RefreshProperties(RefreshProperties.All)]
[NotifyParentProperty(true)]

The RefreshProperties() attribute should be familiar to you because we discussed it in the previous article of this series. It simply refreshes the property window by re-querying all the property values. More important is the NotifyParentProperty() attribute. This attribute governs whether the parent property (Name) is to be notified when any of the child properties (FirstName and LastName) are changed. This way the parent property can reflect the newly assigned values.
Coding the custom control

Now it's time to develop our custom control. We will call it as FullNameLabel and it resembles as shown below:

public class FullNameLabel : WebControl
{
[DesignerSerializationVisibility
(DesignerSerializationVisibility.Visible)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public FullName Name
{
get
{
return ViewState["_fullname"] as FullName;
}
set
{
ViewState["_fullname"] = value;
}
}

protected override void Render(HtmlTextWriter writer)
{
if (Name != null)
{
writer.WriteFullBeginTag("span");
writer.Write(Name.FirstName);
writer.Write(" ");
writer.Write(Name.LastName);
writer.WriteEndTag("span");
}
}
}

The code inside the FullNameLabel control is not a rocket science. It simply declares a public property called Name that is of type FullName. It then emits the first name and last name separated by a white space in the overridden Render() method.

Carefully notice the declaration of FullNameLabel class. It is marked with two special attributes viz. [DesignerSerializationVisibility] and [PersistenceMode]. The [DesignerSerializationVisibility] attribute governs whether a property will be serialized. The DesignerSerializationVisibility enumeration has four possible values viz. Default, Visible, Content, Hidden. The value of Content indicates that the contents of the property will be serialized.

The [PersistenceMode] attribute governs how a property will be serialized. The PersistenceMode enumeration has four values namely Attribute, InnerProperty, InnerDefaultProperty and EncodedInnerDefaultProperty. The value of InnerProperty indicates that the Name property will be serialized as a nested tag of the custom control tag.

You can see details of other enumerated values of DesignerSerializationVisibility and PersistenceMode enumeration in MSDN help.

If you use the FullNameLabel control on a web form you should see its Name property in the property window as shown below:

Notice how the FirstName and LastName properties appear as sub-properties of Name property. The serialized markup of the control after the Name property is set looks like this:

runat="server" EnableTheming="True">



Notice how the Name tag appears as a nested tag of tag.


Referrence articles

Create Custom Control
Enhance Experience
Code Serialization