Professional ASP.NET 1.1 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Professional ASP.NET 1.1 [Electronic resources] - نسخه متنی

Alex Homeret

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید






Writing a TextBox Control

To understand some of the more advanced development aspects of server control, it is useful to actually write a simple control like a textbox. Although this isn't the most exciting control to write, especially since it already exists, it clearly demonstrates some important aspects that control authors need to understand including:



Interacting with postback



Using viewstate



Raising events from a control



To render a textbox, the server control needs to output an HTML

input element with a

type attribute containing the value

"text" . Using the

WebControl as our control's base class makes this first task simple. We have to perform several main tasks:



Create a new class that derives from the

WebControl class.



Implement a public constructor that calls the base class constructor specifying that our server control should output an

input element.



Override the

AddAttributesToRender method. This is called to allow derived classes to add attributes to the root element (

input ).



Add the

type attribute with a value of

text .



Add a

name attribute whose value is derived from the

UniqueID property. This property is used by ASP.NET to hold the unique

id of each control. We have to output the name property containing this value since an HTML form will not postback the value entered into an input field without a name.



Here is the sourcecode that implements these steps:


using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

namespace WroxControls

{

public class MyTextBox : WebControl

{

public MyTextBox() : base("input")

{

}

protected override void AddAttributesToRender(HtmlTextWriter writer)

{

base.AddAttributesToRender(writer);

writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);

writer.AddAttribute(HtmlTextWriterAttribute.Type, "input" );

}

}

}


The implementation of the

AddAttributesToRender method calls the base-class implementation. This is called since the base-class implementation will add various other attributes, such as style attributes, to your element depending on other properties that have been set in the base class.

Once compiled, you can use the following ASP.NET page to reference your control (assuming the control is called

MyTextBox and is compiled to the

MyTextBox.dll assembly):


<%@ Register TagPrefix="Wrox" Namespace="WroxControls"

Assembly="MyTextBox" %>

<html>

<body>

<form runat="server">

<H3>TextBox Control</H3>

<P>Enter a value: <Wrox:MyTextBox runat="server" />

<BR>

<ASP:Button Text="Postback" runat="server" />

</form>

</body>

</html>


In this ASP.NET page we've declared an instance of the

MyTextBox control, and a button within a form element so that we can cause a postback to occur. When viewed in the browser, this page initially renders as shown in Figure 18-18:


Figure 18-18:

Enter some text into the textbox and click the Postback button. You will find that after the postback occurs, the textbox is blank again. This demonstrates that as a control developer, you have to do some work for a control to become intelligent.

For an intelligent textbox, you need to access the postback data submitted as part of a form. If data is present for the control, add a

value attribute to the

input element. Assuming you can populate a member variable

_text with the postback for the textbox, you could add the following code to output a

value attribute:


public MyTextBox() : base ("input")

{

}

string _value;

protected override void AddAttributesToRender(HtmlTextWriter writer)

{

base.AddAttributesToRender(writer);

writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);

writer.AddAttribute(HtmlTextWriterAttribute.Type, "input" );

if ( _value != null )

writer.AddAttribute( "value", _value );

}


To access postback data, a server control has to implement the

IPostBackDataHandler interface.


The IPostBackDataHandler Interface


The

IPostBackDataHandler interface has two methods:

bool IPostBackDataHandler.LoadPostData(string postDataKey,
NameValueCollection postCollection);
void IPostBackDataHandler.RaisePostDataChangedEvent()

The

LoadPostData method is called when postback occurs and a control has postback data. The method is called in sequence for all controls on a page that need to access postback data. If a control does not have any postback data (for example, if the control was disabled), the method is not called. A control can explicitly ask for this method to be called even if does not have postback, by calling the

RegisterRequiresPostBack method of the

Page class.

The

LoadPostData call passes all of the postback data submitted in a form using a

NameValueCollection . A control can access any of the data in this collection. To access the specific postback data item associated with a control, the

postDataKey variable is used as the key into the collection. The value of this key field is the unique name assigned to the control either automatically by ASP.NET, or the value assigned by a user if an

id attribute was specified in the control declaration.

The

LoadPostData method returns a

Boolean value. If

true is returned, the

RaisePostDataChangedEvent method will be called after the

LoadPostData method has been called for all other server controls on a page with postback data. If a

false value is returned, the

RaisePostDataChangedMethod will not be called.

A control should return

true in the

LoadPostData method to raise an event as the result of certain data being present, or changes to data caused as a result of postback data. Events must be raised in the

RaisePostDataChangedEvent method since raising events in

LoadPostData will cause unpredictable results. Any event handler that caught and processed an event raised in

LoadPostData would not be able to depend on consistent state being present in other controls, since such controls may not have initialized or updated their state based upon postback.

To make your textbox control intelligent, implement the

IPostBackDataHandler interface as follows:


bool IPostBackDataHandler.LoadPostData(string postDataKey,

NameValueCollection postCollection)

{

_value = postCollection[postDataKey];

return false;

}


void IPostBackDataHandler.RaisePostDataChangedEvent()

{

}






Note

The

NameValueCollection class is defined in the

System.Collections.Specialized namespace, and so it has to be imported into any class files before this code will compile.


The

LoadPostData method uses the

postDataKey variable to copy the postback data for the control into the

_value variable. Since this variable is already being used to render a value, if present, the textbox should now appear intelligent. We return

false from

LoadPostData since we're not yet interested in raising any events. For the same reason, the implementation of the

RaisePostDataChangedEvent is also empty.

With these changes in place, enter a value into the textbox, and hit the Postback button to cause a roundtrip to the server. Any value entered will be remembered, since the value attribute will be emitted by our control, as seen in Figure 18-19:


Figure 18-19:

If you look at the HTML output for this page, you'll see the value attribute, which is why the value redisplays correctly:


<html>

<body>


<form name="_ctl0" method="post" action="mytextbox.aspx" id="_ctl0">

<input type="hidden" name="__VIEWSTATE"

value="dDwtMzM4NzUxNTczOzs+YFp06wUt7ZoMN2kDeJMiO1YBg9U=" />


<H3>TextBox Control (VB)</H3>

<P>Enter a value: <input id="name" name="name" type="input" />

<BR>


<input type="submit" name="_ctl1" value="Postback" />


<P>

<span id="status"></span>


</form>


</body>

</html>


If you declared a couple more of

MyTextBox controls in the ASP.NET page, entered values, and used the button to cause postback, you would see that they all remember their state, just as with other standard server controls in earlier chapters. Each declared server control has a unique name assigned to it, so each manages its own postback and renders itself correctly, without interfering with other controls. All control behavior added to the textbox is encapsulated and reusable. This makes server controls very powerful, and makes complex page development much simpler. Once individual controls are written, they take care of all the plumbing code to remember their state.


Raising Events from Controls


ASP.NET provides a powerful server-side event model. As a page is being created, server controls can fire events that are caused either by aspects of a client-side postback, or by controls responding to page code that is calling their methods or changing properties. These events can be captured in an ASP.NET page, or can be caught by other server controls.

ASP.NET server controls support events using delegates, just like any other .NET class does. With ASP.NET, the

EventHandler delegate is used when defining most events. The standard definition for this delegate defines a method signature with two parameters–the first parameter of type

object , and the second of type

EventArgs –that you will have seen many times in event handlers like

Page_Load . When a server control raises an event, the first parameter of the event method will contain a reference to the server control. Supplying a reference to the control that raised the event allows event methods to handle events from multiple controls.Depending on the parameters you need your controls to raise, you can replace the

EventHandler delegate with a custom delegate.

To demonstrate events, we'll add an event to our textbox control that is raised when its value changes.

Events in Action


To support an event in the textbox control, first define a public

event of the type

EventHandler called

TextChanged . The code in C# will look as follows:


public event EventHandler TextChanged;


In VB:


Event TextChanged As EventHandler


Next, change the

LoadPostData implementation to always return

true :

bool IPostBackDataHandler.LoadPostData(string postDataKey,
NameValueCollection postCollection)
{
_value = postCollection[postDataKey];

return true;
}


This will cause the

RaisePostDataChangedEvent to be called, from which it is safe to raise events caused by postback.

In this method, we use the delegate to raise a

TextChanged event, assuming there are listeners (for example, the delegate is not

null ):


void IPostBackDataHandler.RaisePostDataChangedEvent()

{

if ( TextChanged != null )

TextChanged( this, EventArgs.Empty );

}


You're probably wondering if these changes will always raise a

TextChanged event when postback occurs, even if the text has not changed. As it stands, this is precisely what will happen. We'll shortly refine the event handler to only call the event when the value actually changes. But for now, let's just see how events are raised.

To use the new event, declare an event handler called

OnNamedChanged in the ASP.NET page:


<script runat="server" language="C#">

private void OnNameChanged( object sender, EventArgs e )

{

status.Text = "Value changed to " + name.Text;

}

</script>


To wire the event handler up to the text changed event, add an

OnTextChanged attribute and specify the name of the method to be called when the event is fired (in this case,

OnNameChanged ):


<P>Enter a value: <Wrox:MyTextBox id="name" runat="server"

OnTextChanged="OnNameChanged" />


Prefix an event name with

'On' to associate event handlers with server-control events in server control declarations. If your control had an additional event called

TextInvalid , the event name would be

OnTextInvalid .

The

OnNameChanged event handler displays the value of text in a label field when the event is fired:


<asp:Label runat="server" id="status" />


For your event handler code to work, you have to declare a

Text property on your server control that exposes the held value so it can be displayed:


public string Text

{

get { return _value; }

}


With all these changes in place, entering a value of Events in ASP.NET in the textbox and calling the Postback button will cause a message to appear below the Postback button, as shown in Figure 18-20:


Figure 18-20:

The following code shows how to implement a server-side control that supports events in VB:


Imports System

Imports System.Web

Imports System.Web.UI

Imports System.Web.UI.WebControls

Imports System.Collections.Specialized


Namespace WroxControls

Public Class MyTextBoxVB

Inherits WebControl

Implements IPostBackDataHandler


Public Sub New()

MyBase.New("input")

End Sub 'New


Public Event TextChanged As EventHandler

Private _value As String

Public ReadOnly Property Text As String

Get

Return _value

End Get

End Property


Protected Overrides Sub AddAttributesToRender(writer As HtmlTextWriter)

MyBase.AddAttributesToRender(writer)

writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID)

writer.AddAttribute(HtmlTextWriterAttribute.Type, "input")

If Not (_value Is Nothing) Then

writer.AddAttribute("value", _value)

End If

End Sub


Function LoadPostData(postDataKey As String, _

postCollection As NameValueCollection) As Boolean

Implements IPostBackDataHandler.LoadPostData

_value = postCollection(postDataKey)

Return True

End Function


Sub RaisePostDataChangedEvent()

Implements IPostBackDataHandler.RaisePostDataChangedEvent

RaiseEvent TextChanged(Me, EventArgs.Empty)

End Sub

End Class

End Namespace


The Framework Event Pattern


When supporting events, classes in the .NET Framework define a protected method called

OnEventName , which actually raises the event. The reason for this is that it enables derived classes to perform event handling by overriding a method instead of attaching a delegate. This is simpler and more efficient. To follow this pattern for the

TextChange event, you could implement the

IPostBackDataHandler.RaisePostDataChangedEvent method as follows:

void IPostBackDataHandler.RaisePostDataChangedEvent()
{

OnTextChanged(EventArgs.Empty );
}

protected void OnTextChanged (EventArgs e)

{

if ( TextChanged != null )

TextChanged ( this, e);

}


In this code, the

RaisePostDataChangedEvent method calls the

OnTextChanged method to raise the event. Derived classes that want to raise events can simply override the

OnTextChanged method to catch the event, and optionally call the base-class implementation if they want other listeners to receive the event.

Causing Postback from Any Element–IPostBackEventHandler


In HTML, only the

button and

imagebutton elements can actually cause a postback to occur. When designing controls, you may want other elements of a control's user interface such as an anchor element to cause postback. You may also want a control's user interface to be able to raise different types of postback events, which the control can process to manipulate its user interface. For example, a calendar control may want to have previous-month and next-month events.

To show how to support postback events, we'll write a simple counter control. The control's user interface displays a counter (starting at 50) and provides two hyperlinks that enable you to increase or decrease the value (see Figure 18-21):


Figure 18-21:

If you click Increase Number twice, the count would increase to 52, as shown in Figure 18-22:


Figure 18-22:

If you click Decrease Number four times, the number goes down to 48 (see Figure 18-23):


Figure 18-23:

This control is raising postback events to itself. To achieve this, a control must do two things:



Derive from the

IPostBackEventHandler interface and implement the

RaisePostBackEvent method.



Call the

Page class'

GetPostBackEventReference method to create some script code that can be rendered into a page to force a postback to occur. This of course means the browser must support JavaScript for this feature.



The

IPostBackEventHandler class's

RaisePostBackEvent method is called when a postback is caused by script code calling

Page class's

GetPostBackEventReference . The

RaisePostBackEvent method accepts a single

string parameter that can be used to determine what event has been raised. In the case of the counter control, we use the value

inc to signal our counter should be increased, and

dec to signal it should be decreased:


public void RaisePostBackEvent(string eventArgument)

{

if ( eventArgument == "inc" )

Number = Number + 1;

if ( eventArgument == "dec" )

Number = Number – 1;

}


When a control renders its user interface, it calls the

Page class's

GetPostBackEventReference method passing itself as a parameter. The return value is a string containing JavaScript code that will cause a postback event to be raised when it is called. In our control, this JavaScript is placed into an anchor element, so when the user clicks the anchor, a postback occurs.

The following code shows how this method was used to generate the

inc postback event:


writer.Write("<a href="javascript:" +

Page.GetPostBackEventReference(this,"inc") +

"'>Increase Number</a>");


Here is the complete sourcecode for the counter control:


using System;

using System.Web;

using System.Web.UI;

namespace WroxControls

{

public class MyFirstControl : Control,

IPostBackEventHandler

{

public int Number

{

get

{

if ( ViewState["Number"] != null )

return (int) ViewState["Number"];

return 50;

}

set

{

ViewState["Number"] = value;

}

}


public void RaisePostBackEvent(string eventArgument)

{

if ( eventArgument == "inc" )

Number = Number + 1;

if ( eventArgument == "dec" )

Number = Number – 1;

}


protected override void Render(HtmlTextWriter writer)

{

writer.Write("The Number is " + Number.ToString() + " (" );

writer.Write("<a href="javascript:" +

Page.GetPostBackEventReference(this,"inc") +

"'>Increase Number</a>");

writer.Write(" or " );

writer.Write("<a href="javascript:" +

Page.GetPostBackEventReference(this,"dec") +

">Decrease Number)</a>");

}

}

}


This control demonstrates how to:



Derive and implement the

IPostBackEventHandler



Use the

Page.GetPostBackEventReference to raise two events



The next section in the chapter covers viewstate, so for now ignore the implementation of the

Number property.

The control renders the following HTML:


<html>

<body>


<form name="_ctl0" method="post" action="myfirstcontrol.aspx" id="_ctl0">

<input type="hidden" name="__VIEWSTATE"

value="dDwxMjc4NDMyNjExO3Q8O2w8aTwxPjs+O2w8dDw7bDxpPDE+Oz47bDx0PHA8bDxOdW1iZXI

7PjtsPGk8NDg+Oz4+Ozs+Oz4+Oz4+Oz6XT9F3HCi1voCIwAsaNszynaGM2w==" />


The Number is 48 (<a href="'_ctl1','inc')">Increase

Number</a> or <a href="'_ctl1','dec')">Decrease

Number)</a>


<input type="hidden" name="__EVENTTARGET" value=" />

<input type="hidden" name="__EVENTARGUMENT" value=" />

<script language="javascript">

<!--

function __doPostBack(eventTarget, eventArgument) {

var theform;

if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {

theform = document.forms["_ctl0"];

}

else {

theform = document._ctl0;

}

theform.__EVENTTARGET.value = eventTarget.split("$").join(":");

theform.__EVENTARGUMENT.value = eventArgument;

theform.submit();

}

// -->

</script>

</form>


</body>

</html>


Notice how the following script block containing the

__doPostBack function is automatically rendered into the output stream when an ASP.NET server control calls the

Page.GetPostBackEventReference reference:

<script language="javascript">
<!--
function __doPostBack(eventTarget, eventArgument) {
var theform;
if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {
theform = document.forms["_ctl0"];
}
else {
theform = document._ctl0;
}
theform.__EVENTTARGET.value = eventTarget.split("$").join(":");
theform.__EVENTARGUMENT.value = eventArgument;
theform.submit();
}
// -->
</script>

This function is called by the script code returned from

Page.GetPostBackEventReference :

<a href="'_ctl1','inc')">

Now that we have covered handling postback and events, let's look at how a control can persist state during postback using viewstate.


Using ViewState


After an ASP.NET page is rendered, the page object, which created the page and all of its server controls, is destroyed. When a postback occurs, a new page and server-control objects are created.

When writing a server control you often need to store and manage state. Since a control is created and destroyed with each page request, any state held in object member variables will be lost. If a control needs to maintain state, it has to do so using another technique. As you have seen with the textbox control, one way of managing state is to use postback. When a postback occurs, any postback data associated with a control is made available to it via the

IPostBackData interface. A control can therefore repopulate its class variables, making the control appear to be stateful.

Using postback data to manage the state of a control is a good technique when it can be used, but there are some drawbacks. The most obvious one is that only certain HTML elements, such as

input , can use postback. If you had a label control that needed to remember its value, you couldn't use postback.

Also, postback is only really designed to contain a single item of data. For example, our textbox control needs to remember its last value so it can raise a

TextChanged event when the value changes. To maintain this additional state, one option would be to use hidden fields. When a control renders its output, it could also output hidden fields with other values that need to be remembered. When a postback occurs, these values would be retrieved into the

LoadPostData method. This approach would work for a single control, but could be problematic in cases where many instances of the same control were on a page (for example, what would you call the hidden fields? How could you ensure the names do not clash with names a page developer may have used?)

To resolve the problems of managing state ASP.NET has a feature called viewstate. In a nutshell, viewstate is a hidden input field that can contain state for any number of server controls. This hidden field is automatically managed for you, and as a control author you never need to access it directly.

Introducing the StateBag


All server controls have a

ViewState property. This is defined in the

Control class as the

StateBag type, and allows server controls to store and retrieve values that are automatically round-tripped and recreated during a postback.

During the save state stage of a page, the ASP.NET Framework enumerates all server controls within a page and persists their combined state into a hidden field called

__VIEWSTATE . If you view any rendered ASP.NET containing a form element, you will see this field:


<input type="hidden" name="__VIEWSTATE" value="dDwtMTcxOTc0MTI5NDs7Pg==" />


When a postback occurs, ASP.NET decodes the

__VIEWSTATE hidden field and automatically repopulates the viewstate for each server control as they are created. This reloading of state occurs during the load state stage of a page for controls that are declared on an ASP.NET page.

If a control is dynamically created, either on a page or within another composite control, the state will be loaded at the point of creation. ASP.NET keeps track of what viewstate hasn't been processed, and when a new control is added to the

Controls property of a

Control (remember a page is a control), it checks to see if it has any viewstate for the control. If it has, it is loaded into the control at that point.

To see viewstate in action, change your textbox control to store its current value in viewstate, rather than the

_value field. By doing this, when

LoadPostData is called to enable the textbox control to retrieve its new value, you can compare it with the old value held in viewstate. Return

true if the values are different. This will cause a

TextChanged event to be raised in

RaisePostDataChangedEvent . If the values are the same, return

false , so that

RaisePostDataChangedEvent is not called, and no event is raised.

The

StateBag class implements the

IDictionary interface, and for the most part is used just like the

Hashtable class with a string key. All items stored are of the

System.Object type, and thus, any type can be held in the viewstate, and casting is required for retrieving an item.

In the earlier textbox control, we used a

_value string member variable to hold the current value of the textbox. Delete that variable and rewrite the property to use viewstate:


public string Text

{

get

{

if ( ViewState["value"] == null )

return String.Empty;

return (string) ViewState["value"];

}

set

{

ViewState["value"] = value;

}

}


Since you've deleted the

_value member variable and replaced it with this property, you need to change all references to it, with the

Text property. You could directly reference the

ViewState where you previously used

_value , but it's good practice to use properties to encapsulate your usage of viewstate, making the code cleaner and more maintainable (for example, if you changed the viewstate key name used for the text value, you'd only have to do it in one place).

With this new property in place, you can revise the

LoadPostData to perform the check against the existing value as discussed:


bool IPostBackDataHandler.LoadPostData(string postDataKey,

NameValueCollection postCollection)

{

bool raiseEvent = false;

if ( Text != postCollection[postDataKey] )

raiseEvent = true;

Text = postCollection[postDataKey];

return raiseEvent;

}


Before testing this code to prove that the

TextChanged event is now only raised when the text changes, you need to make a small change to the ASP.NET page. As you'll recall from earlier, we have an event handler that sets the contents of a label to reflect our textbox value when

TextChanged is raised:


<script runat="server" language="C#">

private void OnNameChanged( object sender, EventArgs e )

{

status.Text = "Value changed to " + name.Text;

}

</script>


The label control uses viewstate to remember its value. When a postback occurs, even if this event is not raised, the label will still display the text from the previous postback, making it look like an event was raised. So, to know if an event really was raised, reset the value of the label during each postback. You could do this within the page

init or

load events, but since the label uses viewstate to retain its value, you can simply disable viewstate for the control using the

EnableViewState attribute as follows:


<ASP:Label runat="server" EnableViewState="false" id="status" />


During the save state stage of a page, the ASP.NET page framework will not persist viewstate for the controls with an

EnableViewState property of

false . This change to the page will therefore make the label forget its value during each postback.





Note

Setting

EnableViewState to

false does not prevent a control from remembering state using postback, as the state is rendered to the browser as the

Text property of the control, and resubmitted during postback As such, should you need to reset the value of a textbox, you'd have to clear the

Text property in a page's

init or

load event.


With all these changes made, if you enter a value of Wrox Press and press the Postback button, you will see that during the first postback our event is fired, and our label control displays the value (see Figure 18-24):


Figure 18-24:

If you click the postback button again, the textbox control will use its viewstate to determine that the postback value has not changed, and it will not fire its

TextChanged event. Since the label control does not remember its state, as viewstate was disabled for it, the value-changed message will not appear during the second postback since the label will default back to its original blank value (see Figure 18-25):


Figure 18-25:

Our textbox control is now pretty functional for a simple control–it can remember its value during postback, can raise events when its text changes, and can have style properties applied in the same way as other Web controls using the various style attributes:


<Wrox:MyTextBox id="name" runat="server"

BackColor="Green"

ForeColor="Yellow"

BorderColor="Red"

OnTextChanged="OnNameChanged" />


More on Events


Any server control that derives from the

Control base classes automatically inherits several built-in events that page developers can also handle:



Init : Called when a control has to be constructed and its properties have been set



Load :Called when a control's viewstate is available



DataBinding :Called when a control bound to a data source should enumerate its data source and build its control tree



PreRender :Called just before the UI of a control is rendered



Unload : Called when a control has been rendered



Disposed :Called when a control is destroyed by its container



These events behave just like any other event. For example, you could catch the

PreRender event of the

TextBox and restrict its length to seven characters, by adding an

OnPreRender attribute to the control declaration:


<P>Enter a value: <Wrox:MyTextBox id="name" runat="server"

BackColor="Green"

ForeColor="Yellow"

BorderColor="Red"

OnTextChanged="OnNameChanged"

OnPreRender="OnPreRender" />


And an event handler that restricts the size of the

TextBox value if it exceeds seven characters:


private void OnPreRender( object sender, EventArgs e )

{

if ( name.Text.Length > 7 )

name.Text = name.Text.Substring(0,7);

}


As a control author, you can also catch these standard events within your controls. Do this by either wiring up the necessary event wire-up code, or, as you've seen already, overriding one of these methods:



OnInit(EventArgs e)



OnLoad(EventArgs e)



OnDataBinding(EventArgs e)



OnPreRender(EventArgs e)



OnUnload(EventArgs e)



Disposed()



The default implementation of each of these methods raises the associated events listed earlier. For example,

OnInit fires the

Init event, and

OnPreRender fires the

PreRender event. When overriding one of these methods, you should call the base-class implementation of the method so that events are still raised, assuming that is the behavior you want:

protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if ( _text == null )
_text = "Here is some default text";
}

Event Optimization in C# Using the EventHandlerList


When an event is declared within a class definition, additional memory must be allocated for an object instance at runtime for the field containing the event. As the number of events a class supports increases, the memory consumed by each and every object instance increases. Assuming that a control supports ten events (six built-in and four custom events), and assuming an event declaration requires roughly 16 bytes of memory, each object instance will require 160 bytes of memory. If nobody is interested in any of these events, this is a lot of overhead for a single control.

To only consume memory for events that are in use, ASP.NET controls can use the

EventHandlerList class. The

EventHandlerList is an optimized list class designed to hold delegates. The list can hold any number of delegates, and each delegate is associated with a key. The

Control class has an

Events property that returns a reference to an instance of the

EventHandlerList . This instantiates the class on demand, so if no event handlers are in use, there is almost no overhead:


protected EventHandlerList Events

{

get

{

if (_events == null)

_events = new EventHandlerList();

}

return _events;

}


The

EventHandlerList class has two main methods:


void AddHandler( object key, Delegate handler );


And:


void RemoveHandler( object key, Delegate handler );


AddHandler is used to associate a delegate (event handler) with a given key. If the method is called with a key for which a delegate already exists, the two delegates will be combined and both will be called when an event is raised.

RemoveHandler simply performs the reverse of

AddHandler .

Using the

Events property, a server control should implement support for an event using a property declared as the type event:


private static readonly object _textChanged = new object();

public event EventHandler TextChanged

{

add { Events.AddHandler(EventPreRender, value); }

remove { Events.RemoveHandler(EventPreRender, value); }

}


Since this property is declared as an event, you have to use the

add and

remove property accessor declarations, rather than

get and

set . When

add or

remove are called, the value is equal to the delegate being added or removed, so we use this value when calling

AddHandler or

RemoveHandler .





Note

As Visual Basic .NET does not support the

add /

remove accessor, you can't use optimized event handlers in Visual Basic .NET.


To create a unique key for your events, which will not clash with any events defined in your base classes, define a static, read-only member variable called

_textChanged , and instantiate it with an object reference. You could use other techniques for creating the key, but this approach adds no overhead for each instance of the server control, and is also the technique used by the built-in ASP.NET server controls. By making the key value static, there is no per-object overhead.

Checking and raising an event using the

Events property is done by determining if a delegate exists for the key associated with an event. If it does, you can raise it to notify one or more subscribed listeners:


void IPostBackDataHandler.RaisePostDataChangedEvent()

{

EventHandler handler = (EventHandler) Events[_textChanged];

if (handler != null)

handler(this, EventArgs.Empty);

}


Using the

EventHandler technique, a control can implement many events without causing excessive overhead for controls that do not have any event listeners associated with them. Since the

Control class already implements most of the work for you, it makes sense to always implement your events in this way.

Tracking ViewState


When adding and removing items from viewstate, they are only persisted by a control if its viewstate is being tracked. This tracking only occurs after the initialization phase of a page is completed. This means if a server control makes any changes to itself or to another control before this phase, and the

OnInit event has been raised, the changes will not be saved.


Types and ViewState


We mentioned earlier that the

StateBag method used for implementing viewstate allows any type to be saved and retrieved from it. While this is true, this does not mean that you can use any type with it. Only types that can be safely persisted can be used. As such, types that maintain resources such as database connections or file handles should not be used.

ViewState is optimized and designed to work with the following types:



Int32 ,

Boolean ,

String , and other primitive types



Arrays of

Int32 ,

Boolean ,

String , and other primitive types



ArrayList ,

Hashtable .



Types that have a type converter. A type converter is a class derived from

System.ComponentModel.TypeConverter that can convert one type into another. For example, the type converter for the

Color class can convert the string

red into the enumeration value for red. ASP.NET requires a type converter that can convert a type to and from a string.



Types that are serializable (marked with the serializable attribute, or support the serialization interfaces).



Pair ,

Triplet (defined in

System.Web.UI , and respectively hold two or three of the other types listed).



ViewState is converted from these types into a string by the Limited Object Serialization (LOS) formatter class (

System.Web.UI.LosFormatter ).

The LOS formatter used by ASP.NET encodes a hash code into viewstate when a page is generated. This hash code is used during postback to determine if the static control declarations in an ASP.NET page have changed (for example, the number and ordering of server controls declared within an ASP.NET page). If a change is detected, all viewstate is discarded, since viewstate cannot be reliably processed if the structure of a page has changed. This limitation stems from the fact that ASP.NET automatically assigns unique identifiers to controls, and uses these identifiers to associate viewstate with individual given controls. If a page structure changes, so do the unique identifiers assigned to controls, and therefore the viewstate-control relationship is meaningless. In case you're wondering, yes, this is one technical reason why ASP.NET only allows a page to postback to itself.


More on Object Properties and Template UI


Earlier, we discussed how the default control builder of a server control would automatically map sub- elements defined within a server-control declaration to public properties of that control. For example, consider the following server-control declaration:

<Wrox:ICollectionLister id="SessionList" runat="server">
<HeadingStyle ForeColor="Blue">
<Font Size="18"/>
</HeadingStyle>
<ItemStyle ForeColor="Green" Font-Size="12"/>
</Wrox:ICollectionLister>

The control builder of the

ICollectionLister control shown here would try to initialize the

HeadingStyle and

ItemStyle object properties, determining the type of the object properties by examining the meta data of the

ICollectionLister class using reflection. As the

HeadingStyle element in this example has a

Font sub-element, the control builder would determine that the

HeadingStyle object property has an object property of

Font .

Using the ICollectionLister Server Control


The

ICollectionLister server control is a simple composite control that can enumerate the contents of any collection class implementing the

ICollection . For each item in the collection, it creates a

Label control, and sets the text of the label using the

ToString method of the current item in the collection. This causes a linebreak because for each item in the collection, the label starts with

<BR> . The control also has a fixed heading of

ICollection Lister Control which is also created using a label control.

The

ICollectionLister control has three properties:



DataSource : A public property of the

ICollection type. When

CreateChildControls is called, this property is enumerated to generate the main output of the control.



HeadingStyle : A public property of the

Style type. This allows users of the control to specify the style attributes used for the hard-coded heading text. The

Style.ApplyStyle method is used to copy this style object into the

Label control created for the header.



ItemStyle : A public property of the

Style type. This allows users of the control to specify the style attributes used for each of the collections that is rendered. The

Style.ApplyStyle method is used to copy this style object into the

Label control created for each item.



The code for this server control is shown here:


using System;

using System.Web;

using System.Web.UI;

using System.Collections;

using System.Web.UI.WebControls;

namespace WroxControls

{

public class ICollectionLister : WebControl, INamingContainer

{

ICollection _datasource;

public ICollection DataSource

{

get { return _datasource; }

set { _datasource = value; }

}


Style _headingStyle = new Style();

public Style HeadingStyle

{

get{ return _headingStyle; }

}


Style _itemStyle = new Style();

public Style ItemStyle

{

get{ return _itemStyle; }

}


protected override void CreateChildControls()

{

IEnumerator e;

Label l;


// Create the heading, using the specified user style

l = new Label();

l.ApplyStyle( _headingStyle );

l.Text = "ICollection Lister Control";

Controls.Add( l );


// Create a label for each key/value pair in the collection

if ( _datasource == null )

throw new Exception("Control requires a datasource");


e = _datasource.GetEnumerator();

while( e.MoveNext() )

{

l = new Label();

l.ApplyStyle( _itemStyle );

l.Text = "<BR>" + e.Current.ToString();

Controls.Add( l );

}

}

}

}


There is nothing new in this code that hasn't already been discussed. Refer to Chapter 15 for an explanation of using

IEnumerator and

ICollection .

The following ASP.NET page uses the

ICollectionLister control to list the contents of a string array. This array is created in the

Page_Load event and associated with a server control which has been given a name/Id of

SessionList in this page:


<%@ Register TagPrefix="Wrox" Namespace="WroxControls"

Assembly="DictionaryLister" %>

<script runat="server" language="C#">

void Page_Load( object sender, EventArgs e )

{


string[] names = new string[3];

names[0] = "Richard";

names[1] = "Alex";

names[2] = "Rob";

SessionList.DataSource = names;

}

</script>


<Wrox:ICollectionLister id="SessionList" runat="server">

<HeadingStyle ForeColor="Blue">

<Font Size="18"/>

</HeadingStyle>


<ItemStyle ForeColor="Green" Font-Size="12"/>

</Wrox:ICollectionLister>


The output from this page (if viewed in color) is a blue header with green text for each item in the collection:


Figure 18-26:

For controls that have fixed style and layout requirements, initializing them using object properties as we have in the

ICollectionLister control is a good approach. You will have seen the same approach used throughout the standard ASP.NET server controls, such as the data grid and data list. However, for a control to provide ultimate flexibility, it's better to enable the user of the control to define what the UI of a control looks like by using templates. You've seen this in earlier chapters, with controls such as the data grid.

Using Templates


As you saw in Chapter 7, templates allow the users of a control to define how chunks of its UI–such as the header or footer–should be rendered.

Templates are classes that implement the

ITemplate interface. As a control developer, you declare public properties of the

ITemplate type to support one or more templates. When the default control builder sees a property of this type, it knows to dynamically build a class that supports the

ITemplate interface, which can be used to render the section of UI the template defines.

Supporting template properties in a server control is relatively straightforward, although when using them within a data-bound control, things can initially seem a little complex, since the creation of child controls has to be handled slightly differently.

Let's introduce templates by rewriting the

ICollectionLister control to support a heading and item template. Make the following changes to your code:



Change the

HeadingStyle and

ItemStyle properties to the

ITemplate type.



Make the

HeadingStyle and

ItemStyle properties writeable. This has to be done since the objects implementing the

ITemplate interface are dynamically created by the ASP.NET page and then associated with the server control.



Use the

TemplateContainer attribute to give the control builder a hint about the type of object within which your templates will be instantiated. This reduces the need for casting in databinding syntax.



The changed code is shown here:


ITemplate _headingStyle;

[TemplateContainer(typeof(ICollectionLister))]

public ITemplate HeadingStyle

{

get{ return _headingStyle; }

set{ _headingStyle = value; }

}

ITemplate _itemStyle;

[TemplateContainer(typeof(ICollectionLister))]

public ITemplate ItemStyle

{

get{ return _itemStyle; }

set{ _itemStyle = value; }

}


At runtime, if a user specifies a template, the properties will contain a non-null value. Null means no template has been specified.

The

ITemplate interface has one method called

InstantiateIn . This method accepts one parameter of the type

Control . When called, this method populates the

Controls collections of the control passed in with one or more server controls that represent the UI defined within a template by a user. Any existing controls in the collection are not removed, so you can instantiate a template against another server control one or more times.





Note

A server control could use the

Page class'

LoadTemplate method (string filename) to dynamically load templates, but this is not recommended. It is very slow and is known to be unreliable. If you need dynamic templates, you should write your own class that implements the

ITemplate interface.


Using the

InstantiateIn method, you can change the

CreateChildControls to use your new template properties to build the server controls for the header and each item. Since we're not supporting the data-binding syntax yet, the UI created for each item in the collection will not contain any useful values.

In the following code, the

InstantiateIn method is called only if a template is not null. If a template is null, we throw an exception to let the user know the control needs a data source:


protected override void CreateChildControls()

{

IEnumerator e;

if ( _headingStyle != null )

_headingStyle.InstantiateIn( this );

if ( _datasource == null )

throw new Exception("Control requires a data source");

e = _datasource.GetEnumerator();

while( e.MoveNext() )

{

if( _itemStyle != null)

_itemStyle.InstantiateIn( this);

}

}


With the new template properties and revised

CreateChildControls , you can now declare a page that uses templates to style your controls UI. Here is a basic example that uses a

<H3> element for the heading, and some bold text for each item (remember we're not showing the item value yet):


<Wrox:ICollectionLister id="SessionList" runat="server">

<HeadingStyle>

<h3>ICollection Lister</H3>

</HeadingStyle>


<ItemStyle>

<BR><Strong>An item in the collection</Strong></BR>

</ItemStyle>

</Wrox:ICollectionLister>


With these changes, the UI will now render as shown in Figure 18-27:


Figure 18-27:

Although not visually stunning, these changes allow the UI of our control to be completely controlled and changed by the user in their declaration of our server control. As you've seen in previous chapters, this is a very powerful technique.

Your controls template support can use data-binding syntax without any additional changes. However, it is limited to the data it can access. You can access public properties or methods on the page within which the control is declared, or any public property or method of any other server control you have access to. For example, if you had a

Name public property declared in your ASP.NET page, you could bind your item template to this using the databinding syntax introduced in Chapter 7:

<ItemStyle>
<BR><Strong>An item in the collection: <%#Name%></Strong></BR>
</ItemStyle>

When this expression is evaluated, ASP.NET will try and locate the

Name property on the naming container first (the control in which the template was instantiated in this case); if it's not found there, it will check the ASP page. Assuming you defined this property to return a Templates Rock string, you'd see Figure 18-28 as the output from the control:


Figure 18-28:

To bind to a text field called

mylabel declared within the same page, use the following syntax:


<ItemStyle>

<BR><Strong>An item in the collection: <%#mylabel.Text%></Strong></BR>

</ItemStyle>


To bind to the naming container in which the template is instantiated, use the

Container. syntax:


<ItemStyle>

<BR><Strong>An item in the collection: <%#Container.DataItem%></Strong></BR>

</ItemStyle>


Using the last syntax, you could be forgiven for thinking you could enable the item template to access the current collection item being enumerated. To achieve this, it looks as if you'd simply add a public

object property to your

DataItem server control:


object _dataitem;

public object DataItem

{

get{ return _dataitem; }

}


After this, set that property to the current item being enumerated in the loop that instantiates the item template, as follows:


...

e = _datasource.GetEnumerator();

while( e.MoveNext() )

{

if ( _itemStyle != null )

{

// Set the current item

_dataitem = e.Current;

_itemStyle.InstantiateIn( this );

}

}

...


But if you made these changes and compiled them, you'd encounter an interesting problem, as seen in Figure 18-29:


Figure 18-29:

Each template item instantiated has the same value! This occurs because the data binding for controls instantiated using a template, or just added using

Controls.Add by hand, are not data bound unless the parent control is data-bound. This means your collection has already been enumerated, and the

DataItem will always point to the last item in the collection, as the controls instantiated by the item templates are data-bound. To resolve this problem, instantiate your template on a control that has a

DataItem property that holds the correct value. This control will not render any UI, and will do very little except expose the

DataItem property:


public class CollectionItem : WebControl, INamingContainer

{

object _dataitem;

public object DataItem

{

get{ return _value; }

}

public CollectionItem(object value)

{

_dataitem = value;

}

}


The class derives from the

WebControl since it will be the container for the controls instantiated by the item template. It also implements the

INamingContainer interface to signal that it is a naming container for any child controls. This is important. Without this, the data-binding syntax would still refer to the parent control.

Using this new class, you can change your enumeration code to create an instance of the class for each item enumerated in the collection, passing in the current item being enumerated as a parameter to the constructor. The item template is then instantiated against the

CollectionItem object created, before being added as a child control of the

ICollectionLister .

Here is the revised section of the enumeration code:


...

e = _datasource.GetEnumerator();

CollectionItem item;

while( e.MoveNext() )

{

if ( _itemStyle != null )

{

item = new CollectionItem( e.Current );

_itemStyle.InstantiateIn( item );

Controls.Add( item );

}

}

...


The end result of these changes is that the server control hierarchy shown in Figure 18-30 will be created:


Figure 18-30:

At the top of this diagram is the instance of the

ICollectionLister control declared on the page. Assuming the

DataSource associated with the control only contained two items, the

CollectionItem object would have two

CollectionItem child server controls. Each of these child server controls has a

DataItem property that exposes the associated collection item, passed in via the constructor. The item template is instantiated within each of the

CollectionItem objects, so its child controls vary depending on the "what's defined in the item" template. However, any data-binding using the

Container syntax will always refer back to its parent

CollectionItem , and therefore the correct

DataItem .

Your item template is now being instantiated within the

CollectionItem class. Thus, you have to update the

TemplateContainer attribute declared on the

ItemTemplate property to reflect this. Without this, ASP.NET would throw a cast exception when evaluating a data-binding expression:


ITemplate _itemStyle;

[TemplateContainer(typeof(CollectionItem))]

public ITemplate ItemStyle

{

get{ return _itemStyle; }

set{ _itemStyle = value; }

}


With these changes in place, your UI renders each item in the collection and displays its value, as shown in Figure 18-31:


Figure 18-31:

When implementing a control that supports templates, here are a few simple rules you should follow:



If you are going to instantiate a template more than once, do not instantiate it against the same instance of a control unless you have very good reason to do so. For example, if a template does not contain any data-binding syntax and you can ensure none of the controls will be assigned an

id (

ids must always be unique within a naming container).



If a control will be supporting a header template, it should always support a footer template.



This is important for scenarios where a user may want to create an HTML table (or any other item that has a start element, several items, and then an end item).



Be consistent with the intrinsic controls, and follow the same naming conventions that they use.




DataBind and OnDataBinding


When a server control can be data-bound, it should support a couple of additional features:



The ability for the page developer to determine when (and if) a control should data-bind itself This high degree of control over when a control accesses its data source, allows a page developer to optimize data source usage keeping it to a minimum.



The ability to recreate itself and all child controls during postback, without being connected to its data source. The goal here is to reduce load on the data source provider.



To signal a control to connect to its data source and create its child controls, a page developer calls the

Control.DataBind method. This call results in the

Control.OnDataBinding method being called and the

DataBinding event being raised. This behavior is in line with all other stages of pages.

When a server control's

OnDataBinding method is called, it should create its child control tree as we did previously in the

CreateChildControls method, but with a few changes:



The number of controls instantiated (the numbers of items in the collection) is remembered using

ViewState . You have to do this, as you need to recreate the same number of controls when a postback occurs. All other state will automatically be remembered by the other server controls instantiated as part of the template.



Because

OnDataBinding may be called one or more times, the

ClearChildViewState and

Controls.Clear methods are called to clear any existing viewstate for the control, and delete all child controls.



The

ChildControlsCreated property is set to

true . Setting this flag ensures that

CreateChildControls is not subsequently called.



The following code implements these changes:


protected override void OnDataBinding( EventArgs args )

{

base.OnDataBinding(args);

if ( _datasource == null )

throw new Exception("Control requires a data source");


// Clear all controls and state

ClearChildViewState();

Controls.Clear();

IEnumerator e;

int iCount;


if ( _headingStyle != null )

_headingStyle.InstantiateIn( this );


e = _datasource.GetEnumerator();

CollectionItem item;

while( e.MoveNext() )

{

if ( _itemStyle != null )

{

item = new CollectionItem( e.Current );

_itemStyle.InstantiateIn( item );

Controls.Add( item );

iCount++;

}

}


// Remember the number of controls, so we can recreate the

// same controls, without the data source.

ViewState["count"] = iCount;


// stop CreateChildControls() being called again


ChildControlsCreated = true;


// Ensure viewstate is being tracked

TrackViewState();

}


When a postback occurs, a server control's

CreateChildControls method will typically be called, unless a page developer explicitly calls

DataBind . This method should recreate the control tree, using only information stored in viewstate.

Here is the implementation of

CreateChildControls . The basic creation logic is similar to

OnDataBinding , except that the data source is not at all used:


protected override void CreateChildControls()

{

int iCount;

int i;

CollectionItem item;


if ( _headingStyle != null )

_headingStyle.InstantiateIn( this );


iCount = (int) ViewState["count"];

for( i=0; i< iCount; i++ )

{

if ( _itemStyle != null )

{

item = new CollectionItem( null );

_itemStyle.InstantiateIn( item );

Controls.Add( item );

}

}

}


The changes in this code are:



The data source hasn't been used.



The number of controls to create was determined by the

count property held in viewstate.



A null value was passed to the constructor of

CollectionItem , since the

DataItem will not be used.



With these changes in place, you have a data-bound templated control. The control should behave just like any of the built-in controls you have used.

/ 244