Data source controls are a new type of server control introduced in Microsoft Visual Studio 2005. They are a key part of the data binding architecture and can provide a declarative programming model and automatic data binding behavior through data binding controls. This article and subsequent articles in this series will introduce the core elements of implementing data source controls.
Introduction
In short, a data source control summarizes a data store and some operations that can be performed on the contained data. The DataBound control is associated with a data source control through its DataSourceID property. Most traditional data stores are either tabular or hierarchical, and data source controls are divided accordingly. What we want to introduce here is the data source control in tabular format.
The data source control itself doesn't do much; all logic is encapsulated in DataSourceView-derived classes. At least one DataSourceView must implement the function of retrieving (that is, SELECTing) a set of rows. It can provide the function of modifying data (ie INSERT, UPDATE and DELETE) (optional). Data-bound controls can check enabled feature sets through various Can??? properties. The data source control itself is just a container for one or more uniquely named views. By convention, the default view can be accessed by its name, or it can be empty. Whether or what kind of relationship exists between different views can be appropriately defined based on the implementation of each data source control. For example, a data source control might provide different filtered views of the same data through different views, or it might provide a set of subrows in a secondary view. You can use the DataMember property of a data-bound control to select a particular view (if the data source control provides multiple views). Please note that none of the built-in data source controls in Whidbey currently offer multiple views.
Finally, let me introduce some content. Data source controls (and their views) implement two sets of APIs. The first set of APIs is an abstract interface defined for four common data operations that can be used in a regular way from any data-bound control. The second group is optional, defined in terms of the domain or data store it represents, is usually strongly typed, and is intended for application developers.
Example
In these articles, you will implement a WeatherDataSource that will work against the REST (English) XML API provided by weather.com (English) to retrieve weather information based on zip code. Derived data source controls are usually implemented first.
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 a named view instance, and GetViewNames to return the set of available views.
Here choose Derive from DataSourceControl. One thing that is not easy to notice is that the data-bound control actually looks for the IDataSource interface, and the DataSource control implements the interface by implementing GetView and GetViewNames. The reason the interface is needed is to enable the data source control to be both tabular and hierarchical (if possible, in which case derive from the main model and implement another model as the interface). Secondly, it also allows other controls to be converted in various scenarios to double the capacity of the data source. Also note the public ZipCode property and the GetWeather method that returns a strongly typed Weather object. This API is suitable for page developers. Page developers don't need to think about DataSourceControl and DataSourceView.
The next step is to implement the data source view itself. This particular example only provides SELECT-level functionality (which is the minimum requirement and the only functionality that is useful 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);
}
}
By default, the DataSourceView class returns false from properties such as CanUpdate and throws NotSupportedException from Update and related methods. The only thing you need to do here in the WeatherDataSourceView is to override the abstract ExecuteSelect method and return an IEnumerable containing the "selected" weather data. In the implementation, a helper WeatherService class is used, which simply uses a WebRequest object to query weather.com (in English) using the selected zip code (nothing special about that).
You may have noticed that ExecuteSelect is marked as protected. What the data-bound control actually calls is the public (and sealed) Select method passed in the callback. The implementation of Select calls ExecuteSelect and calls the callback with the resulting IEnumerable instance. This pattern is very weird. There is a reason for this, which will be explained in subsequent articles in this series. Please wait...
Here is an example of this usage:
Zip Code: <asp:TextBox runat="server" id="zipCodeTextBox" />
<asp:Button runat="server" onclick="OnLookupButtonClick" Text="Look" />
<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>
This code sets the zip code in response to user input, which causes the data source to issue a change notification, causing the bound FormView control to perform data binding and change the display.
Now, the data access code is encapsulated in the data source control. Additionally, this model enables weather.com (in English) to publish a component that can also encapsulate details specific to its service. Hopefully it will work. In addition, the abstract data source interface allows the FormView to work only with weather data.
In the next article, you'll enhance the data source control to automatically handle changes to the filter value (that is, zip code) used to query the data.