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

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

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

948 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-4. An embedded resource

To get your resource, you need to tell the WebResource.axd handler what resource you need and what assembly contains it. You can generate the right URL using the Page.ClientScript.GetWebResourceUrl() method, as shown here:

protected override void OnInit(EventArgs e)

{

ImageUrl = Page.ClientScript.GetWebResourceUrl(typeof(CustomImageButton), "CustomServerControls.button1.jpg");

}

Note Incidentally, web resources automatically take localization into account. If you have a satellite resource assembly with a locale-specific version of this image file, that version would be used instead. For more information, refer to Chapter 17.

The actual URL looks something like this:

WebResource.axd?a=CustomServerControls&r=button.jpg&t=632059604175183419

The a and r query string parameters specify the assembly and resource names, respectively. The t is a workaround to support caching. Essentially, the WebResource.axd caches every requested resource. That way you can request the same image hundreds of times without incurring extra work digging up the resource from the assembly. (This is particularly important to ensure performance if you have an assembly that’s packed with a large number of resources.) However, caching introduces its own problem—namely, you don’t want to reuse a cached resource if the assembly contains a newer version. The t parameter defends against this. It’s an assembly timestamp. When the custom control assembly is rebuilt, the generated URLs will have a different t value; as a result, the browser will make a new request, and the WebResource.axd file will get the latest content.

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

949

Note You can find an example of this technique in the ImageButton control with the downloadable code for this chapter.

The WebResource.axd handler has one other trick in store. You can supply a Boolean third parameter in the WebResource attribute constructor to tell it to perform automatic substitution. This allows you to create an embedded resource that points to other embedded resources.

This trick works only for text-based resources (think of an HTML Help file, for example). Here’s what the attribute looks like:

[assembly: WebResource("CustomServerControls.Help.htm", "text/html", true)]

Now the WebResource.axd will scan through the resource and look for expressions in this format:

<%= WebResource(HelpTitle.gif) %>

Every time it finds one of these, it will replace it with another automatically generated WebResource.axd URL. So, if you write this in your HTML resource:

<img src="WebResource(HelpTitle.gif)" />

then it’s automatically converted into something like this before it’s served:

<img src= "WebResource.axd?a=CustomServerControls&r=HelpTitle.gif&t=632059604175183419" />

Code Serialization

When you configure control properties in the Properties window, Visual Studio needs to be able to create and modify the corresponding control tag in the .aspx file. This process is called code serialization, and it often works automatically. However, you can run into trouble if you use properties that are themselves complex types or if you create a templated control or a control that supports child controls.

In the following sections, you’ll learn about the different ingredients that affect control serialization and what changes you need to make in order to resolve common problems.

Type Converters

The Properties window deals seamlessly with common data types. String data doesn’t present a problem, but the Properties window can also convert strings to numeric types. For example, if you look at the Width property of a control, you’ll see a value such as 50 px. You can enter any characters in this field, but if you try to commit the change (by pressing Enter or moving to another field) and you’ve included characters that can’t be interpreted as a unit, the change will be rejected.

This behavior is made possible by type converters, specialized classes that are designed for the sole purpose of converting a specialized data type to a string representation and back. Most of the core .NET data types have default type converters that work perfectly well. (You can find these type converters in the System.ComponentModel.TypeConverter namespace.) However, if you create your own structures or classes and use them as properties, you may also want to create custom type converters that allow them to work in the Properties window.

950 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

A Control with Object Properties

The next example uses a RichLabel control that’s a slightly revised version of the XmlLabel control presented in Chapter 27. The difference is that while the XmlLabel is designed to show XML documents, the RichLabel control is designed to support different types of content.

Essentially, the RichLabel can support any type of content that’s defined in the following RichLabelTextType enumeration. In this simple example, the RichLabelTextType enumeration includes only two options: Xml (which uses the same code as the XmlLabel) and Html (which treats the text as is and doesn’t perform any additional processing). However, you could easily add the rendering code for different types of text.

public enum RichLabelTextType {Xml, Html}

The RichLabel also allows you to choose what tag you want to use to format important details (such as the XML tags in XML rendering mode). The way this works is through another class, named RichLabelFormattingOptions. The RichLabelFormattingOptions class defines two properties: Type (which holds a value from the RichLabelTextType enumeration) and HighlightTag (which stores a tag name as a string, such as b for the <b> tag, which applies bold formatting).

[Serializable()]

public class RichLabelFormattingOptions

{

private RichLabelTextType type; public RichLabelTextType Type

{

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

}

private string highlightTag; public string HighlightTag

{

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

}

public RichLabelFormattingOptions(RichLabelTextType type, string highlightTag)

{

this.highlightTag = highlightTag; this.type = type;

}

}

The RichLabel class includes a Format property, which exposes an instance of the custom RichLabelFormattingOptions class. The rendering logic in the RichLabel control uses this information to customize the HTML it generates.

Here’s the code for the RichLabel control:

[DefaultProperty("RichText")] public class RichLabel : WebControl

{

public RichLabel() : base()

{

Text = "";

// Default to XML text with tags formatted in bold.

Format = new RichLabelFormattingOptions(RichLabelTextType.Xml, "b");

}

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

951

[Category("Appearance")]

[Description("The content that will be displayed.")] public string Text

{

get {return (string)ViewState["Text"];} set {ViewState["Text"] = value;}

}

[Category("Appearance")]

[Description("Options for configuring how text is rendered.")] public RichLabelFormattingOptions Format

{

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

}

protected override void RenderContents(HtmlTextWriter output)

{

string convertedText = ""; switch (Format.Type)

{

case RichLabelTextType.Xml:

// Find and highlight the XML tags.

convertedText = RichLabel.ConvertXmlTextToHtmlText( Text, Format.HighlightTag);

break;

case RichLabelTextType.Html: // Keep the text as is. convertedText = Text; break;

}

output.Write(convertedText);

}

}

public static string ConvertXmlTextToHtmlText(string inputText, string highlightTag)

{

// (Code omitted.)

}

}

Alternative designs are possible. For example, you could add these two pieces of information (Type and HighlightTag) as separate properties in the RichLabel class, in which case you wouldn’t need to take any extra steps to ensure proper serialization. However, you might decide to group related properties together using a custom class for a number of reasons. Perhaps you want the ability to reuse the RichLabelFormattingOptions class in order to specify text-formatting options for other controls. Or maybe you need to create a more complex control that accepts several different pieces of text and can convert all of them using independent RichLabelFormattingOptions settings. In both of these situations, it becomes useful to group the properties using the RichLabelFormattingOptions class.

However, the RichLabel control doesn’t work well with Visual Studio. When you try to modify this control at design time, you’ll immediately notice the problem. The Properties window doesn’t allow you to edit the RichLabel.Format property. Instead, it shows an empty edit box where you can’t type anything. To solve this problem, you need to create a custom type converter, as explained in the next section.

952 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

Creating a Custom Type Converter

A custom type converter is a class that can convert from your proprietary data type (in this case, the RichLabelFormattingOptions class) to a string and back. In the following example, you’ll see such a class, named RichLabelFormattingOptionsConverter.

The first step is to create a custom class that derives from the base class TypeConverter, as shown here:

public class RichLabelFormattingOptionsConverter : TypeConverter { ... }

By convention, the name of a type converter class consists of the class type it converts, followed by the word Converter.

Once you create the type converter, you have several methods to override:

CanConvertFrom(): This method examines a data type and returns true if the type converter can make the conversion from this data type to the custom data type.

ConvertFrom(): This method performs the conversion from the supplied data type to the custom data type.

CanConvertTo(): This method examines a data type and returns true if the type converter can make the conversion from the custom object to this data type.

ConvertTo(): This method performs the conversion from the custom data type to the requested data type.

Remember that the key task of a type converter is to convert between your custom data type and a string representation. This example uses a string representation that includes both values from the RichLabelFormattingOptions object, separated by a comma and a space and with angled brackets around the tag name. Here’s what the string format looks like:

Type Name, <HighlightTag>

Here’s an example with XML formatting and a <b> tag:

Xml, <b>

With that in mind, you can create two helper methods in the converter class to perform this conversion. The first is a ToString() method that builds the required string representation:

private string ToString(object value)

{

RichLabelFormattingOptions format = (RichLabelFormattingOptions)value; return String.Format("{0}, <{1}>", format.Type, format.HighlightTag);

}

The second part is a FromString() method that decodes the string representation. If the string isn’t in the format you need, the FromString() code raises an exception. Otherwise, it returns the new object instance.

private RichLabelFormattingOptions FromString(object value)

{

string[] values = ((string)value).Split(','); if (values.Length != 2)

throw new ArgumentException("Could not convert the value");

try

{

//Convert the name of the enumerated value into the corresponding

//enumerated value (which is actually an integer constant).

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

953

RichLabelTextType type = (RichLabelTextType)Enum.Parse( typeof(RichLabelTextType), values[0], true);

// Get rid of the spaces and angle brackets around the tag name. string tag = values[1].Trim(new char[]{' ','<','>'});

return new RichLabelFormattingOptions(type, tag);

}

catch

{

throw new ArgumentException("Could not convert the value");

}

}

Before attempting a conversion from a string to a RichLabelFormattingOptions object, the Properties window will first query the CanConvertFrom() method. If it receives a true value, it will call the actual ConvertFrom() method. All the CanConvertFrom() method needs to do is check that the supplied type is a string, as follows:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)

{

if (sourceType == typeof(string))

{

return true;

}

else

{

return base.CanConvertFrom(context, sourceType);

}

}

The ConvertFrom() method calls the conversion by calling the FromString() method shown earlier:

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)

{

if (value is string)

{

return FromString(value);

}

else

{

return base.ConvertFrom(context, culture, value);

}

}

Note It is good object-oriented programming practice to always give the base classes from which you inherit a chance to handle a message you are not going to support. In this case, any requests to perform a conversion from an unrecognized type are passed to the base class.

The same process occurs in reverse when converting a RichLabelFormattingOptions object to a string. First, the Properties window calls CanConvertTo(). If it returns true, the next step is to call the ConvertTo() method. Here’s the code you need:

954C H A P T E R 2 8 D E S I G N - T I M E S U P P O RT

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)

{

if (destinationType == typeof(string))

{

return true;

}

else

{

return base.CanConvertTo(context, destinationType);

}

}

public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)

{

if (destinationType == typeof(string))

{

return ToString(value);

}

else

{

return base.ConvertTo(context, culture, value, destinationType);

}

}

Now that you have a fully functioning type converter, the next step is to attach it to the corresponding property.

Attaching a Type Converter

You can attach a type converter in two ways. You can add the TypeConverter attribute to the related class (in this case, RichLabelFormattingOptions), as shown here:

[TypeConverter(typeof(RichLabelFormattingOptionsConverter))] public class RichLabelFormattingOptions

{ ... }

This way, whenever an instance of this class is used for a control, Visual Studio knows to use your type converter.

Alternatively, you can attach the type converter directly to the property in your custom control that uses it, as shown here:

[TypeConverter(typeof(RichLabelFormattingOptionsConverter))] public RichLabelFormattingOptions Format

{ ... }

This approach makes the most sense if you are using a generic data type (such as a string) and you want to customize its behavior in only this case.

Now you can recompile the code, and try using the RichLabel control in a sample web page. When you select a RichLabel, you’ll see the current value of the RichLabel.Format property in the Properties window (shown in Figure 28-5), and you can edit it by hand.

Note When changing details such as type converter, control designer, and control builders, your changes will not appear immediately in the design environment after a recompile. Instead, you may need to close the solution with the test web pages and then reopen it.

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

955

Figure 28-5. A string representation of the RichLabelFormattingOptions object

Of course, unless you enter the correct string representation, you’ll receive an error message, and your change will be rejected. In other words, the custom type converter shown here gives you the ability to specify a RichLabelFormattingOptions object as a string, but the process certainly isn’t user-friendly. The next section shows you how to improve this level of support.

The ExpandableObjectConverter

ASP.NET web controls support a number of object properties. The best example is Font, which refers to a FontInfo object with properties such as Bold, Italic, Name, and so on. When you set the Font property in the Properties window, you don’t need to type all this information in a single, correctly formatted string. Instead, you can expand the Font property by clicking the plus (+) box and edit the FontInfo properties individually.

You can enable the same type of editing with your own custom object types—you simply need to create a custom type converter that derives from the ExpandableObjectConverter class instead of the base TypeConverter class. For example, you could take the RichLabelFormattingOptionsConverter developed in the previous section, and change it as shown here:

public class RichLabelFormattingOptionsConverter : ExpandableObjectConverter { ... }

Now you can specify the Format property by typing in a string or expanding the property and modifying one of the two subproperties. Figure 28-6 shows the much more convenient interface that you’ll see in the Properties window.

This looks good at first pass, but it has still a few quirks. One problem is that when you change a subproperty (Type or HighlightTag), the string representation that’s shown in the Format box isn’t immediately updated. To solve this problem you need to apply the NotifyParentProperty and RefreshProperties attributes to the properties of the RichLabelFormattingOptions class. At the same time, you might also want to add a Description attribute to configure the text that will appear in the Properties window for this subproperty.

956 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-6. Editing properties of the RichLabelFormattingOptions object

Here’s the revised code for the RichLabelFormattingOptions class:

public class RichLabelFormattingOptions

{

private RichLabelTextType type;

[RefreshProperties(RefreshProperties.Repaint)]

[NotifyParentProperty(true)]

[Description("Type of content supplied in the text property")] public RichLabelTextType Type

{

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

}

private string highlightTag;

[RefreshProperties(RefreshProperties.Repaint)]

[NotifyParentProperty(true)]

[Description(

"The HTML tag you want to use to mark up highlighted portions.")] public string HighlightTag

{

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

}

public RichLabelFormattingOptions(RichLabelTextType type, string highlightTag)

{

this.highlightTag = highlightTag; this.type = type;

}

}

This solves the synchronization and editing problems, but all the quirks still aren’t fixed. The problem is that although you can edit the RichLabel.Format property, the information you set isn’t

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

957

persisted into the control tag. This means that the changes you make at design time are essentially ignored. To resolve this problem, you need to dig a little deeper into how .NET serializes control properties, as described in the next section.

Serialization Attributes

You can control how control properties are serialized into the .aspx file using attributes. You need to consider two key attributes—DesignerSerializationVisibility and PersistenceMode.

The DesignerSerializationVisibility attribute determines whether a property will be serialized. You have three choices:

Visible: This is the default value. It specifies that the property should be serialized, and it works for simple data types (such as strings, dates, and enumerations) and the numeric data types.

Content: This serializes the entire content of an object. You can use this value to serialize complex types with multiple properties, such as a collection.

Hidden: This specifies that a property shouldn’t be serialized at all. For example, you might use this to prevent a calculated value from being serialized.

The PersistenceMode attribute allows you to specify how a property is serialized. You have the following choices:

Attribute: This is the default option. The property will be serialized as an HTML attribute of the control.

InnerProperty: The property will be persisted as a nested tag inside the control. This is the preferred setting to generate complex nested hierarchies of objects. Examples are the Calendar and DataList controls.

InnerDefaultProperty: The property will be persisted inside the control tag. It will be the only content of the control tag. An example is the Text property of the Label control. When using a default property, the property name doesn’t appear in the nested content.

EncodedInnerDefaultProperty: This is the same as InnerDefaultProperty, except that the content will be HTML encoded before it is persisted.

To understand how these different options work, it’s worth considering a few examples. The PersistenceMode.Attribute choice is the default option you’ve seen with the core set of ASP.NET control tags. If you combine this attribute with DesignerSerializationVisibility.Content in a property whose type contains subproperties, ASP.NET uses the object-walker syntax, in the form of PropertySubProperty="Value". You can see an example with the Font property, as shown here:

<apress:ctrl Font-Size="8pt" Font-Names="Tahoma" Bold="True" ... />

On the other hand, consider what happens if you create a custom control that overrides the persistence behavior of the Font property to use PersistenceMode.InnerProperty, as shown here:

[PersistenceMode(PersistenceMode.InnerProperty)] public override FontInfo Font

{

get {return base.Font;}

}

Now the persisted code for the Font property takes this form:

<apress:ctrl ... >

<Font Size="8pt" Names="Tahoma" Bold="True"></Font> </apress:ctrl>