Data Source Controls (Part 1 - The Basics)

The first in a series of posts on authoring data source controls...

Data source controls are a new type of server control introduced in Whidbey, 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 post, and over the course of a few subsequent posts in this mini-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 posts to tabular data sources. Depending on the response I get, I might take up the topic of hierarchical data sources as well.

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 (i.e. SELECT) a set of rows. Optionally it may provide the ability to modify data (i.e. 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. Eventually, I'll try to cover this in the future if it is a topic of interest... (let me know).

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 posts, 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 and 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. (this approach has been pretty useful as part of a workflow framework I am prototyping for v-next)

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 etc. and throws NotSupportedException from Update and related methods. All that I needed to do here in WeatherDataSourceView is override the abstract ExecuteSelect method, and return an IEnumerable that contains the weather data that was "selected." In my implementation, I use a helper WeatherService class that simply uses 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 couple posts on 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 post, I'll enhance the data source control with the ability to automatically handle changes in filter values (i.e. zip code) used to query data. By all means, send in your comments, if you'd like to drill further into a specific topic.

Posted on Thursday, 6/30/2005 @ 12:00 AM | #ASP.NET


Comments

15 comments have been posted.

David Taylor

Posted on 7/1/2005 @ 5:18 AM
Lovely Nikhil, I cannot wait until most Microsoft and 3rd party product ship with out of the box DataSourceControls that are much more specific to the product than doing the mapping via the ObjectDataSource.

--> return (s != null) ? s : String.Empty;
Didn't you mean:
--> return s ?? String.Empty;
...Just joking, I know you are trying not to confuse people until they get used to the new C# stuff.

Also, looking at: String.Compare(value, ZipCode, StringComparison.Ordinal) != 0)

Wasn't like so much simpler when we just wrote code that worked in our own culture and ignored the subtle bugs it introduced in other cultures ;-)

Makes you conder if ordinal comparisons should have been the default in V1...

Have a good weekend.

Nikhil Kothari

Posted on 7/1/2005 @ 7:40 AM
I have always thought default culture-sensistive string comparison was a bad idea - most of my code would use CultureInfo.InvariantCulture. I think one should put thought into which comparisons are subject to culture, and explicitly elect to factor in the user's culture.
In Whidbey, StringComparison.Ordinal, and OrdinalIgnoreCase were introduced (InvariantCulture may introduce some subtle bugs as well depending on the scenario), and I think that would have been the perfect default - basically the strcmp behavior from C/C++ and one should always choose, explicitly, to have comparison do more behind the scenes.

James Hancock

Posted on 7/1/2005 @ 6:35 PM
Thank you so much for starting this series!

Here's a HUGE vote for covering the Update/Insert/Delete logic, I've been messing around with ObjectDataSource and really been frustrated by it's use because there is ZERO documentation on how to use Update/Insert/Delete and ObjectDataSource doesn't let you pass an already created ObjectBindingList or other list directly to it, you have to use a function, which seems counterintuitive to me.

We've created a business objects dynamic list control that only returns the relivent information from the database instead of everything in the business object so that display is fast. But I'm having a heck of a time persisting a collection of columns (just an ordinary class) to the asp.net page and having it work in the designer view (works fine in code view and at runtime, but cacks in Design view failing to load the columns).

This data model has the possibilities to be really powerful, but right now, the documentation makes it not easy to use, and it's very counter intuitive to the old DataSource stuff, so it's frustrating for people that grew up on DataSource/DataMember. This series will really help!

David Taylor

Posted on 7/1/2005 @ 8:00 PM
The more documentation about creating DataSourceControls the better we will all be!

About 6-7 months ago I wrote a nice DataSourceControl for Beta1 to expose a sharepoint List. I included a UI designer that let you browse the tree structure of sharepoint and click on the list you wanted to use. I cheated a little, in that I inherited from ObjectDataSource. But I did get it working to the point of letting you select a list and then do one way binding (did not do the update stuff)....It was rewarding to see it all working...But then I got busy at work and did not get time to finish it.

I can really see a benefit of the more senior backend developers writing DataSourceControls, and then the web developers just use them and focus on the presentation. Should allow a great separation of responsibilities!

David Taylor

yaip

Posted on 7/2/2005 @ 5:22 PM
I am a bit confused. I have been using the combination of ObjectDataSource, DataSet (DataComponent in Beta 1) which lets me keep all my SPs together and GridView to develop a page. Am I doing something wrong? Am I missing something here?

Nikhil Kothari

Posted on 7/3/2005 @ 7:03 PM
Responding to things brought up James... first on the design-time experience - the designer associated with data source control has the ability to offer up data and schema at design-time. This data is used for data-binding and filling column dropdowns etc. Secondly, your question about update/insert/delete not working with list, brings up an interesting point about the basic architecture that I didn't discuss so here it is.

The data source control architecture is optimized for web scenarios, i.e. select returns a set of rows, while insert/update/delete work with a single row at a time (unlike the client scenarios which are optimized for the disconnected and dataset-based multiple operations model). For update/delete the row to be acted upon needs to be identified via (in SQL terms) the "where" clause, and the corresponding parameter values. Opening up the OM for data source view, you'll see only Select deals with IEnumerable. The other methods deal with IOrderedDictionary which implies name/value pairs for single row operations.

Yes, I realize single row is limiting some times. A data-bound control that allows multiple row edits, can call individual methods multiple times. We do have some ideas of how we can extend the data source interface over time to handle the multiple row operations more efficiently in a future release.

I wasn't planning on covering design-time in this series, but sounds like it would be useful, so I might add that bit to my plan. Also, I wasn't planning on showing insert/update/delete, and its kind of hard to work it into the weather scenario I have going here. I'll re-evaluate posting about it as a separate series, but it might take some time.

Yaip: You should be able to do what you are doing... the data components generated from the .xsd files have method signatures optimized for Web scenarios as well (i.e. the methods that work with a single row).

James Hancock

Posted on 7/3/2005 @ 9:17 PM
Nikhil: Thanks for the response.

The design-time experience is fine for me for the most part, although I would argue that the ObjectDataSource should just allow me to assign the IListSource to it instead of having to call the function, but I can get around that by wrapping ObjectDataSource and having a method that sets it now that I think about it.

Your comments about Delete/Insert/Modify on single items is something that I figured out the hard way, because again, there is no documentation on it at all. (the documentation is light, and the sample code that it refers to isn't there, online or offline help) That would be fine, if I could figure out how to get it to pass the ID from the column and all of the values in some sort of way that I could understand and have it automatically update the object that it's attached to, but it doesn't seem to work that way.

Hence my huge request for this all to be explained because it has been dropped out there and no one has told us how to use it :)

And if anyone has some tips for Collections of a class in an asp.net Control (Based on the dataobject stuff) and getting it to work, I would be eternally greatful. I've messed around with everything that I could find in the forums.asp.net and got it to the point it is now, but design mode (as opposed to code mode in the designer) still dies.

yaip

Posted on 7/4/2005 @ 9:55 AM
Following are the typical steps I take to build a web page with a GridView. I am assuming here that a table is alrady created in my SQL Server datastore.

- Create SP.
- In VS, Create a DataSet (DataComponent in B1) and configure it to add the SP that I have created.
- Create a web page.
- Drag an ObjectDataSource on its design surface.
- Configure it to use the newly created TableAdapter (in DataSet). Only use the SELECT method.
- Drag a GridView control on my design surface.
- Bind it to the ObjectDataSource and configure it to fine tune it.
- Use code-behind to use CREATE/UPDATE/DELETE in appropriate event procedures.

How does DataSourceView fit in this scenario?

Bradley Millington

Posted on 7/5/2005 @ 12:33 PM
Yaip: Your steps above look just fine, except you don't really need to use code-behind to handle UPDATE, INSERT, DELETE. You should be able to associate the Update, Insert, and Delete methods of a TableAdapter class in your DataSet to the ObjectDataSource, provided you have associated Update, Insert, and Delete stored procs to those operations. You can either define your own sprocs or let the designer create them for you. For example, I created these sprocs:

CREATE PROCEDURE GetContacts
AS
select [ID], [Name] from [Contacts]
RETURN

CREATE PROCEDURE UpdateContactName (@name varchar(50), @original_id int)
AS
Update [Contacts] SET [Name] = @Name where [ID] = @original_ID
RETURN

CREATE PROCEDURE DeleteContact (@original_ID int)
AS
DELETE FROM [Contacts] WHERE [ID] = @original_ID
RETURN

CREATE PROCEDURE InsertContact (@name varchar(50))
AS
INSERT INTO [Contacts] ([Name]) VALUES (@Name)
RETURN

Once I associate these to a TableAdapter in my DataSet, I can wire them up to ObjectDataSource like so:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
TypeName="DataSetTableAdapters.GetContactsTableAdapter" SelectMethod="GetContacts"
UpdateMethod="Update" DeleteMethod="Delete" InsertMethod="Insert"
OldValuesParameterFormatString="original_{0}">
<UpdateParameters>
<asp:Parameter Name="name" Type="String" />
<asp:Parameter Name="original_id" Type="Int32" />
</UpdateParameters>
<DeleteParameters>
<asp:Parameter Name="original_id" Type="Int32" />
</DeleteParameters>
<InsertParameters>
<asp:Parameter Name="name" Type="String" />
</InsertParameters>
</asp:ObjectDataSource>

Then the GridView/DetailsView/FormView can automatically perform these operations without the need to write any page code.

Hope this helps,
Bradley

Nikhil Kothari

Posted on 7/5/2005 @ 12:38 PM
Thanks Brad for posting the answer. Fyi, Bradley Millington is the PM for all the data-related features on the ASP.NET team.
This is a nice lead to my next post (http://www.nikhilk.net/DataSourceControlParameters.aspx) will introduce parameters, and how a data source can add support for them.

yaip

Posted on 7/7/2005 @ 7:06 PM
Thanks Bradley. BTW, Bradley was extremely helpful to me when I had connectivity problems when I moved from Beta 1 to Beta 2.

The reason I use code-behind is because it gives me more control (or so I feel??). I use FooterRow in the GridView to Add a record. Also, I use duplicate checks on alternate keys by using a Try..Catch (while Adding as well as Updating the record)

phoxnix

Posted on 8/22/2005 @ 8:17 PM
I think the sample code is hard to understand......

Nikhil Kothari

Posted on 8/24/2005 @ 9:59 PM
Knowing what was hard might help... it might also help clarify your questions via comments. That said, the sample code does make the assumption you know the basics about server control authoring.

EricBrown

Posted on 4/21/2006 @ 2:23 PM
>Note that none of the built-in data source controls in Whidbey currently offer multiple views.
>Eventually, I'll try to cover this in the future if it is a topic of interest... (let me know).

Have you as yet covered this? Can you direct me to good resources on how this is done?

Thanks,

Eric-

Richard Gates

Posted on 5/8/2006 @ 4:07 PM
I wonder why you have to used Stored Proc's to do the INSERT/UPDATE and DELETE. I've noticed that on a project I'm working on when I have multiple tables on in the Dataset Designer and modify the default query to have joins on the data in other tables the UPDATE, INSERT and DELETE functions are missing from the tabs in the Configure wizard for the ObjectDataSource. What I've read is that is because each ObjectDataSource is tied to one table. The problem with that is that most databases I've seen will have some sort of join in place so they can show data from another table in a single gridview. Maybe I'm missing the reason why the Datasets are there in the UI. I figured what I'm trying to do isn't that uncommon, but apparently it is.
The discussion on this post has been closed. Please use my contact form to provide comments.