Friday 4 September 2009

How To Create Web Server Controls with Smart Tags

General Assumptions
For the sake of this blog I will assume that you know and understand the differences between web user controls and web server controls. This blog concentrates on the latter and how you can create server controls that will present the developer with intuitive and helpful tools at design time for setting the basic options of the control.

Introduction
Throughout this blog I will be referring to a recent server control I developed that would render a carousel style news feed at run time. I wanted the server control to integrate will into the Visual Studio toolbox and to be easily configurable by the developer at run time using the familiar Smart Tag presentation you're probably familiar with. The code examples in this blog will be in C#.

Requirements of the Control

  1. The control needed to be data bindable.
  2. The control needed to be stylable (i.e. allow the developer to set whatever visual styles they wanted)
  3. The control needed to present as many properties within the Smart Tag as possible
Getting Started
The ASP.NET Server Control template creates the following basic code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MyNamespace
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")]
public class WebCustomControl1 : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? String.Empty : s);
}

set
{
ViewState["Text"] = value;
}
}

protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
}
}

This code is very helpful in itself. First of all, it defines a couple of useful attributes, most notably ToolboxData. This attribute defines the string that will be created in the markup when the developer using your control drags and drops an instance of it onto the designer work area.
Secondly it creates a demo property called "Text" that is defined as the default property by the DefaultProperty attribute. The "Text" demo property has several attributes set:

[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]

I won't go into the details of all of these attributes other than to say that the Category attribute specifies the category that this property will be displayed under on the properties grid for the control (within the Visual Studio design environment), and the DefaultValue attribute allows you to set the default value for this property.

Moving On
You may have noticed that the template generated code inherits the WebControl class. This is the base class for all web server control, but is not the only one available to you. Check out the MSDN documentation to view a complete list of base classes you can extend to suit your needs. Because my control was to be a data bound control I extended the DataBoundControl class, as follows:

public class DataBoundCarousel : DataBoundControl, INamingContainer

I won't go any further into the mechanics of my control as the purpose of this blog is to help you understand what's necessary in order to provide other developers with the cool Smart Tags that are available in VS.

Creating the Designer Class

The Designer for your control is defined with the Designer attribute, declared above the class declaration, as follows:

[ToolboxData("<{0}:DataBoundCarousel runat=server></{0}:DataBoundCarousel>")]
[Designer(typeof(DataBoundCarouselControlDesigner))]
public class DataBoundCarousel : DataBoundControl, INamingContainer
The DataBoundCarouselControlDesigner class is actually a very basic class which extends the appropriate base designer class. In my case this was the DataBoundControlDesigner class, but for your needs may be different. There are a couple of members of the base class that you need to implement in your class. The GetDesignTimeHtml method can be overriden if you want your class to return custom HTML at design time.

Getting an understanding of this Designer class took me a little while and a good few code examples before it sunk in. Let's look at the example from my code. First of all you must override the Initialize method, which takes an IComponent paramter. This parameter will be passed by the designer in VS and will be a handle to your server control. Store this in a private field within your Designer:


private DataBoundCarousel _myControl;
public override void Initialize(IComponent component)
{
base.Initialize(component);
_myControl = (DataBoundCarousel)component;
}


Moving on to the most important base class to override, the ActionLists property. This property returns a DesignerActionListCollection object.

public override DesignerActionListCollection ActionLists
{
get
{
_actionLists = new DesignerActionListCollection();
_actionLists.AddRange(base.ActionLists);
_actionLists.Add(new DataBoundActionList(this));

return _actionLists;
}
}
This read-only property instantiates a private field as a new DesignerActionListCollection object. It adds any ActionLists from the base class and then, most importantly, adds a new custom object, DataBoundActionList, which we'll look at in detail next. In my code the
DataBoundActionList class is a private class within the Designer class. This means that it has access to the _myControl private field member of the Designer class. Let's look at some important parts of this class. First of all, its definition and constructor:

private class DataBoundActionList : DesignerActionList
{
private DataBoundCarouselControlDesigner _parent;

public DataBoundActionList(DataBoundCarouselControlDesigner parent)
: base(parent.Component)
{
_parent = parent;
}
Notice that the constructor receives a handle to the containing Designer class, which it then stores in a private member field.

Before going any further into the description of the DataBoundActionList class, I'll now go straight into describing the styling properties of my contro, which will then lead me back nicely to the DataBoundActionList class.

I defined several areas of my control that would apply different CSS classes at run time. Rather than hard code these CSS classes into the rendered HTML I wanted to allow the developer using my control to set the contents of these CSS classes at design time. I therefore created properties of my control that would take the styling created by the developer. Here's an example of one such property:

[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Editor(typeof(MultiLineEditor), typeof(System.Drawing.Design.UITypeEditor))]
public string ImageStyle
{
get
{
string _imageStyle = "";

if (ViewState["ImageStyle"] != null)
{
_imageStyle = (string)ViewState["ImageStyle"];
}

return _imageStyle;
}

set
{
ViewState["ImageStyle"] = value;

if (IsDesignMode)
{
//Notification so that the VS PropertyGrid detects the change
IComponentChangeService _changeService = (IComponentChangeService)this.Site.GetService(typeof(IComponentChangeService));
_changeService.OnComponentChanged(this, TypeDescriptor.GetProperties(this).Find("ImageStyle", true), "foo", "foo2");
}
}
}

The discerning reader will notice a couple of things in the above code that I haven't as yet mentioned. For example, what does the Editor attribute do? Where is MultiLineEditor defined? What is the IsDesignMode property?

1) The
Editor attribute allows us to define a control that the property grid will display when the developer clicks on the property in the Property Grid. I chose to create a custom control, because I wanted the developer using my control to have a multiline textbox to edit their CSS in.

2) The complete code for my
MultiLineEditor class is below:

[System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
public class MultiLineEditor : UITypeEditor
{
public MultiLineEditor()
{
}

public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}

//displays the UI for value selection
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if (value.GetType() != typeof(string) )
{
return value;
}

IWindowsFormsEditorService _editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (_editorService != null)
{
//Display a multiline text box
TextBox _textBox = new TextBox();
_textBox.Text = (string)value;
_textBox.Multiline = true;
_textBox.AcceptsReturn = true;
_textBox.Height = 125;

_editorService.DropDownControl(_textBox);
return _textBox.Text;
}
return value;
}

}
A few things worthy of note here... First, GetEditStyle method allows us to return the way that the designer will present the control. The enumeration is basically, None, Modal or DropDown. Second, the EditValue method is where we create the control that will be returned. I'm not going to lie and say that I fully understand the need for the IWindowsFormsEditorService object, but needless to say, it works. And, if it ain't broke...

3) IsDesignMode is a property that allows me to evaluate whether or not the code is being run from the VS design mode, or at run time. Although there is a DesignMode property a small caveat comes into play here in that the
DesignMode
property only returns true if the hosting control is in design mode. This custom property allows the code to get a more useable handle on the real design mode of the control.


private bool IsDesignMode
{
get
{
Control ctrl = this;
while (ctrl != null)
{
if (ctrl.Site == null)
return false;
if (ctrl.Site.DesignMode == true)
return true;
ctrl = ctrl.Parent;
}
return false;
}
}
So far, I've shown you the code that will allow the proper rendering of the property in the VS Property Grid, but we now need to return to the DataBoundActionList class to see what needs to be done to render a Smart Tag.

The base DesignerActionList class of my DataBoundActionList class contains a method that must be overriden, called GetSortedActionItems. This method is where we return each of the items that will appear on our Smart Tag.

public override DesignerActionItemCollection GetSortedActionItems()
{
DesignerActionItemCollection _items = new DesignerActionItemCollection();
_items.Add(new DesignerActionHeaderItem("Configure Data"));
_items.Add(new DesignerActionPropertyItem("SortOrder", "Sort Order", "Configure Data"));
_items.Add(new DesignerActionMethodItem(this, "ConfigureDataBindings", "Configure..."));

_items.Add(new DesignerActionHeaderItem("Styles"));
_items.Add(new DesignerActionPropertyItem("SummaryStyle", "Summary Style:", "Style", "Sets the style of the summary section of the carousel"));
_items.Add(new DesignerActionPropertyItem("TabStyle", "Tab Style:", "Style", "Sets the style of the tabs on the carousel"));
_items.Add(new DesignerActionPropertyItem("CurrentTabStyle", "Current Tab Style:", "Style", "Sets the style of the currently selected tabe on the carousel"));
_items.Add(new DesignerActionPropertyItem("ImageStyle", "Image Style:", "Style", "Sets the style of the images displayed on the carousel"));

_items.Add(new DesignerActionHeaderItem("Misc"));
_items.Add(new DesignerActionPropertyItem("CycleInterval", "Animation Cycle (ms):", "Misc", "Sets the style of the images displayed on the carousel"));
_items.Add(new DesignerActionPropertyItem("MaxNumberToDisplay", "Max. number of articles:", "Misc", "Sets the maximum numbeer of articles displayed on the carousel"));
_items.Add(new DesignerActionPropertyItem("EmbedJQuery", "Embed jQuery", "Misc", "Switch off jQuery embedding if the hosting page already contains a reference to the jQuery library"));
_items.Add(new DesignerActionPropertyItem("AutoAnimate", "AutoAnimate", "Misc", "Switch off auto animation when you do not want the carousel to automatically rotate through articles"));

return _items;
}
There are various Designer Action Items that can be added, but I'm going to focus here on the DesignerActionPropertyItem class. This class takes the name of the property as its first constructor argument, the display text as its second, the section of the Smart Tag it will reside in as its third, and a description of the property as its fourth. So, let's look at the line that defines the Smart Tag panel item for the ImageStyle property:

_items.Add(new DesignerActionPropertyItem("ImageStyle", "Image Style:", "Style", "Sets the style of the images displayed on the carousel"));
Now, let's look at the code of the property that this panel item will call:


[Editor(typeof(MultiLineEditor), typeof(System.Drawing.Design.UITypeEditor))]
public string ImageStyle
{
get
{
return _parent._myControl.ImageStyle;
}
set
{
_parent._myControl.ImageStyle = value;
}
}
Notice that this property sets and gets the corresponding property of the control. Remember that we are here looking at the code at the level of the DesignerActionList class. This class is purely for use at design time. What we're really interested in is setting the properties of our control for use at run time.

Notifying the Design Environment
One of the quirks I came across during th development of my control was that the markup generated for my control wouldn't update when I changed the values of properties from within my Smart Tag panel. I was preplexed for a little while about this and eventually found out how to raise an event that would notify the design environment when my property had changed. Basically, in the setter for each of the properties on my control I call the following code:
               if (IsDesignMode)
{
//Notification so that the VS PropertyGrid detects the change
IComponentChangeService _changeService = (IComponentChangeService)this.Site.GetService(typeof(IComponentChangeService));
_changeService.OnComponentChanged(this, TypeDescriptor.GetProperties(this).Find("ImageStyle", true), "foo", "foo2");
}
Knowing this in advance could save you a load of hassle at development time.

Conclusion
If you're developing custom server controls at this kind of level you aren't a beginner programmer. So, I'll hope you will excuse the fact that a lot of code has been omitted from this blog. I've provided code examples for all of the important details and I hope that there's enough information here to get you rolling with the development of your own controls.
Don't forget that all of the classes and ample documentation is provided on MSDN.


No comments:

Post a Comment