Data Source Controls, Part 2: Parameters

 

Nikhil Kothari
Microsoft Corporation

November 2005

Applies to:
   Microsoft Visual Studio 2005
   Microsoft ASP.NET 2.0
   Data Source Controls

Summary: This is the second article in a series on authoring data source controls. In this article, Nikhil looks at adding support for adding parameters to the query feeding the control. (6 printed pages)

Click here to download the code sample for this article.

Contents

Introduction
The Sample

This article originally appeared on Nihkil's blog; you can join in on the discussion there.

Data source controls need parameter values to specify which data needs to be selected, or how and what data should be modified. Typically the page contains some UI that defines parameters that must be used as part of the select operation, while data-bound controls provide parameter values for insert, update, and delete operations. However, it is equally likely to have a mixture of the two occur in either case. In part 1, the data source control exposed a ZipCode property that could be set declaratively, or in code in response to a user action. Parameters were designed to accomplish this scenario in a declarative (and extensible) manner.

Introduction

The Parameter base class represents a generic parameter. Microsoft Visual Studio 2005 provides things like QueryStringParameter to pull data from a query string argument into the data source. Another especially useful parameter is ControlParameter, which allows pulling data from any control property. You can define your own parameter type if the built-in types do not satisfy your needs. Doing so will allow you to keep your pages free of glue code that is instead neatly packaged in the parameter implementation.

In addition to pulling values from different sources, parameters can track changes to values and notify the owner data source of changes, which in turn raises data source change notifications, and which eventually trigger data-binding in data-bound controls. This, in short, is the magic behind declarative master detail scenarios, when using ControlParameters.

The Sample

I'll now add parameter functionality to the WeatherDataSource as I build it further.

public class WeatherDataSource : DataSourceControl {

    public static readonly string ZipCodeParameterName = "ZipCode";
    ...

    private ParameterCollection _parameters;

    private ParameterCollection Parameters {
        get {
            if (_parameters == null) {
                _parameters = new ParameterCollection();
                _parameters.ParametersChanged 
                   += new EventHandler(this.OnParametersChanged);
                if (IsTrackingViewState) {
                    ((IStateManager)_parameters).TrackViewState();
                }
            }
            return _parameters;
        }
    }
    ...

    public string GetSelectedZipCode() {
         if (_parameters != null) {
            Parameter zipCodeParameter = 
               _parameters[ZipCodeParameterName];
            if (zipCodeParameter != null) {
                IOrderedDictionary parameterValues = 
                    _parameters.GetValues(Context, this);
                return (string)parameterValues[zipCodeParameter.Name];
            }
        }

        return ZipCode;
    }

    protected override void LoadViewState(object state) {
        object baseState = null;

        if (state != null) {
            Pair p = (Pair)state;
            baseState = p.First;

            if (p.Second != null) {
                ((IStateManager)Parameters).LoadViewState(p.Second);
            }
        }
        base.LoadViewState(baseState);
    }

    protected override void OnInit(EventArgs e) {
        Page.LoadComplete += new EventHandler(this.OnPageLoadComplete);
    }

    private void OnPageLoadComplete(object sender, EventArgs e) {
        if (_parameters != null) {
            _parameters.UpdateValues(Context, this);
        }
    }

    private void OnParametersChanged(object sender, EventArgs e) {
        CurrentConditionsView.RaiseChangedEvent();
    }

    protected override object SaveViewState() {
        object baseState = base.SaveViewState();
        object parameterState = null;

        if (_parameters != null) {
            parameterState = ((IStateManager)_parameters).SaveViewState();
        }

        if ((baseState != null) || (parameterState != null)) {
            return new Pair(baseState, parameterState);
        }
        return null;
    }

    protected override void TrackViewState() {
        base.TrackViewState();
        if (_parameters != null) {
            ((IStateManager)_parameters).TrackViewState();
        }
    }
}

Microsoft ASP.NET provides a ParameterCollection that you can use pretty much as-is. It includes both change tracking and state management functionality. You simply need to call on its API appropriately to incorporate these functionalities in addition to exposing the collection as a property off your control. The key points to note in the preceding code are:

  • The data source control exposes a property of type ParameterCollection to allow the developer to add a parameter representing the zip code value to be used. If a parameter has been set, it is used; otherwise, the ZipCode property value is used.
  • The control overrides state management related methods to pull in state management capability built into ParameterCollection.
  • The control uses the new LoadComplete event of the page lifecycle to update parameter values, which it registers for by overriding OnInit. The data source control also registers for the ParametersChanged event raised by the ParameterCollection if any parameters have changed values during initialization, postback processing, or page code (which has all happened by the time LoadComplete is raised). Like before, when the ZipCode property was set, a change notification is raised that indicates to the data-bound control that it will need to perform data-binding again (which then happens during PreRender).
  • The need to participate in the lifecycle is one of the reasons data sources are implemented as controls, albeit non-visual controls. Another reason is so that data-bound controls can use FindControl by using their DataSourceID property, and reap the benefits of INamingContainer-based hierarchical name scopes (which enables implementing nesting data scenarios by placing a data source control within a template, and having it be repeated per row). The fact that data sources are controls has been a point of debate and disagreement—hopefully this explains some of the reasoning behind this.

The DataSourceView now simply needs to call GetSelectedZipCode instead of directly using the ZipCode property. I've also changed the data source view code to return null if a ZipCode is not selected (rather than throw an exception), which causes the data-bound control to show its "empty" view. This is mostly a convention, though in retrospect, I think this should really be an integral aspect of data source control semantics.

private sealed class WeatherDataSourceView : DataSourceView {
    ...

    internal Weather GetWeather() {
        string zipCode = _owner.GetSelectedZipCode();
        if (zipCode.Length == 0) {
            return null;
        }

        WeatherService weatherService = new WeatherService(zipCode);
        return weatherService.GetWeather();
    }
}

That's pretty much it. Here is the updated usage sample, which is now declarative.

Zip Code: <asp:TextBox runat="server" id="zipCodeTextBox" />
<asp:Button runat="server" Text="Lookup" />
<hr />

<asp:FormView runat="server" DataSourceID="weatherDS">
  <ItemTemplate>
    <asp:Label runat="server"
      Text='<%# Eval("Temperature", 
           "The current temperature is {0}.") %>' />
  </ItemTemplate>
</asp:FormView>
<nk:WeatherDataSource runat="server" id="weatherDS">
  <Parameters>
    <asp:ControlParameter Name="ZipCode" ControlID="zipCodeTextBox" />
  </Parameters>
</nk:WeatherDataSource>

Notice that I didn't specify Text as the property to look up on the ControlParameter tag in markup. ControlParameter automatically figures out the default property to work against when one is not specified. It does so by inspecting the ControlValueAttribute on the class. TextBox defines Text as the property that contains its "control value." This concept is applicable to a number of controls besides the traditional input controls. For example, GridView exposes its SelectedDataKey as its "control value." This is a new thing that control developers should start thinking about, so as to enable better integration with ControlParameter.

In the next article, I'll add asynchronous data access to the control.

 

About the author

Nikhil Kothari is an architect on the Web Platform and Tools team at Microsoft (which delivers IIS, ASP.NET, and Visual Studio Web development tools). Specifically, he is responsible for the overall Web Forms (a.k.a. server controls, a.k.a. page framework) feature area. He's been working on the team since the early XSP and ASP+ days; prior to his current role, he led the development of the page framework and several of the controls you can find on the ASP.NET toolbox today.

© Microsoft Corporation. All rights reserved.