
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf

C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT |
959 |
RestrictedCalendar stores a collection of dates the user isn’t allowed to select in a NonSelectableDates property. Ordinarily, you would deal with the serialization of the NonSelectableDates property by adding the attributes shown here:
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)] public DateTimeCollection NonSelectableDates
{
get {return (DateTimeCollection)ViewState["NonSelectableDates"];} set {ViewState["NonSelectableDates"] = value;}
}
Everything seems fine at first. In fact, you don’t even need to create a type converter for the NonSelectableDates property. That’s because .NET automatically recognizes it as a collection and uses the CollectionConverter. The CollectionConverter simply displays the text (Collection), as shown in Figure 28-7.
Figure 28-7. A collection property
You can click the ellipsis next to the property name to open a designer where you can add DateTime objects. You can even choose values for each date using a drop-down calendar, as shown in Figure 28-8. This graphical functionality is actually the work of another component, called a type editor. (You’ll learn about type editors in the next section.)
This all works well enough. If you look at the generated HTML for the RestrictedCalendar after you add two dates, you’ll see something like this:
<cc1:RestrictedCalendar id="RestrictedCalendar8" runat="server"> <NonSelectableDates>
<System.DateTime Year="2004" DayOfWeek="Friday" Second="0" Minute="0" TimeOfDay="00:00:00" Day="20" Millisecond="0" Date="2004-08-20" Hour="0" DayOfYear="233" Ticks="632285568000000000" Month="8"></System.DateTime> <System.DateTime Year="2004" DayOfWeek="Saturday" Second="0" Minute="0" TimeOfDay="00:00:00" Day="21" Millisecond="0" Date="2004-08-21" Hour="0" DayOfYear="234" Ticks="632286432000000000" Month="8"></System.DateTime>
</NonSelectableDates>
</cc1:RestrictedCalendar>

960 C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT
Figure 28-8. Adding dates to a collection property
This code raises two problems. First, there’s more information there than you really need to store. The RestrictedCalendar is interested only in the date portion of the DateTime object, and the time information is wasted space. A more serious problem is that when you request this page, ASP.NET won’t be able to re-create the DateTimeCollection that’s exposed by the NonSelectableDates property. Instead, it will raise an error when attempting to deserialize the nested tags and set read-only properties such as Year, Month, and Ticks.
To solve this problem, you need to plug into the serialization and parsing infrastructure for your control. The first step is to customize the code that’s serialized for each DateTime object in the NonSelectableDates collection. To accomplish this, you need to create a new class called a control designer. Control designers are complex components that can perform a whole variety of designtime services, including generating HTML for a design-time representation and providing services for entering and editing templates at design time.
In this case, you’re interested in only one aspect of the control designer—its ability to control how the content inside the control tag is serialized. To accomplish this, you create a class that inherits from ControlDesigner, and you override the GetPersistenceContent() method. This method will read the list of restricted dates and create a new tag for each DateTime object. This tag will then be added into the RestrictedCalendar control tag.
Here’s the complete code:
public class RestrictedCalendarDesigner : ControlDesigner
{
public override string GetPersistenceContent()
{
StringWriter sw = new StringWriter(); HtmlTextWriter html = new HtmlTextWriter(sw);
RestrictedCalendar calendar = this.Component as RestrictedCalendar; if (calendar != null)
{
// Create tags in the format:

C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT |
961 |
//<DateTime Value='xxx' />
foreach (DateTime date in calendar.NonSelectableDates)
{
html.WriteBeginTag("DateTime"); html.WriteAttribute("Value", date.ToString()); html.WriteLine(HtmlTextWriter.SelfClosingTagEnd);
}
}
return sw.ToString();
}
}
To tell the control to use this control designer, you need to apply a Designer attribute to the RestrictedCalendar class declaration. At the same time, you should also set ParseChildren to false so that the nested tags the ControlDesigner creates aren’t treated as control properties.
[ControlBuilder(typeof(RestrictedCalendarBuilder))]
[ParseChildren(false)]
public class RestrictedCalendar : Calendar { ... }
This accomplishes half the process. Now when you add restricted dates at design time, the control tag markup will be created in this format:
<cc1:RestrictedCalendar id="RestrictedCalendar1" runat="server"> <DateTime Value="8/27/2004 " />
<DateTime Value="8/28/2004 " /> </cc1:RestrictedCalendar>
The next step is to enable your control to read this custom HTML and regenerate the RestrictedDates collection at runtime. To make deserialization easier, you need to create a class that models the <DateTime> tag. In this case, the <DateTime> tag has only a single attribute named value. As a result, this following class works perfectly well:
public class DateTimeHelper
{
private string val; public string Value
{
get {return val;} set {val = value;}
}
}
Note how the public property of this class matches the serialized tag exactly. That means ASP.NET will be able to deserialize the tag into a DateTimeHelper without needing any extra help. However, you still need to take extra steps to instruct the ASP.NET parser to use the DateTimeHelper class for deserialization. Finally, you also need to write code that can examine the DateTimeHelper and use it to configure a RestrictedCalendar instance.
To perform the first task of these two tasks, you need the help of a control builder. When ASP.NET parses a page, it enlists the help of a control builder to interpret the HTML and generate the control objects. The default control builder simply examines the ParseChildren attribute for the control and then tries to interpret the nested tags as properties (if ParseChildren is true) or as child controls (if ParseChildren is false). The custom control builder that the RestrictedCalendar will use overrides the GetChildControlType(), which is called every time the parser finds a nested tag.
The GetChildControlType() method examines a nested tag and then returns a Type object that tells the parser what type of child object to create. In this case, your custom control builder should find a <DateTime> tag and then inform the runtime to create a DateTimeHelper object.

962 C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT
Here’s the complete control builder code:
public class RestrictedCalendarBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, IDictionary attribs)
{
if (tagName == "DateTime")
{
return typeof(DateTimeHelper);
}
return base.GetChildControlType (tagName, attribs);
}
}
To associate this builder with the RestrictedCalendar control, you need to add a Designer attribute to the class declaration:
[ControlBuilder(typeof(RestrictedCalendarBuilder))]
[ParseChildren(false)]
[Designer(typeof(RestrictedCalendarDesigner)) public class RestrictedCalendar : Calendar
{ ... }
Your odyssey still isn’t quite complete. Now the ASP.NET parser can successfully create the DateTimeHelper object, but it doesn’t know what to do with it. Because you’ve set ParseChildren to false, the parser won’t attempt to recognize it as a property. Instead, it will call the AddParsedSubObject() method of your control class, which will fail because the DateTimeHelper isn’t a control and can’t be added to the Controls collection. Fortunately, you can override the AddParsedSubObject() method to provide more suitable functionality. In this case, you need to take the supplied DataTimeHelper object and use it to add a new DateTime to the NonSelectableDates collection, as shown here:
protected override void AddParsedSubObject(object obj)
{
if (obj is DateTimeHelper)
{
DateTimeHelper date = (DateTimeHelper)obj; NonSelectableDates.Add(DateTime.Parse(date.Value));
}
}
Now you’ve finished all the code required to both serialize and parse the custom HTML content. This process clearly wasn’t easy, and it demonstrates that though basic design-time support is easy, advanced custom control design is a highly complex topic. To become an expert, you’ll need to study the MSDN documentation or continue your exploration with a dedicated book about server controls.
Type Editors
So far you’ve seen how type converters can convert various data types to strings for representation in the Properties window. But some data types don’t rely on string editing at all. For example, if you need to set an enumerated value (such as BorderStyle), you can choose from a drop-down list of all the values in the enumeration. More impressively, if you need to set a color, you can choose from a drop-down color picker. And some properties have the ability to break out of the Properties window altogether. One example is the Columns property of the DataGrid. If you click the ellipsis next to the


964 C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT
The code for the actual control isn’t shown here, but you can refer to the downloadable examples for this chapter to take a closer look. However, the full code for the type editor that uses this control is as follows:
public class ColorTypeEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context)
{
// This editor appears when you click a drop-down arrow. return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
IWindowsFormsEditorService srv = null;
//Get the editor service from the provider,
//which you need to create the drop-down window. if (provider != null)
srv = (IWindowsFormsEditorService) provider.GetService(typeof(IWindowsFormsEditorService));
if (srv != null)
{
//Create an instance of the custom Windows Forms
//color-picking control.
//Pass the current value of the color. ColorTypeEditorControl editor =
new ColorTypeEditorControl((System.Drawing.Color)value, context.Instance as WebControl);
//Show the control.
srv.DropDownControl(editor);
// Return the selected color information. return editor.SelectedColor;
}
else
{
// Return the current value. return value;
}
}
public override bool GetPaintValueSupported(ITypeDescriptorContext context)
{
// This type editor will generate a color box thumbnail. return true;
}

C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT |
965 |
public override void PaintValue(PaintValueEventArgs e)
{
// Fills the left rectangle with a color.
WebControl control = e.Context.Instance as WebControl; e.Graphics.FillRegion(new SolidBrush(control.BackColor),
new Region(e.Bounds));
}
}
To use this type editor, you need to attach it to a property that uses the Color data type. Most web controls already include color properties, but you can override one of them and apply a new Editor attribute.
Here’s an example that does exactly that to attach the type editor to the BackColor property of the RichLabel control:
[Editor(typeof(ColorTypeEditor), typeof(UITypeEditor))] public override Color BackColor
{
get {return base.BackColor;} set {base.BackColor = value;}
}
Control Designers
You’ve probably noticed that custom controls aren’t all treated the same on the design surface. ASP.NET tries to show a realistic design-time representation by running the rendering logic, but exceptions exist. For example, composite and templated controls aren’t rendered at all in the design-time environment, which means you’re left with nothing but a blank rectangle on
your design surface.
To deal with these issues, controls often use custom control designers that produce basic HTML that’s intended only for design-time display. This display can be a sophisticated block
of HTML that’s designed to reflect the real appearance of the control, a basic snapshot that shows a typical example of the control (as you’ll see for a DataGrid that doesn’t have any configured columns), or just a gray placeholder box with a message (as shown for the Repeater and DataList when they don’t have any templates).
If you want to customize the design-time HTML for your control, you can derive a custom designer from the ControlDesigner base class and override one of the following three methods:
•GetDesignTimeHtml(): Returns the HTML that’s used to represent the current state of the control at design time. The default implementation of this method simply returns the result of calling the RenderControl() method.
•GetEmptyDesignTimeHtml(): Returns the HTML that’s used to represent an empty control. The default implementation simply returns a string that contains the name of the control class and the ID.
•GetErrorDesignTimeHtml(): Returns the HTML that’s used if a design-time error occurs in the control. This HTML can provide information about the exception (which is passed as an argument to this method).
Of course, these methods reflect only a small portion of the functionality that’s available through the ControlDesigner. You can override many more methods to configure different aspects of design-time behavior. In the following section, you’ll see how to create a control designer that adds enhanced support for the SuperSimpleRepeater.


C H A P T E R 2 8 ■ D E S I G N - T I M E S U P P O RT |
967 |
■Note Keep in mind that ASP.NET isn’t able to decide when your control is empty. Instead, you’ll need to call the GetEmptyDesignTimeHtml() method when necessary. As you’ll see in this example, the GetDesignTimeHtml() method calls GetEmptyDesignTimeHtml() if a template isn’t present.
Coding the GetErrorDesignTimeHtml() method is just as easy. Once again, you can use the CreatePlaceHolderDesignTimeHtml() method, but this time you should supply the details about the exception that occurred.
protected override string GetErrorDesignTimeHtml(Exception e)
{
string text = string.Format("{0}{1}{2}{3}",
"There was an error and the control can't be displayed.", "<br />", "Exception: ", e.Message);
return CreatePlaceHolderDesignTimeHtml(text);
}
The final step is to build the GetDesignTimeHtml() method. This code retrieves the current instance of the SuperSimpleRepeater control from the ControlDesigner.Component property. It then checks for an item template. If no template is present, the empty HTML is shown. If a template is present, the control is data bound, and then the design-time HTML is displayed, as follows:
public override string GetDesignTimeHtml()
{
try
{
SuperSimpleRepeater repeater = (SuperSimpleRepeater1)base.Component; if (repeater.ItemTemplate == null)
{
return GetEmptyDesignTimeHtml();
}
else
{
String designTimeHtml = String.Empty; repeater.DataBind();
designTimeHtml = base.GetDesignTimeHtml(); return designTimeHtml;
}
return base.GetDesignTimeHtml();
}
catch (Exception e)
{
return GetErrorDesignTimeHtml(e);
}
}
This produces the vastly improved design-time representation shown in Figure 28-11, which closely resembles the actual runtime appearance of the SuperSimpleRepeater.