8.2 State Management


As with most aspects of Web application development, building custom controls involves thinking carefully about state management. In this section, we explore two mechanisms for maintaining state in custom controls: using view state and explicitly handling post-back data.

8.2.1 ViewState

All the Web controls in the base class libraries retain their state across post-backs, and thus users will expect all controls they use to work this way. This means that any controls you develop should support state retention. Fortunately, ASP.NET helps you by providing a collection of name /value pairs accessible by any control through its ViewState property that is an instance of System.Web.UI.StateBag . Any name/value pair placed in this collection before the rendering of a page is stored in the hidden __VIEWSTATE input field, and when the page is next accessed via a POST request, the contents of the hidden __VIEWSTATE input field are parsed and used to reconstitute the ViewState collection.

As an example of retaining state using the ViewState collection, Listing 8-25 shows the Name control shown earlier rewritten to save and restore its two properties from the ViewState collection. This version of the control no longer maintains local fields to store the property values but relies instead on the ViewState collection to always have the current values for all its properties.

This technique of using the ViewState collection as the state repository for properties works well for any simple property. More complex properties (such as lists or arrays of data) need to be saved to and restored from ViewState more carefully, as we will see shortly. It is important to note that you should always initialize any elements your control depends on in your control's constructor to guarantee that they will be initialized to some default values if they are accessed before they are set. Alternatively, you could implement the get method of each property to conditionally check that there is a valid entry in the ViewState collection before returning that value, and if there is none, initialize it with some default value.

Listing 8-25 Name Control Rewritten to Use ViewState
 Public Class NameControl   Inherits Control   Public Sub New()     ViewState("IsImportant") = True     ViewState("Name") = ""   End Sub   Public Property Name As String     Get       Return CType(ViewState("Name"), String)     End Get     Set (value As String)       ViewState("Name") = value     End Set   End Property   Public Property IsImportant As Boolean     Get       Return CBool(ViewState("IsImportant"))     End Get     Set (value As Boolean)       ViewState("IsImportant") = value     End Set   End Property   Protected Overrides Sub Render( _                           writer As HtmlTextWriter)     If IsImportant Then       writer.RenderBeginTag(HtmlTextWriterTag.H1)     Else       writer.RenderBeginTag(HtmlTextWriterTag.H2)     End If     writer.Write(Name)     writer.RenderEndTag()   End Sub End Class 

The effect of retaining our state across post-backs is that clients can now rely on our control retaining its state like all the other controls. Keep in mind that if the client explicitly disables ViewState at either the control or the Page level, the ViewState collection will be empty at the beginning of each request, and your control properties will always have their default values.

For an example of a client that depends on our control retaining its state across post-backs, consider the page shown in Listing 8-26. Notice that when the page is first accessed (when IsPostBack is false ), the Name and IsImportant properties of the Name controls are initialized, but on subsequent post-backs to the same page (when IsPostBack is true ), the control properties are not touched. Because our control is now saving its properties in the ViewState collection, the values of these properties will be properly restored.

Listing 8-26 Name Control Client Page Relying on Post-Back State Retention
 <%@ Page Language='VB' %> <%@ Register Namespace='EssentialAspDotNet.CustomControls'              TagPrefix='eadn' Assembly='name' %> <script runat='server'>   Protected Sub DoSubmit(src As Object, e As EventArgs)     ' do something   End Sub   Protected Sub Page_Load(src As Object, e As EventArgs)     If Not IsPostBack Then 'populate custom controls         m_nc1.Name = "Foo"         m_nc1.IsImportant = True         m_nc2.Name = "Bar"         m_nc2.IsImportant = False     End If      ' if not IsPostBack, no need to repopulate      ' controls because their state will have been      ' retained   End Sub </script> <html><body> <form runat='server'> <eadn:NameControl id='m_nc1' runat='server' /> <eadn:NameControl id='m_nc2' runat='server' /> <br/> <asp:Button runat='server' text='submit'             Onclick='DoSubmit'/> </form> </body></html> 

Using the ViewState collection works well for primitive types and for state that maps directly to properties. If you have more complex state in your control that can reasonably be represented only by using local data structures, it will be cumbersome to figure out how to use the ViewState to cache all your data structure state. Instead, you can override a pair of virtual methods defined in the Control base class, and manually populate and retrieve state from the ViewState stream. Any object that is serializable can be persisted to the ViewState stream, which includes all the standard collection classes in the base class libraries (and any of your own classes that are marked with the Serializable attribute).

For an example of a control that performs more sophisticated view state management, consider the BarGraph control shown in Listing 8-27. This control maintains three pieces of data internally: a list of strings, a list of doubles, and a single double instance. Its rendering involves showing a table consisting of color -filled span elements displaying the current values of the two lists, as shown in Figure 8-4. The values that populate these two lists are added when a client programmatically invokes the AddValue() method of the control, so there is no simple mapping between the state of this control and the ViewState property inherited from the Control class. Instead, the BarGraphControl class overrides the SaveViewState and LoadViewState functions to manually populate the object array to be serialized into the __VIEWSTATE field. The SaveViewState function is called when a control is asked to add what it wants to into the outgoing __VIEWSTATE field, and should return an array of objects to be serialized into the __VIEWSTATE field. In almost all cases, you will want to invoke the base class's version of SaveViewState and add the result into your locally constructed object array, as shown in the BarGraphControl 's implementation. This will ensure that any state managed by the base class will also be saved. In the BarGraphControl 's function, the three other pieces of state in the class are added to a locally constructed object array, and because each of the types added to the array is serializable, they will be successfully added to the view state stream. The LoadViewState function does the opposite ”it is called just before the Load event is fired , and it receives an object array as input used to rehydrate any local control state from the view state stream. Again, most implementations will want to call the base class version of LoadViewState with the first element of the array. In our BarGraphControl example, once the base class has been called, we load the state from each element of the array into our local fields.

Listing 8-27 BarGraph Control Performing Manual View State Management
 Public Class BarGraphControl   Inherits Control   Private _dataDescriptions As ArrayList   Private _dataValues As ArrayList   Private _max As Double = 0   Public Sub New()     _dataDescriptions = New ArrayList()     _dataValues = New ArrayList()   End Sub   Protected Overrides Sub LoadViewState(_                           ByVal savedState As Object)     If Not savedState Is Nothing Then       ' Load State from the array of objects that       ' was saved in SaveViewState       Dim vState As Object() = CType(savedState, Object())       If Not vState(0) Is Nothing Then         MyBase.LoadViewState(vState(0))       End If       If Not vState(1) Is Nothing Then         _dataDescriptions = CType(vState(1), ArrayList)       End If       If Not vState(2) Is Nothing Then         _dataValues = CType(vState(2), ArrayList)       End If       If Not vState(3) Is Nothing Then         _max = CType(vState(3), Double)       End If     End If   End Sub   Protected Overrides Function SaveViewState() As Object     Dim vState(4) As Object     vState(0) = MyBase.SaveViewState()     vState(1) = _dataDescriptions     vState(2) = _dataValues     vState(3) = _max     Return vState   End Function   Public Sub AddValue(ByVal name As String, _                       ByVal val As Double)     _dataDescriptions.Add(name)     _dataValues.Add(val)     If val > _max Then       _max = val     End If   End Sub   Protected Overrides Sub Render(_                       ByVal output As HtmlTextWriter)     output.RenderBeginTag(HtmlTextWriterTag.Table)     Dim elem As Object     For Each elem In _dataValues       'rendering details omitted (see sample)     Next elem     output.RenderEndTag()   End Sub End Class 
Figure 8-4. BarGraph Control Rendering

graphics/08fig04.gif

8.2.2 Explicit Post-Back Data Handling

Using view state to retain control state across post-backs is necessary for controls that render themselves as HTML elements whose contents are not sent as part of a POST request (such as tables and spans ). If you are building a control that renders itself using HTML elements whose contents are sent through a POST request, you can load the state of the control directly from the POST variable collection instead of further burdening the hidden __VIEWSTATE field.

For example, suppose you were building a control that rendered itself as an INPUT tag in HTML. The contents of INPUT tags within a FORM element are always sent back as part of a POST request, so instead of saving and loading your control's state to and from view state, you could tap directly into the POST body for its latest value. To do this, your control must implement the IPostBackDataHandler interface, shown in Listing 8-28.

Listing 8-28 IPostBackDataHandler Interface Definition
 Public Interface IPostBackDataHandler   Function LoadPostData( _              ByVal postDataKey As String, _              ByVal postCollection As NameValueCollection _            ) As Boolean   Sub RaisePostDataChangedEvent() End Interface 

For controls that implement this interface, the LoadPostData method will be invoked just before the Load event fires, and will contain the entire contents of the POST body in the NameValueCollection . The postDataKey string passed in will contain the unique identifier associated with your control, which can be used to index the postCollection to find the current value of your control within the POST variable collection. The result of this method should be true if you change the value of the control's state, false otherwise .

If your control wants to propagate change notifications through server-side events, you can use a method of IPostBackDataHandler called RaisePostDataChangedEvent that is called whenever you return true from your implementation of LoadPostData . To fire this event when your control's data has changed, you need to keep track of the last value of your control in addition to the current value. The easiest way to do this is by using view state to store the current value of your control, and then checking the value stored in view state against the value sent back as part of the POST request. If they are different, you know that the control was changed between the last request and the current request, and you should return true from LoadPostData to indicate so.

Listing 8-29 shows a custom control called CustomTextBox that implements IPostBackDataHandler . It renders itself as an INPUT tag and can therefore extract its value from the POST body whenever a request is processed . It also exposes a public event called TextChanged that is fired whenever the RaisePostDataChangedEvent is invoked. To ensure that this event is fired only when the contents of the INPUT control have changed, it stores its Text property in ViewState , and when it loads a new value in from the POST body, it checks to see whether it has changed and returns true or false accordingly .

Listing 8-29 A Control That Performs Explicit Post-Back Data Handling
 Public Class CustomTextBox   Inherits Control   Implements IPostBackDataHandler   Private m_Text As String   Public Property Text As String     Get       Return m_Text     End Get     Set (value As String)       m_Text = value     End Set   End Property   Public Event TextChanged As EventHandler   Public Function LoadPostData(postDataKey As String, _     postCollection As NameValueCollection) As Boolean _            Implements IPostBackDataHandler.LoadPostData     Dim presentValue As String = Text     Dim postedValue As String =                      postCollection(postDataKey)     If presentValue Is Nothing Or _                Not presentValue.Equals(postedValue) Then       Text = postedValue       Return True     End If     Return False   End Function   Public Sub RaisePostDataChangedEvent() _   Implements IPostBackDataHandler.RaisePostDataChangedEvent     RaiseEvent TextChanged(Me, EventArgs.Empty)   End Sub   Protected Overrides Sub Render(output As HtmlTextWriter)     output.AddAttribute(HtmlTextWriterAttribute.Name, _                          UniqueID)     output.AddAttribute(HtmlTextWriterAttribute.Value, _                          Text)     output.RenderBeginTag(HtmlTextWriterTag.Input)     output.RenderEndTag()   End Sub End Class 

Many controls in the base class libraries implement IPostBackDataHandler , including TextBox , HtmlInputText , CheckBox , HtmlSelect , DropDownList , and so on. Because all the primitive HTML elements whose contents are propagated back within the body of a POST request already have control classes defined, it is unlikely that you will want to create your own control classes that implement IPostBackDataHandler very often. If you want to customize the behavior of one of these primitive control classes, it is often more convenient to create a new class that derives from the base class control implementation. The one other case where you may consider implementing this interface is for a composite control that contains several HTML controls in its rendering (such as a collection of INPUT controls). Controls that contain other controls, however, can usually be more conveniently implemented as composite controls, which are server controls that contain other server controls and which we discuss next.



Essential ASP.NET with Examples in Visual Basic .NET
Essential ASP.NET with Examples in Visual Basic .NET
ISBN: 0201760398
EAN: 2147483647
Year: 2003
Pages: 94
Authors: Fritz Onion

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net