Introduction
When developing controls for a designer environment like Visual Studio or WebMatrix, the complexities of persisting control properties as serialized HTML can come into play. In many cases, the built-in serialization of control properties as attributes or nested child tags is sufficient. There are also several code attributes available to a control developer to apply a degree of customization for this process. In some cases however, a developer requires a greater degree of control over how control properties are persisted as HTML.
In this article, I present a custom ASP.NET server control for displaying color choices in a dropdown list. Following a description of the control, I will focus on the problem of persisting a custom collection of items in the context of the Visual Studio Web Form designer, and offer a solution using custom ControlDesigner
and ControlBuilder
objects.
HtmlColorDropDown Control
The HtmlColorDropDown
web control encapsulates an HTML dropdown list (i.e., <select>
tag) with colors for items (<option>
tags). The control can render the items themselves as foreground or background colors within the <option>
tags. The choice of colors offered is determined by the value of the Palette
property, which can be one of the following:
AllNamedColors
|
All named colors (not including System colors) available in the System.Drawing.KnownColor enumeration. |
Simple
|
A small subset of common named colors. |
WebSafe
|
The 216 colors considered "web-safe"; these use only hex values #FF, #CC, #99, #66, #33, and #00 for each of the red, green, and blue components. |
UserDefined
|
Colors identified by the control user through the UserDefinedColors property. |
The UserDefinedColors
property is a ColorCollection
object. ColorCollection
is a strongly-typed collection class inheriting from CollectionBase
, using standard System.Drawing.Color
objects for items.
The DisplayColorMode
property governs how colors will be displayed in each item, if at all:
ColorAsBackground
|
Display each item using its color for the background; foreground text is displayed in either black or white, depending on which offers a better contrast. |
ColorAsForeground
|
Display each item using its color for the foreground text. |
Plain
|
Do not display colors in the items. |
The selected item is available as a Color
object through the SelectedColor
property. The properties SelectedColorName
and SelectedColorHexString
return the name of the selected color (for named colors) and the HTML hex value (e.g., "#FFC309") as strings respectively. These are all read/write properties; the setting of one affects the value of all three. If an attempt is made to set the SelectedColor
to a color that doesn't exist in the current Palette
, a value of Color.Empty
is assigned instead. Likewise, if the Palette
is changed, SelectedColor
may be set to Color.Empty
if its existing value isn't among the new list of choices.
Note that the proper display of foreground or background colors rendered in <option>
tags is dependent on the browser. Most modern browsers interpret item colors identified as CSS properties through a style
attribute, without problems. Interestingly however, not all render the colors of the selected item when the dropdown is collapsed. To work around this, the AutoColorChangeSupport
property is provided. When true
(its default value), a small JavaScript onChange
handler is rendered with the control, causing the colors of the selected item to be assigned to the parent <select>
tag. The property may be set to false
to prevent this behavior.
The default event for the control is ColorChanged
. This event fires whenever the selection in the dropdown list has changed between postbacks to the server. The following is an example of an .aspx page where the ColorChanged
event is handled:
<%@ Page language="c#" %>
<%@ Register TagPrefix="cc1" Namespace="UNLV.IAP.WebControls"
Assembly="UNLV.IAP.WebControls.HtmlColorDropDown" %>
<script runat="server">
private void HtmlColorDropDown1_ColorChanged(object sender,
System.EventArgs e)
{
Label1.Text = HtmlColorDropDown1.SelectedColorName;
}
</script>
<html>
<head>
<title>Example</title>
</head>
<body>
<form runat="server">
<cc1:HtmlColorDropDown id="HtmlColorDropDown1" runat="server"
DisplaySelectColorItemText="--Select a Color--"
DisplaySelectColorItem="True"
AutoPostBack="True"
OnColorChanged="HtmlColorDropDown1_ColorChanged">
</cc1:HtmlColorDropDown>
<P>The selected color is: <asp:Label id="Label1" runat="server"/></P>
</form>
</body>
</html>
See the downloadable control documentation for a complete description of all HtmlColorDropDown
properties and events.
The Issue: Persisting the Collection of UserDefinedColors
This control was designed with both visual designer (e.g., Visual Studio) and text editor (e.g., Notepad) environments in mind. For designer environments, we inherit much functionality without any extra code. Because we're using regular System.Drawing.Color
objects for the SelectedColor
property for example, the standard ColorBuilder
becomes available within the Visual Studio web form designer.
Likewise, as we have standard Color
objects within the UserDefinedColors
collection, the Visual Studio collection builder is available at no charge:
A problem occurs however when it comes time to serialize this color collection into the server tag HTML. Typically, we would set the PersistenceMode
and DesignerSerializationVisibility
attributes on the collection property to have the collection items persisted as nested tags, like so:
[
PersistenceMode(PersistenceMode.InnerDefaultProperty)
,DesignerSerializationVisibility(
DesignerSerializationVisibility.Content)
]
public ColorCollection UserDefinedColors
{
get {...}
set {...}
}
With these attributes set, we can get the UserDefinedColors
collection persisted. The markup, however, appears as verbose <System.Drawing.Color>
tags, like the following:
<cc1:htmlcolordropdown id="HtmlColorDropDown1" runat="server"
Palette="UserDefined">
<System.Drawing.Color IsEmpty="False" A="255" B="0" IsNamedColor="True"
IsKnownColor="True" Name="Red" G="0" R="255" IsSystemColor="False">
</System.Drawing.Color>
<System.Drawing.Color IsEmpty="False" A="255" B="0" IsNamedColor="True"
IsKnownColor="True" Name="Green" G="128" R="0" IsSystemColor="False">
</System.Drawing.Color>
<System.Drawing.Color IsEmpty="False" A="255" B="255" IsNamedColor="True"
IsKnownColor="True" Name="Blue" G="0" R="0" IsSystemColor="False">
</System.Drawing.Color>
</cc1:htmlcolordropdown>
The problem isn't only in the fact that there is much more information about the colors persisted than we need or want. As the attributes represent read-only properties of the Color
object, an exception will be thrown upon parsing. It would be preferable to persist these inner items differently, with minimal information necessary, like the following:
<cc1:htmlcolordropdown id="HtmlColorDropDown1" runat="server"
Palette="UserDefined">
<Color value="Red"/>
<Color value="Green"/>
<Color value="Blue"/>
</cc1:htmlcolordropdown>
This also makes the control easier to work with for those using plain text editors. A custom ControlDesigner
can help us here in persisting the inner markup in this simplified manner, while a custom ControlBuilder
will help deserialize the <Color value="xxx"/>
tags upon parsing.
Custom Persistence through a ControlDesigner
In order to persist the UserDefinedColors
collection, we'll define a custom subclass of System.Web.UI.Design.ControlDesigner
. A ControlDesigner
's purpose is to provide design-time support for a web control, and it can do so in a number of ways. For example, a ControlDesigner
can be used to customize the HTML that is used to represent a control in a visual designer like Visual Studio. A ControlDesigner
may also be used to add support for the visual editing of control templates.
For the HtmlColorDropDown
control, we need the ability to customize the inner HTML that is serialized when a visual designer persists the object's properties to a web page. For this, we can subclass ControlDesigner
and override the GetPersistInnerHtml
method. The complete code for the subclassed HtmlColorDropDownDesigner
is as follows:
public class HtmlColorDropDownDesigner : ControlDesigner
{
public override string GetPersistInnerHtml()
{
StringWriter sw = new StringWriter();
HtmlTextWriter html = new HtmlTextWriter(sw);
HtmlColorDropDown dd
= this.Component as HtmlColorDropDown;
if (dd != null)
{
foreach(Color c in dd.UserDefinedColors)
{
string s =
(c.IsKnownColor ? c.Name
: ColorUtility.ColorToHexString(c) );
html.WriteBeginTag("Color");
html.WriteAttribute("value", s);
html.WriteLine(HtmlTextWriter.SelfClosingTagEnd);
}
}
return sw.ToString();
}
}
In the overridden GetPersistInnerHtml
method, we iterate through each System.Drawing.Color
item in the UserDefinedColors
property. For each color, we output a child tag in the form:
<Color value="xxx">
where xxx is either the name of the color (for known color items such as "Red") or its HTML hex string (e.g., "#FF0000"). Through this custom persistence, we can represent the full UserDefinedColors
collection without the problems and verbosity of serializing full Color
objects.
It is worthwhile to note that other methods are available for customizing how a control's properties are to be persisted. A TypeConverter
, for example, may be defined for custom types, which can then play a role in serialization. For that matter, a custom CodeDomSerializer
object can persist object properties as programming code rather than HTML. In the case of the HtmlColorDropDown
control, I chose a custom ControlDesigner
overriding GetPersistInnerHtml
as it offered a simple and direct solution for persisting the contents of the ColorCollection
.
The custom designer is associated with the main HtmlColorDropDown
control through the Designer
attribute. Other attributes are also important in this context. The PersistChildren(false)
attribute is applied to the control to ensure that properties are not otherwise persisted as nested server control tags. The PersistenceMode
and DesignerSerializationVisibility
attributes are still applied to the UserDefinedColors
property to ensure that our designer's GetPersistInnerHtml
method in fact has a reason to be called.
[
,PersistChildren(false)
,Designer(typeof(HtmlColorDropDownDesigner))
]
public class HtmlColorDropDown : Control, IPostBackDataHandler
, IPostBackEventHandler, IAttributeAccessor
{
...
[
,PersistenceMode(PersistenceMode.InnerDefaultProperty)
,DesignerSerializationVisibility(
DesignerSerializationVisibility.Content)
]
public ColorCollection UserDefinedColors
{
get {...}
set {...}
}
...
}
Custom Parsing through a ControlBuilder
The custom HtmlColorDropDownDesigner
takes care of persisting the UserDefinedColors
collection as inner HTML content of the server tag. When the page is parsed, three separate classes work together to deserialize this inner content back into the UserDefinedColors
collection: a custom ControlBuilder
, a helper class representing a <Color value='xxx'/>
tag, and the main HtmlColorDropDown
control itself.
The first of the three is a subclass of System.Web.UI.ControlBuilder
. A ControlBuilder
object assists the page parser when building a server control from markup text. The HtmlColorDropDownBuilder
is defined as follows with only one overridden method:
public class HtmlColorDropDownBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName,
IDictionary attribs)
{
if (string.Compare(tagName,"color", true) == 0)
{
return typeof(ColorItemHelper);
}
return base.GetChildControlType (tagName, attribs);
}
}
The custom builder's GetChildControlType
method returns the type ColorItemHelper
for each <Color value='xxx'>
child tag nested within the server control markup. The ColorItemHelper
class is defined very simply:
public class ColorItemHelper
{
private string _value;
public string Value
{
get {return _value;}
set {_value = value;}
}
}
The custom builder is assigned to the HtmlColorDropDown
control through the ControlBuilder
attribute. The main HtmlColorDropDown
control then uses the ColorItemHelper
object passed to it from the parser to add the color to its UserDefinedColors
collection. This is done by overriding the AddParsedSubObject
method of the Control
class:
[
, ParseChildren(false)
, ControlBuilder(typeof(HtmlColorDropDownBuilder))
]
public class HtmlColorDropDown : Control, IPostBackDataHandler
, IPostBackEventHandler, IAttributeAccessor
{
...
protected override void AddParsedSubObject(object obj)
{
if (obj is ColorItemHelper)
{
ColorItemHelper h = obj as ColorItemHelper;
this.UserDefinedColors.Add(h.Value);
}
else
base.AddParsedSubObject (obj);
}
...
}
Note that the ParseChildren(false)
attribute is applied to the main HtmlColorDropDown
class. This ensures that the nested inner HTML is not parsed as properties of the control, allowing our custom parsing to function.
Summary
The HtmlColorDropDown
web control renders a standard HTML <select>
tag with <option>
items representing color choices. The colors themselves may be rendered as the background or foreground color of each item. Three built-in palettes are offered; a fourth, UserDefined
, allows a control user to specify which colors to provide as choices in the list. To support the persistence of user defined colors within a visual designer, I used a custom ControlDesigner
, overriding the GetPersistInnerHtml
method to render colors as simple <Color value='xxx'/>
child tags. I also used a custom ControlBuilder
to assist in parsing the nested tags for the deserialization of the UserDefinedColors
collection. While techniques for custom property persistence and parsing may be tricky, such techniques offer important benefits for control authors and are worth consideration.