Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
104
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

958 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

To allow the RichLabel to serialize its Format property correctly, you need to apply both the PersistenceMode and DesignerSerializationVisibility attributes. The DesignerSerializationVisibility attribute will specify Content, because the Format property is a complex object. The PersistenceMode attribute will specify InnerProperty, which stores the Format property information as a separate, nested tag. Here’s how you need to apply these two attributes:

[TypeConverter(typeof(RichLabelFormattingOptionsConverter))]

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

[PersistenceMode(PersistenceMode.InnerProperty)] public RichLabelFormattingOptions Format

{

get {return (RichLabelFormattingOptions)ViewState["Format"];} set {ViewState["Format"] = value;}

}

Now when you configure the Format property in the Properties window, ASP.NET will create a tag in this form:

<apress:RichLabel id="RichLabel1" runat="server"> <Format Type="Xml" HighlightTag="b"></Format>

</apress:RichLabel>

The end result is that the RichLabel control works perfectly when inserted into a web page at runtime as well as when a developer is using it at design time.

You apply two other related serialization properties at the class level—PersistChildren and ParseChildren. Both attributes control how ASP.NET deals with nested tags and whether it supports child controls. When PersistChildren is true, child controls are persisted as contained tags. When PersistChildren is false, any nested tags designate properties. ParseChildren plays the same role when reading control tags. When ParseChildren is true, the ASP.NET parser interprets all nested tags as properties rather than controls.

When deriving from the WebControl class, the default is that PersistChildren is false and ParseChildren is true, in which case any nested tags are treated as property values. If you want child content to be treated as child controls in the control hierarchy, you need to explicitly set PersistChildren to true and ParseChildren to false. Because the RichLabel control isn’t designed to hold other controls, this step isn’t needed—the defaults are what you want.

Templated Controls

The RichLabel isn’t the only control that needs the serialization attributes. To successfully use the templated controls described in Chapter 27 (such as the SuperSimpleRepeater), all the template properties need to use PersistenceMode.InnerProperty serialization.

Here’s an example of a templated property that’s correctly configured:

[PersistenceMode(PersistenceMode.InnerProperty)]

[TemplateContainer(typeof(SimpleRepeaterItem))] public ITemplate ItemTemplate

{

get {return itemTemplate;} set {itemTemplate=value;}

}

Otherwise, when you set other properties in the control, the template content will be erased.

Controls with Collections

Unfortunately, serialization can become a fair bit more complicated than the RichLabel example. One such case is the RestrictedCalendar control demonstrated in the previous chapter. The

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

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

963

property name, a dialog box will appear where you can configure the column collection using a rich user interface. The RestrictedCalendar in the previous example showed a similar but less impressive example with the collection editor for editing restricted dates.

These properties all rely on UI type editors. Type editors have a single task in life—they generate user interfaces that allow you to set control properties more conveniently. Certain data types (such as collections, enumerations, and colors) are automatically associated with advanced type editors. In other cases, you might want to create your own type editor classes from scratch. All UI type editors are located in the System.Drawing.Design namespace.

Just as with type converters (and almost everything in the extensible architecture of .NET design-time support), creating a new type editor involves inheriting a base class (in this case UITypeEditor) and overriding desired members. The methods you can override include the following:

GetEditStyle(): Specifies whether the type editor is a DropDown (provides a list of specially drawn choices), Modal (provides a dialog box for property selection), or None (no editing supported).

EditValue(): This method is invoked when a property is edited (for example, the ellipsis next to the property name is clicked in the Properties window). Generally, this is where you would create a special dialog box for property editing.

GetPaintValueSupported(): Use this to return true if you are providing a PaintValue() implementation.

PaintValue(): Invoked to paint a graphical thumbnail that represents the value in the property grid. For example, this is used to create the color box for color properties.

The code for UI type editors isn’t overly complicated, but it can take a bit of getting used to for web developers. That’s because it involves using the other user interface platform in .NET—Windows Forms. Although the topic of Windows Forms is outside the scope of this book, you can learn a lot from a basic example. Figure 28-9 shows a custom color editing control that allows you to set various components of a color independently using sliders. As you do, it displays the color in a box at the bottom of the control.

Figure 28-9. Using a custom type editor

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.

966 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

Tip ASP.NET 2.0 adds some new control designer base classes, including CompositeControlDesigner and ContainerControlDesigner, that are useful in many common scenarios (in this case, designing composite and container controls).

A Basic Control Designer

The next example develops a control designer that generates a reasonable representation for the SuperSimpleRepeater developed in the previous chapter. Without a custom control designer, the design-time content of the SuperSimpleRepeater is an empty string.

The first step in creating a designer is to build a class that derives from the ControlDesigner namespace in the System.Web.UI.Design namespace, as shown here:

public class SuperSimpleRepeaterDesigner : ControlDesigner { ... }

You can apply the designer to the control using the Designer attribute, as shown here:

[Designer(typeof(SuperSimpleRepeaterDesigner))]

public class SuperSimpleRepeater : WebControl, INamingContainer { ... }

When creating a control designer, the first step is to create the GetEmptyDesignTimeHtml() method. This method simply needs to return a static piece of text. The ControlDesigner includes a helper method named CreatePlaceHolderDesignTimeHtml(), which generates a gray HTML box with a message that you specify (just like the Repeater control without any templates). You can use this method to simplify your rendering code, as shown here:

protected override string GetEmptyDesignTimeHtml()

{

string text = "Switch to design view to add a template to this control."; return CreatePlaceHolderDesignTimeHtml(text);

}

Figure 28-10 shows the empty design-time view of the SuperSimpleRepeater control.

Figure 28-10. The empty design-time HTML

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.