Data Source Controls (Part 2 - Parameters)

The next installment in the data source control series shows how you can add support for declarative master/detail scenarios...

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 happen 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. Whidbey provides things like QueryStringParameter to pull data from a query string argument into the data source. Another especially useful parameter is ControlParameter that 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, notify the owner data source of changes, which in turn raises data source change notifications, that 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();
        }
    }
}

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 code above 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 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 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 lookup 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.

Posted on Wednesday, 7/6/2005 @ 12:16 AM | #ASP.NET


Comments

12 comments have been posted.

Erik Wynne Stepp

Posted on 7/9/2005 @ 5:52 AM
This code is pretty simple, yet there's still a lot of code to wire up Parameters into the DataSourceControl. At first glance, this seems like something that I might want to enable in nearly all DataSourceControls in order to provide the caller flexibility in how they set the parameters. Is there a reason that Parameters isn't already built into the base DataSourceControl, so that it is there and ready to be used if needed?

Of course, we can always implement our own DataSourceControlWithParameters class that does just that, but this seems to be something that would be of universal value and should be a part of the CLR.

Nikhil Kothari

Posted on 7/9/2005 @ 5:08 PM
Yes, it would be nice, and thats a good observation as well, but it really works in the most basic scenarios only (like WeatherDataSource). In the wide range of possible implementations (which the base class must cater to), a Parameters collection in the base class would break down for various reasons:

- Some data sources will have multiple views, implying possibly multiple parameter collections. So you'd want to qualify the property name appropriately, rather than having a property called "Parameters".
- Most data sources will support insert, update, delete as well, and these typically will have their own parameters. Again you don't want an unqualified property name called "Parameters".
- Some data source implementations put the parameters on the DataSourceView object (the Whidbey ones do), which is especially useful if you plan on adding multiple views later.
- Some data sources may not need parameters at all - eg. a hypothetical ProfileDataSource which exposes Profile data of the current user as data.

Marc C. Brooks

Posted on 8/10/2005 @ 12:15 PM
Why did you add a GetSelectedZipCode method instead of simply modifying the ZipCode property getter?

Nikhil Kothari

Posted on 8/17/2005 @ 4:31 AM
The reason I added GetSelectedZipCode and not change the ZipCode getter is because there is a general philosophy and design guideline that the getter should not return a different value (other than situations like returning String.Empty, even if the value was null). Consider this:

Object1.Prop = Object2.Prop = "abc";

If the getter modified the value, Object1.Prop will not be assigned "abc".

Jason S.

Posted on 11/10/2005 @ 7:20 AM
Excellent series of articles. Any reason why you didn't use ControlState instead of ViewState?

Nikhil Kothari

Posted on 11/12/2005 @ 11:38 AM
ControlState is meant for essential data. Whether property values for parameters in a parameter collection should be controllable by page developers as to whether they are tracked or not, and hence in ViewState. Note that ControlState cannot be toggled off.

Jason S.

Posted on 11/14/2005 @ 11:25 AM
Fair 'nuff. Thanks :)

Doug K.

Posted on 1/18/2006 @ 1:56 PM
I'm not able to access the parameters collection from the containing web page using code like the following:

<ctl:CustomDataSource id="dataSource" runat="server">
<Parameters>
<asp:QueryStringParameter name="ID" QueryStringName="ID" />
</Parameters>
</ctl:CustomDataSource>

The exception states that CustomDataSource does not allow child controls. The control is declared in CustomDataSource like this:

[PersistenceMode(PersistenceMode.InnerProperty)]
public ParameterCollection Parameters {
get {...}
}

I noticed in your code that the Parameters property is private and is not marked with any attributes, but declaring the property didn't work for me that way either.

What's the trick to getting the aspx page to allow me to specify parameters declaratively and, while I'm asking, getting the intellisense to give me a list of classes of the appropriate type?

Thanks.

Hongzhi Zhang

Posted on 2/21/2006 @ 6:02 AM
The Parameters property must be public, and you must define ParseChildren attribute for WeatherDataSource so that the parser to interpret the elements that are contained within the server control's tags as properties instead as child controls.

[ParseChildren(true, "Parameters")]
public class WeatherDataSource : DataSourceControl

Vlad Horby

Posted on 3/14/2006 @ 9:01 AM
I'm trying to create a ParameterCollection property and I want to be able to edit it using the ParameterCollectionEditor.
Using this article and the comments I was able to make it work, but if I edit it in the designer, the <Parameters></Parameters> tag disappears and i lose any parameters it contained.
This is the way i declare the property:
[Editor(typeof(System.Web.UI.Design.WebControls.ParameterCollectionEditor), typeof(UITypeEditor)),
PersistenceMode(PersistenceMode.InnerProperty)]
public ParameterCollection Parameters
{
get...
}
If i have any parameters already defined, it loads them in the editor, but something goes wrong when it saves them.

Any ideas?

Thanks
Vlad

Vlad Horby

Posted on 3/14/2006 @ 5:17 PM
Found it :)
You need this attributes for the CustomDataSource class:

[System.Web.UI.PersistChildrenAttribute(false)]
[System.Web.UI.ParseChildrenAttribute(true)]

Vlad

Andy Lee

Posted on 3/24/2006 @ 3:57 AM
When making a Custome control, the "ControlValueAttribute", shouldn't that be
'ControlValueProperty'.

It took me a while to work that out! Hope it helps someone else
The discussion on this post has been closed. Please use my contact form to provide comments.