State Management





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.

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.

-25 Name Control Rewritten to Use ViewState
public class NameControl : Control
{
  public NameControl()
  {
    ViewState["IsImportant"] = true;
    ViewState["Name"] = "";
  }

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

  public bool IsImportant
  {
    get { return (bool)ViewState["IsImportant"];  }
    set { ViewState["IsImportant"] = value; }
  }

  protected override void Render(HtmlTextWriter writer)
  {
    if (IsImportant)
      writer.RenderBeginTag(HtmlTextWriterTag.H1);
    else
      writer.RenderBeginTag(HtmlTextWriterTag.H2);

    writer.Write(Name);
    writer.RenderEndTag();
  }
}

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.

-26 Name Control Client Page Relying on Post-Back State Retention
<%@ Page Language='C#' %>
<%@ Register Namespace='EssentialAspDotNet.CustomControls'
             TagPrefix='eadn' Assembly='name' %>
<script runat='server'>
     protected void DoSubmit(object src, EventArgs e)
     { /* do something */     }
    protected void Page_Load(object src, EventArgs e)
    {
      if (!IsPostBack) // populate custom controls
      {
        m_nc1.Name = "Foo";
        m_nc1.IsImportant = true;
        m_nc2.Name = "Bar";
        m_nc2.IsImportant = false;
      }

      // if not IsPostBack, no need to repopulate
      // controls because their state will have been
      // retained
    }
</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. 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.

-27 BarGraph Control Performing Manual View State Management
public class BarGraphControl : Control
{
  private ArrayList _dataDescriptions;
  private ArrayList _dataValues;
  private double    _max = 0;

  protected override void LoadViewState(object savedState)
  {
    if (savedState != null)
    {
      // Load State from the array of objects that
       // was saved in SaveViewState
      object[] vState = (object[])savedState;
      if (vState[0] != null)
        base.LoadViewState(vState[0]);
      if (vState[1] != null)
        _dataDescriptions = (ArrayList)vState[1];
      if (vState[2] != null)
        _dataValues = (ArrayList)vState[2];
      if (vState[3] != null)
        _max = (double)vState[3];
    }
  }

  protected override object SaveViewState()
  {
    object[] vState = new object[4];
    vState[0] = base.SaveViewState();
    vState[1] = _dataDescriptions;
    vState[2] = _dataValues;
    vState[3] = _max;

    return vState;
  }

  public BarGraphControl()
  {
    _dataDescriptions = new ArrayList();
    _dataValues = new ArrayList();
  }

  public void AddValue(string name, double val)
  {
    _dataDescriptions.Add(name);
    _dataValues.Add(val);
    if (val > _max)
      _max = val;
  }

  protected override void Render(HtmlTextWriter output)
  {
    output.RenderBeginTag(HtmlTextWriterTag.Table);
    foreach (object elem in _dataValues)
    {
     // rendering details omitted (see sample)
    }
    output.RenderEndTag(); //</table>
  }
}
BarGraph Control Rendering

graphics/08fig04.gif

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.

-28 IPostBackDataHandler Interface Definition
public interface IPostBackDataHandler
{
  bool LoadPostData(string postDataKey,
                    NameValueCollection postCollection);
  void RaisePostDataChangedEvent();
}

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.

-29 A Control That Performs Explicit Post-Back Data Handling
public class CustomTextBox: Control, IPostBackDataHandler
{
  public string Text
  {
     get { return (string) ViewState["Text"]; }
     set { ViewState["Text"] = value;         }
  }

  public event EventHandler TextChanged;

  public virtual bool LoadPostData(string postDataKey,
                      NameValueCollection postCollection)
  {
     string presentValue = Text;
     string postedValue = postCollection[postDataKey];

     if (presentValue == null ||
         !presentValue.Equals(postedValue))
     {
       Text = postedValue;
       return true;
     }

     return false;
  }

  public virtual void RaisePostDataChangedEvent()
  {
     if (TextChanged != null)
       TextChanged(this,e);
  }

  protected override void Render(HtmlTextWriter output)
  {
     output.AddAttribute(HtmlTextWriterAttribute.Name,
                         UniqueID);
     output.AddAttribute(HtmlTextWriterAttribute.Value,
                         Text);
     output.RenderBeginTag(HtmlTextWriterTag.Input);
     output.RenderEndTag();
  }
}

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.


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows