Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
919 |
change their data. For example, consider a text box that’s represented as an <input> tag in a form. When the page posts back, the data from the <input> tag is part of the information in the control collection. The TextBox control needs to retrieve this information and update its state accordingly. To process the data that’s posted to the page in your custom control, you need to implement the IPostBackDataHandler interface. By implementing this interface, you indicate to ASP.NET that
when a postback occurs, your control needs a chance to examine the postback data. Your control will get this opportunity, regardless of which control actually triggers the postback.
The IPostBackDataHandler interface defines two methods:
•LoadPostData(): ASP.NET calls this method when the page is posted back, before any control events are raised. It allows you to examine the data that’s been posted back and update the state of the control accordingly. However, you shouldn’t fire change events at this point, because other controls won’t be updated yet.
•RaisePostDataChangedEvent(): After all the input controls on a page have been initialized, ASP.NET gives you the chance to fire a change event, if necessary, by calling the RaisePostDataChangedEvent() method.
The best way to understand how these methods work is to examine a basic example. The next control emulates the basic TextBox control. Here’s the basic control definition:
public class CustomTextBox : WebControl, IPostBackDataHandler { ... }
As you can see, the control inherits from WebControl and implements IPostBackDataHandler. The control requires only a single property, Text. The Text is stored in view state and initialized
to an empty string in the control constructor. The constructor also sets the base tag to <input>.
public CustomTextBox() : base(HtmlTextWriterTag.Input)
{
Text = "";
}
public string Text
{
get {return (string)ViewState["Text"];} set {ViewState["Text"] = value;}
}
Because the base tag is already set to <input>, there’s little extra rendering work required. You can handle everything by overriding the AddAttributesToRender() method and adding a type attribute that indicates the <input> control represents a text box and a value attribute that contains the text you want to display in the text box, as follows:
protected override void AddAttributesToRender(HtmlTextWriter output)
{
output.AddAttribute(HtmlTextWriterAttribute.Type, "text"); output.AddAttribute(HtmlTextWriterAttribute.Value, Text); output.AddAttribute("name", this.UniqueID); base.AddAttributesToRender(output);
}
You must also add the UniqueID for the control using the name attribute. That’s because ASP.NET matches this string against the posted data. If you don’t add the UniqueID, the LoadPostData() method will never be called, and you won’t be able to retrieve posted data.
920 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
■Tip Alternatively, you can call the Page.RegisterRequiresPostback() method in the OnInit() method of your custom control. In this case, ASP.NET will add the unique ID if you don’t explicitly render it, ensuring that you can still receive the postback.
All that’s left is to implement the IPostBackDataHandler methods to give the control the ability to respond to user changes.
The first step is to implement the LoadPostData() method. This method uses two parameters. The second parameter is a collection of values posted to the page. The first parameter is the key value that identifies the data for the current control. Thus, you can access the data for your control using syntax like this:
string newData = postData[postDataKey];
The LoadPostData() also needs to tell ASP.NET whether a change event is required. You can’t fire an event at this point, because the other controls may not be properly updated with the posted data. However, you can tell ASP.NET that a change has occurred by returning true. If you return true, ASP.NET will call the RaisePostDataChangedEvent() method after all the controls are initialized. If you return false, ASP.NET will not call this method.
Here’s the complete code for the LoadPostData() method in the CustomTextBox:
public bool LoadPostData(string postDataKey, NameValueCollection postData)
{
//Get the value posted and the past value. string postedValue = postData[postDataKey]; string val = Text;
//If the value changed, then reset the value of the text property
//and return true so the RaisePostDataChangedEvent will be fired. if (val != postedValue)
{
Text = postedValue; return true;
}
else
{
return false;
}
}
The RaisePostDataChangedEvent() has the relatively simple task of firing the event. However, most ASP.NET controls use an extra layer, whereby the RaisePostDataChangedEvent() calls an OnXxx() method and the OnXxx() method actually raises the event. This extra layer gives other developers the ability to derive a new control from your control and alter its behavior by overriding the OnXxx() method.
Here’s the remaining code:
public event EventHandler TextChanged;
public void RaisePostDataChangedEvent()
{
// Call the method to raise the change event. OnTextChanged(new EventArgs());
}
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
921 |
portected virtual void OnTextChanged(EventArgs e)
{
// Check for at least one listener, and then raise the event. if (TextChanged != null)
TextChanged(this, e);
}
Figure 27-7 shows a sample page that tests the CustomTextBox control and responds to its event.
Figure 27-7. Retrieving posted data in a custom control
Triggering a Postback
By implementing IPostBackDataHandler, you’re able to participate in every postback and retrieve the posted data that belongs to your control. But what if you want to trigger a postback? The simplest example of such a control is the Button control, but many other rich web controls—including the Calendar and GridView—allow you to trigger a postback by clicking an element or a link somewhere in the rendered HTML.
You can trigger a postback in two ways. First, you can render an <input> tag for a submit button, which always posts back the form. Your other option is to call the JavaScript function called __doPostBack() that ASP.NET automatically adds to the page. The __doPostBack() function accepts two parameters: the name of the control that’s triggering the postback and a string representing additional postback data.
ASP.NET makes it easy to access the __doPostBack() function with the Page.ClientScript.GetPostBackEventReference() method. This method creates a reference to the client-side __doPostBack() function, which you can then render into your control. Usually, you’ll place this reference in the onClick attribute of one of HTML elements in your control. That way, when that HTML element is clicked, the __doPostBack() function is triggered. Of course, JavaScript provides other attributes that you can use, some of which you’ll see in Chapter 29.
The best way to see postbacks in action is to create a simple control. The following example demonstrates a clickable image. When clicked, the page is posted back, without any additional data.
This control is based on the <img> tag and requires just a single property:
public CustomImageButton() : base(HtmlTextWriterTag.Img)
{
ImageUrl = "";
}
public string ImageUrl
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
923 |
This control doesn’t offer any functionality you can’t already get with existing ASP.NET web controls, such as the ImageButton. However, it’s a great starting point for building something that’s much more useful. In Chapter 29, you’ll see how to extend this control with JavaScript code to create a rollover button—something with no equivalent in the .NET class library.
■Note Rather than posting back the entire page, you can use a callback to fetch some specific information from the server. Callbacks are a new ASP.NET 2.0 feature, and we discuss them in Chapter 29.
Extending Existing Web Controls
In many situations, you don’t need to create a new control from scratch. Some of the functionality might already exist in the basic set of ASP.NET web controls. Because all ASP.NET controls are ordinary classes, you can use their functionality with basic object-oriented practices such as composition (creating a class that uses instances of other classes) and inheritance (creating a class that extends an existing class to change its functionality). In the following sections, you’ll see how both tasks apply to custom control design.
Composite Controls
So far you’ve seen a few custom controls that programmatically generate all the HTML code they need (except for the style properties, which can be inherited from the WebControl class). If you want to write a series of controls, you need to output all the HTML tags, one after the other. Fortunately, ASP.NET includes a feature that can save you this work by allowing you to build your control class out of other, existing web controls.
The basic technique is to create a control class that derives from System.Web.UI.WebControls.CompositeControl (which itself derives from WebControl). Then, you must override the CreateChildControls() method. At this point, you can create one or more control objects, set their properties and event handlers, and finally add them to the Controls collection of the current control. The best part about this approach is that you don’t need to customize the rendering code at all. Instead, the rendering work is delegated to the constituent server controls. You also don’t need to worry about details such as triggering postbacks and getting postback data, because the child controls will handle these details themselves.
The following example creates a TitledTextBox control that pairs a label (on the left) with a text box (on the right). Here’s the class definition for the control:
public class TitledTextBox : CompositeControl { ... }
The CompositeControl implements the INamingContainer interface. This interface doesn’t have any methods. It simply instructs ASP.NET to make sure all the child controls have unique ID values. ASP.NET does this by prepending the ID of the server control before the ID of the control. This ensures that there won’t be any naming conflict, even if you add several instances of the TitleTextBox control to a web form.
924 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
■Note In ASP.NET 1.x, the process for creating a composite control was subtly different. No CompositeControl class existed, so you had to derive from the WebControl class yourself. However, there are only two differences between CompositeControl and WebControl. First, CompositeControl implements INamingContainer so all the child controls are uniquely scoped and their IDs won’t conflict with page controls or other instances of your composite control. Second, CompositeControl calls the EnsureChildControls() method automatically when you access the Controls collection, which makes sure child controls are created before you try to manipulate them.
To make life easier, you should track the constituent controls with member variables. This allows you to access them in any method in your control. However, you shouldn’t create these controls yet, because that’s the function of the CreateChildControls() method.
protected Label label; protected TextBox textBox;
The web page won’t be able to directly access either of these controls. If you want to allow access to certain properties, you need to add property procedures to your custom control class, as follows:
public string Title
{
get {return (string)ViewState["Title"];} set {ViewState["Title"] = value;}
}
public string Text
{
get {return (string)ViewState["Text"];} set {ViewState["Text"] = value;}
}
Note that these properties simply store information in view state—they don’t directly access the child controls. That’s because the child controls might not yet exist. These properties will be applied to the child controls in the CreateChildControls() method. All the controls are rendered in a <span>, which works well. It ensures that if the web page applies font, color, or position attributes to the TitledTextBox control, it will have the desired effect on all the child controls.
Now you can override the CreateChildControls() method to create the Label and TextBox control objects. These objects are separated with one additional control object—a LiteralControl, which simply represents a scrap of HTML. In this example, the LiteralControl wraps two nonbreaking spaces. Here’s the complete code for the CreateChildControls() method:
protected override void CreateChildControls()
{
//Add the label. label = new Label();
label.EnableViewState = false; label.Text = Title; Controls.Add(label);
//Add a space.
Controls.Add(new LiteralControl(" "));
// Add the text box. textBox = new TextBox();
textBox.EnableViewState = false;
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
925 |
textBox.Text = Text;
textBox.TextChanged += new EventHandler(OnTextChanged); Controls.Add(textBox);
}
The CreateChildControls() code attaches an event handler to the TextBox.TextChanged event. When this event fires, your TitledTextBox should pass it along to the web page as the TitledTextBox.TextChanged event. Here’s the code you need to implement the rest of this design:
public event EventHandler TextChanged;
protected virtual void OnTextChanged(object sender, EventArgs e)
{
if (TextChanged != null) TextChanged(this, e);
}
Figure 27-9 shows a sample page that tests the TitledTextBox control and responds to its event.
Figure 27-9. Creating a composite control with a label and text box
You may prefer to follow the earlier approach and use an HtmlTextWriter to get full control over the HTML markup you render. But if you want to handle postbacks and events and create complex controls (such as an extended GridView or a navigational aid), using composite controls can simplify your life dramatically.
Derived Controls
Another approach to creating controls is to derive a more specialized control from one of the existing control classes. You can then override or add just the functionality you need, rather than re-creating the whole control. This approach isn’t always possible, because some controls keep key pieces of their infrastructure out of site in private methods you can’t override. However, when it does work, it can save a lot of work.
Sometimes, you might create a derived control so that you can preinitialize an existing control with certain styles or formatting properties. For example, you could create a custom Calendar or GridView that sets styles in the OnInit() method. That way, when you add this Calendar control, it’s already formatted with the look you need. In other cases, you might add entirely new functionality in the form of new methods or properties, as demonstrated in the following examples.
926 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
Creating a Higher-Level Calendar
In previous chapters, you learned how to customize the GridView to add niceties such as a summary row. You also learned how to use day-specific formatting in the Calendar. To implement either one of these changes, you need to handle a generic control event and wait for the element you want to format. A more elegant solution would be to simply set a property and let the control handle the task. You can add this extra layer of abstraction with a custom control.
For example, imagine you want to provide an easy way to designate nonselectable days in a calendar. To accomplish this, you could create a custom calendar control that adds two properties, as shown here:
public class RestrictedCalendar : Calendar
{
public bool AllowWeekendSelection
{
get {return (bool)ViewState["AllowWeekendSelection"];} set {ViewState["AllowWeekendSelection"] = value;}
}
public DateTimeCollection NonSelectableDates
{
get {return (DateTimeCollection)ViewState["NonSelectableDates"];} set {ViewState["NonSelectableDates"] = value;}
}
// (Other code omitted.)
}
The AllowWeekendSelection property indicates whether Saturday and Sunday should be selectable. The NonSelectableDates property provides a collection of exact dates that won’t be selectable. The DateTimeCollection is a custom collection class defined in the control project. It works the same as an ordinary ArrayList, except that it’s strongly typed to accept only DateTime values. You could use a List<Type> generic class, but it’s better to design a custom collection because that makes it easier to add design-time support for the collection (as discussed Chapter 28). You can
see the full collection code with the downloadable code sample.
Now when the calendar is rendered, you can take this information into account and automatically adjust any matching dates. This means you don’t need to handle the DayRender event in your code. Instead, you can specify the restricted dates declaratively using the Properties window.
Here’s the control code that handles the process:
protected override void OnDayRender(TableCell cell, CalendarDay day)
{
if (day.IsWeekend && !AllowWeekendSelection)
{
day.IsSelectable = false;
}
else if (NonSelectableDates.Contains(day.Date))
{
day.IsSelectable = false;
}
//Let the base class raise this event.
//The web page can respond to this event to perform further processing
//(or even reverse the changes made here).
base.OnDayRender(cell, day);
}
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
927 |
Note that your custom control doesn’t handle the DayRender event. Instead, it overrides the corresponding OnDayRender() method. This gives a similar result without worrying about delegate code and event handlers. Although controls don’t need to provide OnXxx() methods for every event, most do as a matter of convention. That makes it easier for you to customize the control.
The RestrictedCalendar control also uses the constructor to initialize some formatting-related properties:
public RestrictedCalendar()
{
//Set default properties. AllowWeekendSelection = true; NonSelectableDates = new DateTimeCollection();
//Configure the default appearance of the calendar. CellPadding = 8;
CellSpacing = 8;
BackColor = Color.LightYellow; BorderStyle = BorderStyle.Groove; BorderWidth = Unit.Pixel(2); ShowGridLines = true;
//Configure the font.
Font.Name = "Verdana";
Font.Size = FontUnit.XXSmall;
//Set calendar settings. FirstDayOfWeek = FirstDayOfWeek.Monday; PrevMonthText = "<--";
NextMonthText = "-->";
//Select the current date by default. SelectedDate = DateTime.Today;
}
This code also demonstrates how you can access the inherited properties of the Calendar control (such as CellPadding and CellSpacing) just as easily as you access the new properties you’ve added (such as AllowWeekendSelection).
This example allows the user to designate specific restricted dates in a specific month and year. You could also use a similar approach to allow the user to restrict specific years, months in any year, days in any month, and so on. In a sense, adding these sorts of properties complicates the Calendar control and makes it less flexible. However, this isn’t a problem if you want to tailor the control for a specific scenario.
■Note The online code for the RestrictedCalendar adds quite a bit more logic to improve design-time support. This code ensures that you can set the restricted dates using the Properties window. You’ll learn more about design-time support in Chapter 28.
Creating a Label for Specific Data
One common reason for creating customized controls is to fine-tune a control for specific types of data. For example, consider the Label control. In its standard form, it’s a flexible all-purpose tool that you can use to render text content and insert arbitrary HTML. However, in many situations it would be nice to have a higher-level way to output text that takes care of some of the encoding.
