Data Source Controls, Part 1: The Basics

 

Nikhil Kothari
Microsoft Corporation

November 2005

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

Summary: This is the first article in a series on authoring data source controls. In this article, Nikhil Kothari looks at the basics of data source controls, and the issues you must keep in mind when designing them. (5 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 are a new type of server control introduced in Microsoft Visual Studio 2005, and are a key part of the data-binding architecture, in the sense that they enable the declarative programming model and automatic data-binding behavior provided by data-bound controls. In this article, and over the course of a few subsequent articles in this series, I'll cover the core aspects of implementing a data source control.

Introduction

Simply said, data source controls abstract a data store, and the operations that can be performed on the contained data. DataBound controls are associated with a data source control via their DataSourceID property. Most conventional data stores are either tabular or hierarchical, and correspondingly data source controls come in two flavors. For now, I am going to scope my control to tabular data sources.

A data source control by itself does not do much; all of the logic is encapsulated in a DataSourceView-derived class. At minimum a DataSourceView must implement the functionality to retrieve (that is, SELECT) a set of rows. Optionally it may provide the ability to modify data (that is, INSERT, UPDATE, and DELETE). The data-bound control can inspect the set of enabled capabilities via various Can??? properties. The data source control itself is simply a container for one or more uniquely named views. By convention, the default view is accessible by its name, as well as null. The relationship between the different views or lack thereof can be defined as appropriate by each data source control implementation. For example, a data source control might offer different filtered views of the same data via different views, or it might offer a set of child rows in a secondary view. The DataMember property of a data-bound control can be used to select a particular view if the data source control offers multiple views. Note that none of the built-in data source controls in Whidbey currently offer multiple views.

One last bit of introduction. A data source control (along with its views) implements two sets of APIs. The first API is an abstract interface defined in terms of the four common data operations that is meant to be used in a generic manner from any data-bound control. The second is optional and is defined using terminology from the domain or data store it represents, is typically strongly typed, and is oriented toward the application developer.

The Sample

During the course of these articles, I'll be implementing a WeatherDataSource that works against the REST XML API provided by weather.com to retrieve weather information by zip code. I typically start by implementing the derived data source control.

public class WeatherDataSource : DataSourceControl {
    public static readonly string 
      CurrentConditionsViewName = "CurrentConditions";

    private WeatherDataSourceView _currentConditionsView;

    private WeatherDataSourceView CurrentConditionsView {
        get {
            if (_currentConditionsView == null) {
                _currentConditionsView =
                    new WeatherDataSourceView(this, 
                       CurrentConditionsViewName);
            }
            return _currentConditionsView;
        }
    }

    public string ZipCode {
        get {
            string s = (string)ViewState["ZipCode"];
            return (s != null) ? s : String.Empty;
        }
        set {
            if (String.Compare(value, ZipCode, 
                StringComparison.Ordinal) != 0) {
                ViewState["ZipCode"] = value;
                CurrentConditionsView.RaiseChangedEvent();
            }
        }
    }

    protected override DataSourceView GetView(string viewName) {
        if (String.IsNullOrEmpty(viewName) ||
            (String.Compare(viewName, CurrentConditionsViewName,
                            StringComparison.OrdinalIgnoreCase) == 0)) {
            return CurrentConditionsView;
        }
        throw new ArgumentOutOfRangeException("viewName");
    }

    protected override ICollection GetViewNames() {
        return new string[] { CurrentConditionsViewName };
    }

    public Weather GetWeather() {
        return CurrentConditionView.GetWeather();
    }
}

As you can see, the basic idea is to implement GetView to return an instance of the named view, and GetViewNames to return the set of available views.

I chose to derive from DataSourceControl here. A subtle point that is not immediately obvious is that in reality the data-bound control looks for the IDataSource interface, which DataSource control implements using your implementations of GetView and GetViewNames. The reason we have an interface is to allow a data source control to be both tabular and hierarchical if appropriate (in which case you derive from your primary model and implement the other as an interface). Secondly, it allows transforming other controls in various scenarios to double up as data sources.

Another thing to note is the public ZipCode property and the GetWeather method that returns a strongly-typed Weather object. This is the API that is geared toward the page developer. The page developer shouldn't have to think in terms of DataSourceControl and DataSourceView.

The next step is to implement the data source view itself. This particular sample only provides SELECT-level functionality (which is the bare minimum requirement, and the only thing that makes sense in this scenario).

private sealed class WeatherDataSourceView : DataSourceView {

    private WeatherDataSource _owner;

    public WeatherDataSourceView(WeatherDataSource owner, string viewName)
        : base(owner, viewName) {
        _owner = owner;
    }

    protected override IEnumerable ExecuteSelect( 
        DataSourceSelectArguments arguments) {
        arguments.RaiseUnsupportedCapabilitiesError(this);

        Weather weatherObject = GetWeather();
        return new Weather[] { weatherObject };
    }

    internal Weather GetWeather() {
        string zipCode = _owner.ZipCode;
        if (zipCode.Length == 0) {
            throw new InvalidOperationException();
        }

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

    internal void RaiseChangedEvent() {
        OnDataSourceViewChanged(EventArgs.Empty);
    }
}

The DataSourceView class by default returns false from properties such as CanUpdate and so on, and throws NotSupportedException from Update and related methods. All that I needed to do here in WeatherDataSourceView was override the abstract ExecuteSelect method, and return an IEnumerable that contains the weather data that was "selected." In my implementation, I used a helper WeatherService class that simply used a WebRequest object to query weather.com using the selected zip code (nothing magic in there).

You might notice that ExecuteSelect is marked as protected. The data-bound control actually calls the public (and sealed) Select method, passing in a callback. The implementation of Select calls ExecuteSelect, and invokes the callback with the resulting IEnumerable instance. This is a rather odd pattern. There is a reason behind it, and I'll get to it in a later article in this series. Stay tuned...

Here is an example of the usage:

Zip Code: <asp:TextBox runat="server" id="zipCodeTextBox" />
<asp:Button runat="server" onclick="OnLookupButtonClick" 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" ZipCode="98052" />

<script runat="server">
private void OnLookupButtonClick(object sender, EventArgs e) {
    weatherDS.ZipCode = zipCodeTextBox.Text.Trim();
}
</script>

The code sets the zip code in response to user input, which causes the data source to raise a change notification, which causes the bound FormView control to perform data-binding and update the display.

The data access code is now encapsulated in the data source control. Furthermore, this model could enable weather.com to publish a component that can in addition encapsulate the details specific to its service. Hopefully it will catch on. Furthermore, the abstract data source interface allows FormView to just work against weather data.

In the next article, I'll enhance the data source control with the ability to automatically handle changes in filter values (that is, zip codes) used to query data.

 

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.