Declarative Data Binding


This section reviews the fundamentals of data binding in ASP.NET, and then introduces the new declarative model that comes with ASP.NET 2.0.

Data Binding

The core features of data binding remain the same in this release of ASP.NET. Controls that support data binding expose a property named DataSource and a method called DataBind(). When a page is loaded, the user of the control initializes the DataSource property to some collection of data, such as an array, a DataReader, or a DataSet. When the data source is ready to be read from, the user of the control calls the DataBind() method on the control, at which point the control reads in all the data from the data source, making a local copy of it. When the page is ultimately rendered, the control takes the cached data it retrieved from the data source and renders its contents into the response buffer in whatever format the control is built to provide. Figure 3-1 shows the data-binding process for a control.

Figure 3-1. Data-binding process


Several controls support data binding, including simple controls, such as the ListBox, and controls designed exclusively for data binding, such as the GridView and DetailsView. As an example of a common use of data binding, Listing 3-2 shows a page that contains a GridView which is data-bound to the "authors" table in the "pubs" database in SQL Server. This example uses a DataReader to retrieve the data, and it takes care to invoke the DataBind() method immediately after the data reader is prepared and before the connection is closed.

Listing 3-2. Imperative data binding

<%@ Page Language="C#" %> <%@ Import Namespace="System.Data.SqlClient" %> <script runat="server">   protected void Page_Load(object sender, EventArgs e)   {     if (!IsPostBack)     {      using (SqlConnection conn =        new SqlConnection("database=pubs;trusted_connection=yes"))      using (SqlCommand cmd =        new SqlCommand("SELECT au_id, au_fname, au_lname FROM authors",                       conn))      {        conn.Open();        SqlDataReader reader = cmd.ExecuteReader();        _authorsGridView.DataSource = reader;        _authorsGridView.DataBind();      }     }   } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>ImperativeDataBinding</title> </head> <body>     <form  runat="server">     <div>       <asp:GridView runat="server"  />     </div>     </form> </body> </html> 

Data Source Controls

While the core concepts of data binding remain unchanged in this release, the way in which you perform data binding has changed. Instead of programmatically populating the DataSource property of a control and invoking the DataBind() method, you typically create the association declaratively using one of the new data source controls. A data source control is an abstraction of the retrieval and propagation of data from and to a data repository (typically a SQL database), and it takes care of making the necessary ADO.NET calls to interact with the database. For example, we can rewrite the data-binding example shown in Listing 3-2 using the new SqlDataSource control to perform the data retrieval in exactly the same way, but without the need to actually write the data access code. Listing 3-3 shows a page that uses declarative data binding with the SqlDataSource control to populate a GridView with the list of authors from the pubs database, as we did earlier.

Listing 3-3. Declarative data binding with SqlDataSource

<asp:GridView  runat="server"                     DataSource /> <asp:SqlDataSource  runat="server"        DataSourceMode="DataReader"        ConnectionString="database=pubs;trusted_connection=yes"        SelectCommand="SELECT au_id, au_fname, au_lname FROM Authors" /> 

There are two key differences to the way declarative data binding works when contrasted with imperative data binding. First of all, the control to which the data is being bound must specify the DataSourceID property to declaratively associate itself with the data source control. Second, it is no longer necessary to write any code to perform the data retrieval, as the GridView knows when it needs the data and the SqlDataSource knows how to retrieve it; it happens during the course of the page execution. Note that the example in Listing 3-3 specified a DataSourceMode of DataReader to make the example technically identical to our earlier imperative example. You can see that the connection string and the SELECT query contain enough information for the SqlDataSource to perform the ADO.NET calls to retrieve the data for the GridView. Figure 3-2 shows the relationship between a data-bound control, a data source control, and the repository of data being used.

Figure 3-2. Relationship between a data-bound control, a data source control, and a database


Data Source Control Details

All data source controls implement the IDataSource interface shown in Listing 3-4, which has one important method: GetView(). This returns a class deriving from the abstract base class DataSourceView, shown in Listing 3-5, which provides the standard data source operations: Select, Insert, Update, and Delete. Although there is a decoupling between the data source and a particular "view" of that data through this interface, all of the data sources that ship with ASP.NET 2.0 provide only one view of the data. This view can always be retrieved by passing an empty string or null to the GetView method.

In addition, the Select method takes a reference to a DataSourceSelectArguments class, shown in Listing 3-6, which has the following public properties: MaximumRows, RetrieveTotalRowCount, SortExpression, StartRowIndex, and TotalRowCount. These properties can be populated prior to executing a Select to influence the results that are returned. In particular, the SortExpression provides sorting, and the MaximumRows, StartRowIndex, and TotalRowCount provide the ability to implement paging of the data returned from the data source. Before populating these properties on the DataSourceSelectArguments class, you can also query a particular data source for its capabilities. This is used by all of the data-bound controls to enable or disable features based on the capabilities of the data source to which they are bound. The query methods to ask about data source features include: CanDelete, CanInsert, CanPage, CanRetrieveTotalRowCount, CanSort, and CanUpdate.

A good example of these values changing is setting the data retrieval mode of a SqlDataSource from DataSet to DataReader. With a DataSet, there is implicit sorting through the SortExpression property exposed by the underlying DataSet. When you switch to DataReader mode, however, there is no implicit way to implement sorting, so the SqlDataSource will return False from the CanSort property. You will see this in the Visual Studio .NET designer: as you switch data source modes, an associated GridView will disable features that are no longer available through the data source.

Altogether, there are four data source controls provided in ASP.NET 2.0: SqlDataSource, ObjectDataSource, XmlDataSource, and the SiteMapDataSource. The first two are rectangular, returning collections of rows of data; the last two are hierarchical, returning trees of data connected with parent-child relationships. These last two implement IHierarchicalDataSource instead of IDataSource, which we discuss later in this chapter.

Listing 3-4. IDataSource interface

public interface IDataSource {       // Events       event EventHandler DataSourceChanged;       // Methods       DataSourceView GetView(string viewName);       ICollection GetViewNames(); } 

Listing 3-5. DataSourceView (abstract) class

public abstract class DataSourceView {   public event EventHandler DataSourceViewChanged;   static DataSourceView();   protected DataSourceView(IDataSource owner, string viewName);   public virtual void Select(DataSourceSelectArguments arguments,                              DataSourceViewSelectCallback callback);   public virtual void Insert(IDictionary values,                              DataSourceViewOperationCallback callback);   public virtual void Update(IDictionary keys, IDictionary values,                              IDictionary oldValues,                              DataSourceViewOperationCallback callback);   public virtual void Delete(IDictionary keys, IDictionary oldValues,                              DataSourceViewOperationCallback callback);   protected internal abstract IEnumerable ExecuteSelect(                                  DataSourceSelectArguments arguments);   protected virtual int ExecuteInsert(IDictionary values);   protected virtual int ExecuteUpdate(IDictionary keys,                   IDictionary values, IDictionary oldValues);   protected virtual int ExecuteDelete(IDictionary keys,                                       IDictionary oldValues);   private void OnDataSourceChangedInternal(object sender, EventArgs e);   protected virtual void OnDataSourceViewChanged(EventArgs e);   protected internal virtual void RaiseUnsupportedCapabilityError(                                     DataSourceCapabilities capability);   public virtual bool CanDelete { get; }   public virtual bool CanInsert { get; }   public virtual bool CanPage { get; }   public virtual bool CanRetrieveTotalRowCount { get; }   public virtual bool CanSort { get; }   public virtual bool CanUpdate { get; }   protected EventHandlerList Events { get; }   public string Name { get; }   //... } 

Listing 3-6. DataSourceSelectArguments class

public sealed class DataSourceSelectArguments {   public DataSourceSelectArguments();   public DataSourceSelectArguments(string sortExpression);   public DataSourceSelectArguments(int startRowIndex, int maximumRows);   public DataSourceSelectArguments(string sortExpression,                                    int startRowIndex, int maximumRows);   public void AddSupportedCapabilities(                                    DataSourceCapabilities capabilities);   public void RaiseUnsupportedCapabilitiesError(DataSourceView view);   public static DataSourceSelectArguments Empty { get; }   public int MaximumRows { get; set; }   public bool RetrieveTotalRowCount { get; set; }   public string SortExpression { get; set; }   public int StartRowIndex { get; set; }   public int TotalRowCount { get; set; }   //... } 

Now that you have an understanding of the interfaces and classes involved with data sources, it is not difficult to see how individual controls use data source controls. In fact, you can even use data source controls yourself programmatically to retrieve data from a data source. Listing 3-7 shows a simple SqlDataSource declaration with an associated SelectCommand that could be used to retrieve results from the movies table. To ask the data source to execute its query, you would call its Select method, which returns a generic collection of items in the form of an IEnumerable interface reference. For the SqlDataSource this collection will either be a list of DataRowView classes if it is in the default DataSet retrieval mode, or it will be a collection of DbDataRecord classes if it is in DataReader mode.

Listing 3-7. SqlDataSource declaration

<asp:SqlDataSource  runat="server"             ConnectionString=                "<%$ ConnectionStrings:moviereviewsConnectionString %>"             SelectCommand="SELECT * FROM movies" /> 

Listing 3-8 shows an example of programmatically interacting with a SqlDataSource on a page. Prior to executing the Select method, this example populates a DataSourceSelectArguments object with a sort expression, but only if the data source supports sorting. It also defines a method, ShowDSResults, that takes a generic IEnumerable collection as a parameter and determines whether it is a DataSet- or DataReader-based retrieval, and enumerates the collection, writing the first three columns in each row out to the response. This example, while not something you would probably do in practice, should give you a good idea of how data-bound controls interact with their associated data source controls.

Listing 3-8. Programmatically using a SqlDataSource

protected void ShowDSResults(IEnumerable e) {   IEnumerator idx = e.GetEnumerator();   while (idx.MoveNext())   {     if (idx.Current is DataRowView) // DataSet used     {       DataRowView drv = (DataRowView)idx.Current;       Response.Output.Write("{0} {1} {2}<br />",                              drv[0], drv[1], drv[2]);     }     else if (idx.Current is DbDataRecord) // IDataReader used     {       DbDataRecord dr = (DbDataRecord)idx.Current;       Response.Output.Write("{0} {1} {2}<br />", dr[0], dr[1], dr[2]);     }   } } protected void Page_Load(object sender, EventArgs e) {   DataSourceSelectArguments dssa = new DataSourceSelectArguments();   if (((IDataSource)_moviesDataSource).GetView("").CanSort)     dssa.SortExpression = "release_date DESC";   ShowDSResults(_moviesDataSource.Select(dssa)); } 

Tying Together Controls and Data Sources

The true power of declarative data sources becomes clear when you begin wiring a full-featured control like the GridView to a data source with all of its features enabled. For a true RAD design experience, try dragging a database table from the Server Explorer window of Visual Studio 2005 onto an ASP.NET page's design surface, as shown in Figure 3-3. This will generate a GridView and an associated SqlDataSource pair of controls to display the contents of that table. In addition, the data source is generated with a complete set of select, insert, update, and delete commands and parameters, and is set to the default DataSet mode, which means that all of the advanced features of the GridView control will be enabled. You can quickly turn on the advanced features like paging, sorting, editing, and deleting by using the GridView Tasks menu and selecting the appropriate checkboxes. Without any further work, you have a fully functional grid complete with editing, paging, sorting, and deleting in place. Listing 3-9 shows the generated controls as placed on your .aspx page, and Figure 3-4 shows the runtime rendering of the grid in action.

Figure 3-3. Designer support for declarative data sources and data-bound controls


Listing 3-9. GridView and SqlDataSource with all features enabled

<asp:GridView  runat="server" AllowPaging="True"               AllowSorting="True"               AutoGenerateColumns="False" DataKeyNames="movie_id"               DataSource               EmptyDataText="There are no data records to display.">   <Columns>     <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />     <asp:BoundField DataField="movie_id" HeaderText="movie_id"                     ReadOnly="True" SortExpression="movie_id"                     Visible="false" />     <asp:BoundField DataField="title" HeaderText="title"                     SortExpression="title" />     <asp:BoundField DataField="release_date" HeaderText="release_date"                     SortExpression="release_date" />   </Columns> </asp:GridView> <asp:SqlDataSource  runat="server" ConnectionString="<%$ ConnectionStrings:moviereviewsConnectionString %>"      DeleteCommand="DELETE FROM movies WHERE movie_id = @movie_id"      InsertCommand="INSERT INTO movies (title, release_date) VALUES (@title, @release_date)"      SelectCommand="SELECT movie_id, title, release_date FROM movies"      UpdateCommand="UPDATE movies SET title = @title, release_date = @release_date WHERE movie_id = @movie_id">      <InsertParameters>        <asp:Parameter Name="title" Type="String" />        <asp:Parameter Name="release_date" Type="DateTime" />      </InsertParameters>      <UpdateParameters>        <asp:Parameter Name="title" Type="String" />        <asp:Parameter Name="release_date" Type="DateTime" />        <asp:Parameter Name="movie_id" Type="Int32" />      </UpdateParameters>      <DeleteParameters>        <asp:Parameter Name="movie_id" Type="Int32" />      </DeleteParameters>  </asp:SqlDataSource> 

Figure 3-4. Default rendering of GridView with attached SqlDataSource with all options enabled


One thing to keep in mind with this example is that most of the advanced features of the SqlDataSourcespecifically paging, sorting, and filteringare provided internally by the DataSet class. If you switch the SqlDataSource mode to use a DataSourceMode of DataReader, these three features will no longer be available in the GridView. This also means that the database itself is not being used to perform the sorting, filtering, or paging, which for large result sets can be very inefficient. Paging in particular is something that you can optimize with little effort to reduce the overall data sent back from the database (we discuss alternatives to using DataSet paging by using the ObjectDataSource later in this chapter).

Data Source Controls, ViewState, and ControlState

The procedure of data binding when using declarative data sources happens in much the same way it does when you do the binding programmatically, except that the association of the data source and the call to DataBind are done implicitly by the control. Specifically, when a control is being constructed, it checks to see if it has an associated declarative data source (indicated by the DataSourceID being populated). If it does, it will implicitly perform the DataBind during the call to the virtual CreateChildControls() method of the control. As in ASP.NET 1.1, controls always keep a local cache of the data to bridge the gap between data binding and rendering. By default, this data will also be stored in ViewState. However, controls that perform data binding implicitly like this realize when ViewState is enabled, and will avoid requerying the data source on postback requests as we did in our earlier example by hand (when we checked for !IsPostBack before calling DataBind). If you disable ViewState on a control, it will retrieve the data again for each request from the data source.

Now this behavior is usually what you want, but you should be aware of it since it can lead to surprising behavior. For example, if you have a button or other postback generating control on a form alongside a GridView, and the effect of the button is to modify the underlying database (say it adds a new row to the table being displayed by the GridView), you won't see the changes made to the database when the page is rendered because the GridView is drawing its state from ViewState and not going back to the database. The solution, of course, is to disable ViewState on the GridView and force it to retrieve the data again with each request.

One of the most frustrating aspects of working with server-side controls in ASP.NET 1.x is the all-or-nothing mentality with respect to view state. Behavioral aspects of controls like pagination in the DataGrid or selection change notifications in text boxes require view state to be enabled to function properly. This was frustrating because controls like the DataGrid (and the new GridView as well) can generate huge amounts of view state, depending on how much data is bound to them. ASP.NET 2.0 addresses this problem by partitioning view state into two separate categories: view state and control state. Control state is another type of hidden state reserved exclusively for controls to maintain their core (behavioral) functionality, whereas view state is now only to contain state to maintain the control's contents (UI). Technically, control state is stored in the same hidden field as view state (it is just another leaf node at the end of the view state hierarchy), but if you disable view state on a particular control (or on an entire page), the control state is still propagated. This means that you can disable ViewState on a GridView control, and know that all of its behavioral elements, like editing, paging, and so on, will remain functional.

Declarative Data Sources Without Embedded SQL

When developers first see declarative data sources, one common reaction is that embedding SQL queries directly in markup on the .aspx page feels wrong. The .aspx page in general should be kept free of application logic, and the ease with which someone could accidentally alter a query seems to be asking for trouble. There are a couple of strategies to avoiding embedded SQL in your pages, including assigning commands programmatically, storing commands in your web.config file, using stored procedures instead of direct SQL, and using the ObjectDataSource with a data access layer. We will discuss the ObjectDataSource in detail later in this chapter.

To store the SQL commands associated with a declarative data source in web.config, you can add the commands as key-value pairs under the AppSettings element, and then use the resource evaluation syntax to extract the values in the data source declaration. Listing 3-10 shows a sample web.config file with four SQL commands for interacting with the movies table, and Listing 3-11 shows a sample SqlDataSource declaration that uses the resource evaluation syntax to extract the commands from web.config and assign them to the command objects of the data source control.

Listing 3-10. Storing commands in AppSettings of web.config

<configuration>   <appSettings>     <add key="MoviesDeleteCommand"       value="DELETE FROM [movies] WHERE [movie_id] = @movie_id" />     <add key="MoviesInsertCommand"       value="INSERT INTO [movies] ([title], [release_date]) VALUES (@title, @release_date)" />     <add key="MoviesSelectCommand"       value="SELECT [movie_id], [title], [release_date] FROM [movies]"/>     <add key="MoviesUpdateCommand"       value="UPDATE [movies] SET [title] = @title, [release_date] = @release_date WHERE [movie_id] = @movie_id" />   </appSettings>     ... </configuration> 

Listing 3-11. Extracting commands from AppSettings in a declarative data source control

<asp:SqlDataSource  runat="server"             ConnectionString=                 "<%$ ConnectionStrings:moviereviewsConnectionString %>"             DeleteCommand="<%$ AppSettings:MoviesDeleteCommand %>"             InsertCommand="<%$ AppSettings:MoviesInsertCommand %>"             SelectCommand="<%$ AppSettings:MoviesSelectCommand %>"             UpdateCommand="<%$ AppSettings:MoviesUpdateCommand %>">   <InsertParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />   </InsertParameters>   <UpdateParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />     <asp:Parameter Name="movie_id" Type="Int32" />   </UpdateParameters>   <DeleteParameters>     <asp:Parameter Name="movie_id" Type="Int32" />   </DeleteParameters> </asp:SqlDataSource> 

Using stored procedures with a declarative data source is not much different from using SQL statements directly. When you specify commands in a SqlDataSource, each command has a corresponding CommandType attribute which determines what type of command will be created under the covers to issue to the database. By default, this command type is set to Text, so if you are using stored procedures, you need to explicitly set it to StoredProcedure. Keeping all of your SQL logic embedded in stored procedures is a good way to remove embedded SQL in your pages, and this is common practice in many Web applications today. Listing 3-12 shows four simple stored procedures to manipulate our movies table, and Listing 3-13 shows an example of a SqlDataSource wired up to use these stored procedures.

Listing 3-12. Sample stored procedures for manipulating movies table

CREATE PROCEDURE GetMovies AS SELECT movie_id, title, release_date FROM movies GO CREATE PROCEDURE DeleteMovie @movie_id INT AS DELETE FROM [movies] WHERE [movie_id] = @movie_id GO CREATE PROCEDURE UpdateMovie     @movie_id INT, @title VARCHAR(64), @release_date DATETIME AS UPDATE [movies] SET [title] = @title, [release_date] = @release_date WHERE [movie_id] = @movie_id GO CREATE PROCEDURE InsertMovie @title VARCHAR(64), @release_date DATETIME AS INSERT INTO [movies] ([title], [release_date]) VALUES (@title, @release_date) Go 

Listing 3-13. Using stored procedures in a SqlDataSource

<asp:SqlDataSource  runat="server"      ConnectionString=                "<%$ ConnectionStrings:moviereviewsConnectionString %>"      DeleteCommand="DeleteMovie" DeleteCommandType="StoredProcedure"      InsertCommand="InsertMovie" InsertCommandType="StoredProcedure"      SelectCommand="GetMovies"   SelectCommandType="StoredProcedure"      UpdateCommand="UpdateMovie" UpdateCommandType="StoredProcedure" >   <InsertParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />   </InsertParameters>   <UpdateParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />     <asp:Parameter Name="movie_id" Type="Int32" />   </UpdateParameters>   <DeleteParameters>     <asp:Parameter Name="movie_id" Type="Int32" />   </DeleteParameters> </asp:SqlDataSource> 

Storing Connection Strings

It is always wise to avoid hard-coding connection strings, whether it's in your code or in a declarative data source control. In the past, developers have typically used the web.config file's appSettings element to store connection strings; this way, the process of changing the data store was as easy as modifying a single element in the configuration file. In ASP.NET 2.0, a new configuration section has been added explicitly for connection strings. In addition, to aid in the retrieval of connection strings from the configuration file, a new evaluation syntax has been introduced. Listings 3-14 and 3-15 show a sample configuration file and a corresponding data source control using the new declarative evaluation syntax to retrieve the connection string.

Listing 3-14. Storing connection strings in web.config

<configuration>   <appSettings/>   <connectionStrings>       <add name="pubs_dsn" connectionString="server=.;..."            providerName="System.Data.SqlClient"/>   </connectionStrings> </configuration> 

Listing 3-15. Referencing a connection string

<asp:SqlDataSource      ConnectionString="<%$ ConnectionStrings:pubs_dsn %>"      ... // or programmatically ConfigurationManager.ConnectionStrings["pubs_dsn"].ConnectionString; 

As you will see in Chapter 5, it is also possible to encrypt portions of your configuration file, so if you are storing credentials in your connection strings it is wise to use this feature.

Data Source Parameters

So far in our exploration of declarative data sources, we have issued a single select statement to retrieve data, without any option for altering that query. However, it can be necessary to change a query dynamically, often in response to user input. Declarative data sources support modifications to queries in the form of parameter collections. Each data source supports several collections of parameters; for the SqlDataSource these include UpdateParameters, DeleteParameters, InsertParameters, SelectParameters, and FilterParameters. Each collection of parameters must correspond to parameters in the underlying query or stored procedure associated with the command type.

Parameter values can be specified programmatically, either through two-way data binding of controls (more on this later), or through a predefined set of parameter sources that includes

  • The value of a property from another control on the same form

  • The value of a cookie

  • A form parameter

  • A query string parameter

  • Data stored in the profile associated with a user

  • Data stored in the session associated with a user

Each of these is a convenient way to populate parameters of a data source from alternate data sources in your application. For example, if you wanted to let the user specify a search string to constrain the list of movies displayed in a GridView, you could add a TextBox to the form and a button to issue a post-back, and then add a ControlParameter to the list of FilterParameters for the SqlDataSource control. Point the ControlParameter class to the identifier of the TextBox you added, and specify a filter expression that constrains the query to list only results whose title contains the text typed into the text box. Listing 3-16 shows this example in its entirety, and Figure 3-5 shows the sample running with a search for "star" being performed.

Figure 3-5. Search feature implemented with a filter parameter


Note that the FilterExpression is only available with the SqlDataSource when running in DataSet mode, and that it expects syntax identical to that required by the RowFilter property of the DataView class, which means that parameters are specified using string placeholder syntax and are populated in order of declaration. Also remember that this feature is implemented completely by the DataSet, so it will not be as efficient as a WHERE clause in a database query for larger result sets. Note that we could have used any of the parameter types for this filter; for example, if the user navigated to this page with a query string to specify a search, we would have used the QueryStringParameter instead of the ControlParameter.

Listing 3-16. Specifying a filter parameter for a SqlDataSource with input from a TextBox

Search: <asp:TextBox  runat="server" /> <asp:Button  runat="server" Text="Search" /><br /> <br /> <asp:GridView  runat="server"            AutoGenerateColumns="False" DataKeyNames="movie_id"            DataSource            EmptyDataText="There are no data records to display.">     <Columns>   <asp:BoundField DataField="title" HeaderText="Title" />   <asp:BoundField DataField="release_date" DataFormatString="{0:d}"                    HeaderText="Release date" HtmlEncode="False" />     </Columns> </asp:GridView> <asp:SqlDataSource  runat="server"        ConnectionString=            "<%$ ConnectionStrings:moviereviewsConnectionString %>" SelectCommand="SELECT [movie_id], [title], [release_date] FROM [movies]" FilterExpression="title LIKE '%{0}%'"> <FilterParameters>   <asp:ControlParameter Control                         PropertyName="Text" /> </FilterParameters> </asp:SqlDataSource> 

Another interesting application of parameters is to create a master-detail relationship between two data-bound controls. The GridView control supports the concept of selecting a row by enabling a CommandField with the ShowSelectButton attribute set to true. With this enabled, the GridView displays a hyperlink column whose default text is Select; when the client clicks this, that row is marked as selected. You can then set up a second data-bound control and data source to display details for the currently selected row by adding a parameter to the Select statement and populating the parameter using a ControlParameter set to the GridView's currently selected row (actually the value of the primary key for that row).

For example, we could create a GridView that listed all of the movies from our database, enable selection, and then create a DataList control to display all of the reviews associated with that movie. Listing 3-17 shows the control declarations to accomplish this, and Figure 3-6 shows the GridView and associated DataList displaying movies and the selected movie's associated reviews, respectively.

Listing 3-17. A master-detail relationship between a GridView and a DataList

<asp:GridView  runat="server" AllowPaging="True"         AllowSorting="True"         AutoGenerateColumns="False" DataKeyNames="movie_id"         DataSource         EmptyDataText="There are no data records to display.">   <Columns>     <asp:CommandField ShowSelectButton="True" />     <asp:BoundField DataField="movie_id" HeaderText="movie_id"          ReadOnly="True" SortExpression="movie_id" Visible="False" />     <asp:BoundField DataField="title" HeaderText="title"                     SortExpression="title" />     <asp:BoundField DataField="release_date" DataFormatString="{0:d}"             HeaderText="release_date"            HtmlEncode="False" SortExpression="release_date" />   </Columns> </asp:GridView> <asp:SqlDataSource  runat="server"      ConnectionString=            "<%$ ConnectionStrings:moviereviewsConnectionString %>"      SelectCommand=            "SELECT [movie_id], [title], [release_date] FROM [movies]" /><br /> <asp:DataList  runat="server" RepeatColumns="2"               RepeatDirection="Horizontal" DataKeyField="review_id"               DataSource>   <ItemTemplate>     summary: <asp:Label  runat="server"                      Text='<%# Eval("summary") %>' /><br />     rating: <asp:Label  runat="server"                      Text='<%# Eval("rating") %>' /><br />     review: <asp:Label  runat="server"                      Text='<%# Eval("review") %>' /><br />     &nbsp;&nbsp;--     <asp:Label  runat="server"                Text='<%# Eval("reviewer") %>' /><br /><br />   </ItemTemplate> </asp:DataList> <asp:SqlDataSource  runat="server"      ConnectionString=        "<%$ ConnectionStrings:moviereviewsConnectionString %>"      SelectCommand="SELECT review_id, movie_id, summary, rating, review, reviewer FROM reviews WHERE (movie_id = @movie_id)">   <SelectParameters>     <asp:ControlParameter Control Name="movie_id"                           PropertyName="SelectedValue" Type="Int32" />   </SelectParameters> </asp:SqlDataSource> 

Figure 3-6. Master-detail showing reviews associated with the selected film


New Data-Bound Controls

There are several new data-bound controls introduced with this release of ASP.NET 2.0, including most prominently the GridView, DetailsView, and FormView. As you have seen, the GridView is the new grid control, and it is intended to replace the DataGrid (although the DataGrid remains for backward compatibility). It is in general a simpler, more modularly constructed control, and it's easier to use and customize than the DataGrid was. You will notice several differences from the DataGrid when you first start working with the GridView, including the addition of several new column types, including ImageField and CheckBoxField. Most of the features of the GridView are shown throughout the examples in this chapter.

The DetailsView fills the hole of a one-page form display that was missing in ASP.NET 1.1. It supports the ability to show, edit, insert, or delete a single row at a time, and will automatically generate default controls for displaying and updating individual fields based on their type in the underlying data source. The DefaultMode property of the control lets you specify which of the three modes of operation it should initially display: Edit, Insert, or ReadOnly. You typically will include a CommandField that displays hyperlinks to let the user switch between the three modes as well (although you can also restrict the user to only using one or two modes if you like).

Like most data-bound controls, it also supports template fields so that you can customize the appearance of any field as much as needed. Each field has three core templates that are displayed when the control is in each of the possible display modes: ItemTemplate, EditItemTemplate, and InsertItemTemplate. If you find that you are writing a lot of template fields, however, you might instead consider using the FormView control, which we will discuss next. Listing 3-18 shows an example of the DetailsView control being bound to a SqlDataSource, and Figure 3-7 shows the resulting control rendered in each of its three available states of operation.

Listing 3-18. DetailsView example

<asp:DetailsView  runat="server"      AllowPaging="True" AutoGenerateRows="False"      DataKeyNames="movie_id" DataSource      Height="50px" Width="125px">   <Fields>     <asp:BoundField DataField="movie_id" HeaderText="movie_id"                     InsertVisible="False"                     ReadOnly="True" Visible="False" />     <asp:BoundField DataField="title" HeaderText="Title" />     <asp:BoundField DataField="release_date" DataFormatString="{0:d}"                     HeaderText="Release date" HtmlEncode="False" />     <asp:CommandField ShowDeleteButton="True" ShowEditButton="True"                       ShowInsertButton="True" />   </Fields> </asp:DetailsView> <asp:SqlDataSource  runat="server"      ConnectionString=          "<%$ ConnectionStrings:moviereviewsConnectionString %>"      DeleteCommand="DELETE FROM movies WHERE movie_id = @movie_id"      InsertCommand="INSERT INTO movies (title, release_date) VALUES (@title, @release_date)"      SelectCommand="SELECT movie_id, title, release_date FROM movies"      UpdateCommand="UPDATE movies SET title = @title, release_date = @release_date WHERE movie_id = @movie_id">   <DeleteParameters>     <asp:Parameter Name="movie_id" Type="Int32" />   </DeleteParameters>   <UpdateParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />     <asp:Parameter Name="movie_id" Type="Int32" />   </UpdateParameters>   <InsertParameters>     <asp:Parameter Name="title" Type="String" />     <asp:Parameter Name="release_date" Type="DateTime" />   </InsertParameters> </asp:SqlDataSource> 

Figure 3-7. Default rendering of DetailsView in ReadOnly, Edit, and Insert modes


The FormView is very much like the DetailsViewit renders one row at a time and provides the ability to display, edit, insert, and delete rows. The difference is that the FormView has no default rendering of fields, but rather relies on user-defined templates to render the data and the input controls. The choice to use a FormView over a DetailsView usually hinges on how much customization of the default rendering of the DetailsView you anticipate. One scenario that almost always mandates a FormView is if you want to add validation controls to your input controls when the client is updating or inserting a row. To do this in the DetailsView you would have to add a separate EditItemTemplate for each field, but in the FormView there is only one EditItemTemplate that you populate with all of the input controls and their corresponding validation controls. When working with the InsertItemTemplate and EditItemTemplates in the FormView, you will typically use the new Bind() expression to perform two-way data binding to a control's field. This expression is evaluated inside of a data-binding expression, and during updates and inserts it will pull the value from the control field with which it is associated. To retrieve the initial value to populate updateable fields, it will extract the value from the underlying row and set the control field with the valuehence the term two-way binding.

Listing 3-19 shows an example of a FormView control bound to the same _moviesDataSource as our earlier DetailsView example, complete with validation on all input elements embedded in the EditItemTemplate and InsertItemTemplate.

Listing 3-19. FormView example

<asp:FormView  runat="server"      AllowPaging="True" DataKeyNames="movie_id"      DataSource>   <EditItemTemplate>     title: <asp:TextBox  runat="server"                 Text='<%# Bind("title") %>' />     <asp:RequiredFieldValidator  runat="server"          ControlToValidate="_titleTextBox"          ErrorMessage="Please enter a title">**     </asp:RequiredFieldValidator><br />     release_date: <asp:TextBox  runat="server"                        Text='<%# Bind("release_date", "{0:d}") %>' />     <asp:RequiredFieldValidator  runat="server"          ControlToValidate="release_dateTextBox"          ErrorMessage="Please enter a release date">**     </asp:RequiredFieldValidator>     <asp:RangeValidator  runat="server"          ControlToValidate="release_dateTextBox"          ErrorMessage="Please enter a valid release date (1/1/2006)"          MaximumValue="1/1/1600" MinimumValue="1/1/1000" Type="Date">**     </asp:RangeValidator><br />     <asp:LinkButton  runat="server"          CausesValidation="True" CommandName="Update" Text="Update" />     <asp:LinkButton  runat="server"          CausesValidation="False" CommandName="Cancel" Text="Cancel" />   </EditItemTemplate>   <InsertItemTemplate>     title: <asp:TextBox  runat="server"                 Text='<%# Bind("title") %>' />     <asp:RequiredFieldValidator  runat="server"          ControlToValidate="_titleTextBox"          ErrorMessage="Please enter a title">**     </asp:RequiredFieldValidator><br />     release_date: <asp:TextBox  runat="server"                         Text='<%# Bind("release_date", "{0:d}") %>' />     <asp:RequiredFieldValidator  runat="server"          ControlToValidate="release_dateTextBox"          ErrorMessage="Please enter a release date">**     </asp:RequiredFieldValidator>     <asp:RangeValidator  runat="server"          ControlToValidate="release_dateTextBox"          ErrorMessage="Please enter a valid release date (1/1/2006)"          MaximumValue="1/1/2600" MinimumValue="1/1/1000" Type="Date">**     </asp:RangeValidator><br />     <asp:LinkButton  runat="server"          CausesValidation="True" CommandName="Insert" Text="Insert" />     <asp:LinkButton  runat="server"          CausesValidation="False" CommandName="Cancel" Text="Cancel" />   </InsertItemTemplate>   <ItemTemplate>     Title: <asp:Label  runat="server"                 Text='<%# Bind("title") %>' /><br />     Release date: <asp:Label  runat="server"                        Text='<%# Bind("release_date", "{0:d}") %>' />     <br />     <asp:LinkButton  runat="server"          CausesValidation="False" CommandName="Edit" Text="Edit" />     <asp:LinkButton  runat="server"          CausesValidation="False" CommandName="Delete" Text="Delete" />     <asp:LinkButton  runat="server"          CausesValidation="False" CommandName="New" Text="New" />   </ItemTemplate> </asp:FormView> 

Data-Binding Evaluation Syntax

The data-binding syntax used in templates in data-bound controls has been simplified and reduced to just Eval("colname") in this release. The reference to the old Container.DataItem local variable is now implicit. As in the old DataBinder.Eval method, the Eval method takes an optional second parameter, which is a string format for the expression.

The new two-way binding syntax, Bind("colname"), is used mostly by the DetailsView and the FormView in their respective EditItem and InsertItem templates as shown in the previous two examples. This provides a way to populate parameters for update and insertion through data binding as well, not just for selection.

Declarative Data-Binding Techniques

As developers shift from the imperative style of data binding to the declarative one, there is often some confusion about how to accomplish tasks equivalently in the new model. For example, how do you perform a nested data bind to populate a drop-down list with a grid? Or how do you modify a parameter programmatically if there is no corresponding control parameter type? This section looks specifically at how to deal with these two issues in the context of declarative data sources.

Nested Declarative Data Binding

One issue that comes up frequently with dealing with data-bound controls is the asymmetric nature of join queries and their corresponding update statements. These are among the most common types of queries used in Web applications, since you are often presenting data from a table that contains foreign-key references to other tables containing the complete name and description (or whatever the extra data is).

For example, consider the reviews table in our sample database, which contains a foreign key column reference to the movies table. To properly present the data from the reviews table, you might build a query with an inner join to retrieve the titles of the movies instead of displaying the movie_id value itself, which is most likely meaningless to the client.

SELECT review_id, m.movie_id, summary, rating, review, reviewer, m.title FROM reviews AS r INNER JOIN movies as m ON r.movie_id=m.movie_id 


However, if you are performing an update or an insert into the table, you need to specify the foreign key id fields directly:

UPDATE reviews SET summary = @summary, rating = @rating,        review = @review, reviewer = @reviewer, movie_id=@movie_id WHERE review_id=@review_id 


Listing 3-20 shows a sample SqlDataSource that encapsulates these two commands.

Listing 3-20. SqlDataSource performing a join query with update and insert commands

<asp:SqlDataSource  runat="server"      ConnectionString=         "<%$ ConnectionStrings:moviereviewsConnectionString %>"      SelectCommand="SELECT review_id, m.movie_id, summary, rating, review, reviewer, m.title FROM reviews AS r INNER JOIN movies as m ON r.movie_id=m.movie_id "      UpdateCommand="UPDATE reviews SET summary = @summary, rating = @rating, review = @review, reviewer = @reviewer, movie_id=@movie_id WHERE review_id=@review_id">   <UpdateParameters>     <asp:Parameter Name="summary" />     <asp:Parameter Name="rating" />     <asp:Parameter Name="review" />     <asp:Parameter Name="reviewer" />     <asp:Parameter Name="review_id" />     <asp:Parameter Name="movie_id" />   </UpdateParameters> </asp:SqlDataSource> 

If you attach a GridView to this data source, you typically will want to give the user a drop-down list in update mode so that she can select from the proper list of movie titles. This is where declarative data sources shine, because you can create a template column for the title column, and in its UpdateItemTemplates specify a DropDownList with an associated DataSourceID attribute pointing to another declarative data source prepared to retrieve all of the movies separately in a nested data bind. Futhermore, you can use a data-binding expression to set the selected element of the DropDownList to the currently selected value for that column in the current row.[2] Listing 3-21 shows an example of a GridView pointing to the DataSource shown in Listing 3-20 with a nested data bind when rendered in Update mode grabbing data from the independent movies DataSource control. Figure 3-8 shows the GridView in update mode when rendered to the client.

[2] Note that this type of lookup table is an ideal candidate for caching, which is also supported by declarative data sources, as you will see in Chapter 8.

Listing 3-21. Nested declarative data-binding example

<asp:GridView  runat="server" AllowPaging="True"         AutoGenerateColumns="False"         DataKeyNames="review_id" DataSource >   <Columns>     <asp:CommandField ShowEditButton="True" />     <asp:BoundField DataField="review_id" HeaderText="review_id"          Visible="False" ReadOnly="True" SortExpression="review_id" />     <asp:BoundField DataField="summary" HeaderText="summary"          SortExpression="summary" />     <asp:BoundField DataField="rating" HeaderText="rating"          SortExpression="rating" />     <asp:BoundField DataField="review" HeaderText="review"          SortExpression="review" />     <asp:BoundField DataField="reviewer" HeaderText="reviewer"          SortExpression="reviewer" />     <asp:TemplateField HeaderText="Title" SortExpression="Title">       <EditItemTemplate>         <asp:DropDownList runat="server"               DataSource DataTextField="title"              DataValueField="movie_id"              SelectedValue='<%# Bind("movie_id") %>' >         </asp:DropDownList>       </EditItemTemplate>       <ItemTemplate>         <%# Eval("title") %>       </ItemTemplate>     </asp:TemplateField>   </Columns> </asp:GridView> <asp:SqlDataSource  runat="server"     ConnectionString=        "<%$ ConnectionStrings:moviereviewsConnectionString %>"     SelectCommand="SELECT movie_id, title FROM movies ORDER BY title" /> 

Figure 3-8. Nested declarative data binding in action


Programmatic Parameter Population

Another common scenario is if you have a declarative data source for a control (let's use an example of a DetailsView with a SqlDataSource) and you want to implicitly populate one or more of the parameters when the user inserts a new item. An example of this might be a timestamp field that should be set to whatever time the new row was inserted, and not something populated by the user, or perhaps a user name that is implicitly filled in based on the credentials of the client. This could equally apply to parameter population for select, update, or delete queries also, but it tends to be most common with inserts.

As you have seen, there are several parameter types available for declarative data sources, including the ability to draw a value from a cookie, a form field, the user's profile data, a query string, and so on. There are occasions, however, where none of these fits the bill and you just need to populate the parameter yourself (like the timestamp case). One solution to this is to add a handler for the appropriate event on the data source control (the events of interest are Inserting, Selecting, Updating, and Deleting, which are all called before the actual SQL call is made). So to complete the example described earlier, imagine having a FormView through which we want our clients to insert new movie reviews into the system, and that we would like the reviewer field of the reviews table to be populated implicitly with the current user name of the authenticated client or "anonymous" if the client is not authenticated. Listings 3-22 and 3-23 show how you might accomplish this by adding a handler for the Inserting event of the SqlDataSource, retrieving the @reviewer parameter from the command's list of parameters, and populating the value with the user name or "Anonymous" depending on whether she is authenticated or not. Note that we are also using the nested declarative data-binding technique to provide a DropDownList control to select the movie.

Listing 3-22. FormView and associated data source for inserting new reviews with the reviewer field being programmatically populated

<h2>Add your own review</h2> <asp:FormView  runat="server"      DataSource DefaultMode="Insert" >   <InsertItemTemplate>     <asp:DropDownList runat="server"           DataSource DataTextField="title"          DataValueField="movie_id"          SelectedValue='<%# Bind("movie_id") %>' /> <br />     Summary: <asp:TextBox  runat="server"                           Text='<%# Bind("summary") %>' /> <br />     Rating: <asp:TextBox  runat="server"                          Text='<%# Bind("rating") %>' />   <br />     Review: <asp:TextBox  runat="server"                          TextMode="multiLine" Rows="5"                          Text='<%# Bind("review") %>' /> <br />     <asp:LinkButton  runat="server"                     CausesValidation="True"                     CommandName="Insert" Text="Insert" />   </InsertItemTemplate> </asp:FormView> <asp:SqlDataSource  runat="server"      ConnectionString=              "<%$ ConnectionStrings:moviereviewsConnectionString %>"      InsertCommand="INSERT INTO reviews(movie_id, summary, rating, review, reviewer) VALUES (@movie_id, @summary, @rating, @review, @reviewer)"      OnInserting="_reviewsDataSource_Inserting">   <InsertParameters>     <asp:Parameter Name="movie_id" />     <asp:Parameter Name="summary" />     <asp:Parameter Name="rating" />     <asp:Parameter Name="review" />     <asp:Parameter Name="reviewer" />   </InsertParameters> </asp:SqlDataSource> <asp:SqlDataSource  runat="server"      ConnectionString=        "<%$ ConnectionStrings:moviereviewsConnectionString %>"      SelectCommand=        "SELECT [movie_id], [title] FROM [movies] ORDER BY title" /> 

Listing 3-23. Event handler for programmatically populating the @reviewer parameter

protected void _reviewsDataSource_Inserting(object sender,                                SqlDataSourceCommandEventArgs e) {   e.Command.Parameters["@reviewer"].Value =       User.Identity.IsAuthenticated ? User.Identity.Name : "Anonymous"; } 

Hierarchical Data Binding

In addition to the rectangular results modeled by the IDataSource interface, this release of ASP.NET also introduces an interface that models hierarchical data through IHierarchicalDataSource. Like its sibling interface IDataSource, this interface exposes a single method to retrieve a "view" of the data, GetHierarchicalView. All of the hierarchical data source controls defined in this release provide only one "view" of the data, however, so the class of real interest is the HierarchicalDataSourceView class which is returned by GetHierarchicalView. This abstract class exposes a Select method that returns an IHierarchicalEnumerable interface result. IHierarchicalEnumerable in turn exposes a GetHierarchyData method which returns an IHierarchyData interface. IHierarchyData exposes the core methods GetChildren and GetParent, which model the parent-child relationships in a hierarchical data source. The complete set of interfaces and classes involved with hierarchical data sources is shown in Listing 3-24.

Listing 3-24. Hierarchical data source interfaces

public interface IHierarchicalDataSource {   event EventHandler DataSourceChanged;   HierarchicalDataSourceView GetHierarchicalView(string viewPath); } public abstract class HierarchicalDataSourceView {   protected HierarchicalDataSourceView();   public abstract IHierarchicalEnumerable Select(); } public interface IHierarchicalEnumerable : IEnumerable {   IHierarchyData GetHierarchyData(object enumeratedItem); } public interface IHierarchyData {   IHierarchicalEnumerable GetChildren();   IHierarchyData GetParent();   bool HasChildren { get; }   object Item { get; }   string Path { get; }   string Type { get; } } 

In Chapter 2 we encountered our first hierarchical data source in the form of the SiteMapDataSource, which exposed the navigational structure of a site defined in the web.sitemap file containing XML data. The other hierarchical data source available is the XmlDataSource, which can be bound to any arbitrary XML document. When you create an XmlDataSource control, you specify the XML document in the DataFile attribute. You can also optionally specify an initial XPath expression to be used to obtain the initial set of nodes from the document with the XPath property, as well as an XSL transform file through the TransformFile property.

What makes this data source even more compelling is the fact that two new data-binding expressions have been added that can be used when binding to a hierarchical data source: XPath and XPathSelect. The XPath data-binding evaluation syntax gives you the ability to declaratively add XPath expressions to be evaluated from the current XmlNode that is being enumerated in a control template, as it is bound to a hierarchical data source. The XPathSelect expression can be used to perform subselections, which can be useful to perform nested bindings, as you will see next.

Listing 3-25 shows an example of binding a DataList to an XmlDataSource, whose data is obtained by specifying an RSS feed as the DataFile. The XPath expression is used to extract a list of item elements from the document, which are then bound iteratively to the DataList control. In the DataList control, we are able to specify XPath expressions in an ItemTemplate to pluck out the XML elements we want to display for each item node found in the initial XPath query. Note that this example is using a nonhierarchical data-bound control (the DataList) with a hierarchical XmlDataSource. This is possible as long as you don't want to do recursive data binding on a node in the result set returned by the data source's initial XPath expression. For true hierarchical data binding, you can use the XmlDataSource with a hierarchical control like the TreeView control, which we will look at next.

Listing 3-25. A simple blog reader using XmlDataSource

<%@ Page Language="C#" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server" /> <body>   <form  runat="server">   <div>   <asp:XmlDataSource  Runat="server"        DataFile="http://pluralsight.com/blogs/fritz/rss.aspx"        XPath="/rss/channel/item" />       <asp:DataList  Runat="server"                     DataSource>         <ItemTemplate>           <b><%# XPath("pubDate")%> - <%# XPath("title") %></b>           <br />           <%# XPath("description") %>         </ItemTemplate>       </asp:DataList>   </div>   </form> </body> </html> 

Binding Hierarchical Data Sources to the TreeView Control

To take full advantage of a hierarchical data source like the XmlDataSource, it is necessary to bind to a control that can represent data hierarchically, like the TreeView control. In Chapter 2 you saw how to bind the TreeView to the SiteMapDataSource to display a tree structure depicting the navigational structure of your site. You can also bind an XmlDataSource to a TreeView control and have it implicitly traverse the XML document, creating nodes and leaves to match the structure of the document. When you are binding to an XmlDataSource, the TreeView also supports the concept of tree node bindings, which are a way of describing how you would like a particular element in the document to be bound to a node, including an associated image, and which attribute of an element (if any) to display for the node. Listing 3-26 shows a sample XML file we will use to bind a TreeView to. Listing 3-27 shows a TreeView bound to an XmlDataSource pointing to the sample XML file, and Figure 3-9 shows the resulting rendering.

Listing 3-26. Sample XML file with bookstore content

<Bookstore>   <genre name="Business">     <book ISBN="BU1032" Title="The Busy Executive's Database Guide"           Price="19.99">       <chapter num="1" name="Introduction">         Abstract...       </chapter>       <chapter num="2" name="Body">         Abstract...       </chapter>       <chapter num="3" name="Conclusion">         Abstract...       </chapter>     </book>     <book ISBN="BU2075" Title="You Can Combat Computer Stress!"           Price="2.99">       <chapter num="1" name="Introduction">         Abstract...       </chapter>       <chapter num="2" name="Body">         Abstract...       </chapter>       <chapter num="3" name="Conclusion">         Abstract...       </chapter>     </book>     <book ISBN="BU7832" Title="Straight Talk About Computers"           Price="19.99">       <chapter num="1" name="Introduction">         Abstract...       </chapter>       <chapter num="2" name="Body">         Abstract...       </chapter>       <chapter num="3" name="Conclusion">         Abstract...       </chapter>     </book>   </genre> </Bookstore> 

Listing 3-27. TreeNodeBinding with the TreeView and the XmlDataSource

<asp:TreeView  runat="server"               DataSource ShowLines="True">   <DataBindings>     <asp:TreeNodeBinding DataMember="book"          ImageUrl="~/img/closedbook.gif" TextField="Title" />     <asp:TreeNodeBinding DataMember="chapter"          ImageUrl="~/img/notepad.gif" TextField="name" />     <asp:TreeNodeBinding DataMember="genre"          ImageUrl="~/img/folder.gif" Text="." TextField="name" />    </DataBindings> </asp:TreeView> <asp:XmlDataSource  runat="server"                    DataFile="~/App_Data/Bookstore.xml" /> 

Figure 3-9. TreeView with TreeNodeBinding rendering


Nested Hierarchical Data Binding

There are occasions where you don't really want the full hierarchical rendering of a TreeView, but you also need to bind more than one level deep in a rectangular control like the DataList or Repeater. When binding to the XmlDataSource, you can accomplish this by performing a nested data bind using the XPathSelect data-binding expression and assigning the result to a nested control's DataSource property. For example, Listing 3-28 shows a sample DataList control bound to all of the book elements in the Bookstore.xml document, but in its ItemTemplate is another DataList declaration, whose DataSource attribute is set to the evaluation of XPathSelect("chapter") to extract all of the chapter elements for that particular book. The resulting (partial) rendering is shown in Figure 3-10.

Listing 3-28. Nested hierarchical data binding

<asp:DataList  runat="server"               DataSource>   <ItemTemplate>     Title: <%# Eval("Title") %><br />     <asp:DataList runat="server"           DataSource='<%# XPathSelect("chapter") %>'>       <ItemTemplate>         <h4>Chapternum: <%# XPath("@num") %></h4>         <h4>Chapter name: <%# XPath("@name") %></h4>         <%# XPath(".") %>         <br />       </ItemTemplate>     </asp:DataList>     <br />   </ItemTemplate> </asp:DataList> <asp:XmlDataSource  runat="server"                    XPath="/Bookstore/genre/book"                    DataFile="~/App_Data/Bookstore.xml" /> 

Figure 3-10. Rendering of nested hierarchical data binding


Binding to Objects

It is common practice in many applications that deal with data to access the data through a data access layer. Creating a level of indirection between a data source and the elements that consume the data makes for a much more flexible architecture, and opens opportunities in the future for changing details of the data storage without affecting the front end. In the examples of using declarative data sources presented so far in this chapter, we mostly used the SqlDataSource control, which requires that you specify either embedded SQL statements or stored procedures to interact with the database. To work with data access layers instead of direct SQL calls, you can use the ObjectDataSource control instead, with much the same effect.

Instead of a connection string, the ObjectDataSource control is initialized with a type name as the primary object to create, as well as method names for performing select, insert, update, and delete operations. At runtime, the ObjectDataSource will create an instance of the type (if the methods are nonstatic), and using reflection, will invoke the select, update, insert, and/or delete methods as needed in response to control interaction. It is possible to enable sorting, paging, and filtering on an object data source as well, by writing methods with parameters for specifying sort expressions, filter expressions, and methods for performing paging, as you will see shortly. Figure 3-11 shows the role of the ObjectDataSource control, and Listing 3-29 shows the core properties you will set when you work with it.

Figure 3-11. ObjectDataSource model


Listing 3-29. Core properties of the ObjectDataSource control

public class ObjectDataSource : DataSourceControl   {     public string TypeName                   { get; set; }     public string DataObjectTypeName         { get; set; }     public string DeleteMethod               { get; set; }     public string InsertMethod               { get; set; }     public string SelectCountMethod          { get; set; }     public string SelectMethod               { get; set; }     public string SortParameterName          { get; set; }     public string UpdateMethod               { get; set; }     public string MaximumRowsParameterName   { get; set; }     public string StartRowIndexParameterName { get; set; }     //... 

When you build a class to interact with the ObjectDataSource, there are a few restrictions that you should consider. First of all, if you create a class with nonstatic methods, the ObjectDataSource will create a new instance of your type for each method call made. It is therefore common to create types that expose only static methods to bind to the ObjectDataSource to avoid the overhead of creating and disposing of an object with each method call. This also means that the class you create should be stateless, as any instance of the class will be referred to only once per call. Finally, the method that corresponds to the Select method of your class must return a collection of items, which at the least implements the IEnumerable or is a DataSet derivative. Listing 3-30 shows a simple static class that returns a list of strings which we can use as a data source. Listing 3-31 shows a sample use of the ObjectDataSource specifying the SimpleDataSource class as the type name and binding the results to a BulletedList control. Figure 3-12 shows the rendered page.

Listing 3-30. Simple data source for use with ObjectDataSource

namespace EssentialAspDotNet2.DataBinding {   public static class SimpleDataSource   {     public static IEnumerable<string> GetItems()     {       for (int i = 0; i < 10; i++)         yield return "Item " + i.ToString();     }   } } 

Listing 3-31. Sample .aspx page using simple data source

<%@ Page Language="C#" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server" /> <body>   <form  runat="server"><div>     <asp:BulletedList  runat="server"                       DataSource />     <asp:ObjectDataSource  runat="server"          SelectMethod="GetItems"          TypeName="EssentialAspDotNet2.DataBinding.SimpleDataSource" />   </div></form> </body> </html> 

Figure 3-12. Simple data source bound to BulletedList with ObjectDataSource


Of course, most of the time the classes you build to interact with the ObjectDataSource will be doing much more than just building an array of strings in memory. As you begin to work with the ObjectDataSource, it is important to keep in mind that its sole purpose is to act as a bridge between data-bound controls and a set of classes providing data. It is not intended as a model for how data access layers should be designed, and in practice you may find that it makes most sense to create a set of "shim" classes that are compatible with the ObjectDataSource control, exposing the methods and parameters needed, but which under the covers invoke methods on classes in a more complete data access layer.

As an example of a slightly more complex data layer, consider the task of building a data access layer for the movies table in our moviereviews database. The first step will be to create an entity class to represent a row from the movies table, exposing property accessors for each of the column types. Listing 3-32 shows the implementation of our Movie entity class.

Listing 3-32. Movie entity class

namespace EssentialAspDotNet2.DataBinding {   public class Movie   {     int _movieId;     string _title;     DateTime _releaseDate;     public Movie() { }     public Movie(int movieId, string title, DateTime releaseDate)     {       _movieId     = movieId;       _title       = title;       _releaseDate = releaseDate;     }     public int MovieId     {       get { return _movieId; }       set { _movieId = value; }     }     public string Title     {       get { return _title; }       set { _title = value; }     }     public DateTime ReleaseDate     {       get { return _releaseDate; }       set { _releaseDate = value; }     }   } } 

Our next task is to build a class that implements the four core access methods to select, update, insert, and delete movies from the database, using instances of our Movie entity class to represent rows in the underlying table. How this class is implemented is completely up to youyou may be calling out to a Web service to retrieve data, or invoking methods in a secondary data access layer, or what have you. The key point is that this class must bridge the gap between the ObjectDataSource control to which it will be bound, and whatever back-end data you are trying to expose. For our example, we will implement the SQL calls directly using ADO.NET. Listing 3-33 shows the MovieReviewsData class with four methods to retrieve and modify movies in our database.

Listing 3-33. MovieReviewsData class

namespace EssentialAspDotNet2.DataBinding {   public static class MovieReviewsData   {     public static ICollection<Movie> GetMovies()     {       string dsn = ConfigurationManager.ConnectionStrings[                       "moviereviewsConnectionString"].ConnectionString;       string sql = "SELECT movie_id, title, release_date FROM movies";       List<Movie> ret = new List<Movie>();       using (SqlConnection conn = new SqlConnection(dsn))       using (SqlCommand cmd = new SqlCommand(sql, conn))       {         conn.Open();         SqlDataReader r = cmd.ExecuteReader();         while (r.Read())           ret.Add(new Movie(r.GetInt32(0), r.GetString(1),                             r.GetDateTime(2)));       }       return ret;     }     public static void UpdateMovie(Movie m)     {       string dsn = ConfigurationManager.ConnectionStrings[                       "moviereviewsConnectionString"].ConnectionString;       string sql = "UPDATE movies SET title=@title, " +                   "release_date=@release_date WHERE movie_id=@movie_id";       using (SqlConnection conn = new SqlConnection(dsn))       using (SqlCommand cmd = new SqlCommand(sql, conn))       {         cmd.Parameters.AddWithValue("@movie_id", m.MovieId);         cmd.Parameters.AddWithValue("@title", m.Title);         cmd.Parameters.AddWithValue("@release_date", m.ReleaseDate);         conn.Open();         cmd.ExecuteNonQuery();       }     }     public static void DeleteMovie(Movie m)     {       string dsn = ConfigurationManager.ConnectionStrings[                       "moviereviewsConnectionString"].ConnectionString;       string sql = "DELETE FROM movies WHERE movie_id=@movie_id";       using (SqlConnection conn = new SqlConnection(dsn))       using (SqlCommand cmd = new SqlCommand(sql, conn))       {         cmd.Parameters.AddWithValue("@movie_id", m.MovieId);         conn.Open();         cmd.ExecuteNonQuery();       }     }     public static void InsertMovie(Movie m)     {       string dsn = ConfigurationManager.ConnectionStrings[                       "moviereviewsConnectionString"].ConnectionString;       string sql = "INSERT INTO movies (title, release_date) " +                    "VALUES (@title, @release_date)";       using (SqlConnection conn = new SqlConnection(dsn))       using (SqlCommand cmd = new SqlCommand(sql, conn))       {         cmd.Parameters.AddWithValue("@title", m.Title);         cmd.Parameters.AddWithValue("@release_date", m.ReleaseDate);          conn.Open();          cmd.ExecuteNonQuery();       }     }   } } 

The last step is to actually wire this data class up to an ObjectDataSource control and bind a control (like a GridView) to that data source. If you are using a GridView, DetailsView, or FormView control to bind to an ObjectDataSource, one thing you will have to take care of by hand is to set the DataKeyNames property of the control to contain the name of the primary key property of your entity class (it could be multiple primary keys if needed, separated by commas). The values of properties in the DataKeyNames collection are cached in the control state of the control, and these values will be used to populate the entity class passed into your update and delete methods (the rest of the entity class will remain empty in the delete method, since you should only need the primary key value(s) to perform the deletion). In our case, we will set the DataKeyNames property in our DataGrid to MovieId. As for the ObjectDataSource itself, we need to specify the type name, the names of the four methods, and the name of the type used as the entity class through the DataObjectTypeName so that the control knows what type to reflect on to view the properties.

Listing 3-34 shows a sample use of the ObjectDataSource initialized with our MovieReviewsData class and bound to a GridView with full editing and deleting features enabled. This same data source could be bound to a DetailsView or FormView control as well to perform insertion in the same manner. The rendering of this page will look identical to the GridView paired with a SqlDataSource shown earlier in Figure 3-4.

Listing 3-34. Binding a GridView to an ObjectDataSource with a custom data access layer

<asp:GridView  runat="server" AllowPaging="True"               AutoGenerateColumns="False"               DataSource               DataKeyNames="MovieId">   <Columns>     <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />     <asp:BoundField DataField="ReleaseDate" DataFormatString="{0:d}"                     HeaderText="ReleaseDate"                     HtmlEncode="False" SortExpression="ReleaseDate" />     <asp:BoundField DataField="MovieId" HeaderText="MovieId"                     SortExpression="MovieId" Visible="False" />     <asp:BoundField DataField="Title" HeaderText="Title"                     SortExpression="Title" />   </Columns> </asp:GridView> <asp:ObjectDataSource  runat="server"             TypeName="EssentialAspDotNet2.DataBinding.MovieReviewsData"             DataObjectTypeName="EssentialAspDotNet2.DataBinding.Movie"             DeleteMethod="InsertMovie"             InsertMethod="InsertMovie"             SelectMethod="GetMovies"             UpdateMethod="UpdateMovie" /> 

You may have noticed that the previous example had enabled paging on the GridView by default, even though we made no effort to support paging in our data access layer. By default, the GridView is happy to provide paging for you by displaying a particular subset of rows from the underlying query, but this means that it will be retrieving all of the rows with each paginated display, even though it may only be showing a small subset. To implement paging more efficiently in your data layer, you need to build an override of your Select method that takes a maximum row count and a start row index pair of parameters. You also need to provide another method in your data class that returns the total count of items. Finally, in the ObjectDataSource declaration itself, you need to populate the MaximumRowsParameterName, SelectCountMethod, and StartRowIndexParameterName properties with the names of the parameters and method added to your class.

You must also set the EnablePaging property to true, which will cause the data source control to look for an overload of your Select method with the two parameter names and will use them to expose paging functionality to controls like the GridView. Listing 3-35 shows an updated version of our GetMovies method that uses the ROW_NUMBER function of SQL Server 2005 to implement paging over the movies table, as well as the new GetMoviesCount method. There are many ways to implement paging over in a query, so the details may well change depending on what features your database supports, but the concepts will be the same. Listing 3-36 shows the updated declaration of the ObjectDataSource control that will take advantage of our custom paging code. No changes are necessary to the GridView in this case.

Listing 3-35. Enhancements to the MovieReviewsData class to support custom paging

public static ICollection<Movie> GetMovies(                 int maxRows, int startRowIndex) {   List<Movie> ret = new List<Movie>();   string dsn = ConfigurationManager.ConnectionStrings[                   "moviereviewsConnectionString"].ConnectionString;   // This assumes SQL Server 2005 with its new ROW_NUMBER function   // to assist with pagination.   //   string sql = "SELECT movie_id, title, release_date FROM " +                "(SELECT ROW_NUMBER() OVER (ORDER BY title) As Row, " +                "movie_id, title, release_date FROM movies)" +                " As TempRowTable";   if (startRowIndex >= 0)   {     sql += " WHERE Row >= " + startRowIndex.ToString() + " AND " +            "Row <= " + (startRowIndex + maxRows).ToString();   }   using (SqlConnection conn = new SqlConnection(dsn))   using (SqlCommand cmd = new SqlCommand(sql, conn))   {     conn.Open();     SqlDataReader r = cmd.ExecuteReader();     while (r.Read())       ret.Add(new Movie(r.GetInt32(0), r.GetString(1),                         r.GetDateTime(2)));   }   return ret; } public static int GetMovieCount() {   int ret = -1;   string dsn = ConfigurationManager.ConnectionStrings[                   "moviereviewsConnectionString"].ConnectionString;   string sql = "SELECT COUNT(*) FROM movies";   using (SqlConnection conn = new SqlConnection(dsn))   using (SqlCommand cmd = new SqlCommand(sql, conn))   {     conn.Open();     ret = (int)cmd.ExecuteScalar();   }   return ret; } 

Listing 3-36. Updated ObjectDataSource declaration taking advantage of custom paging

<asp:ObjectDataSource  runat="server"      TypeName="EssentialAspDotNet2.DataBinding.MovieReviewsData"      DataObjectTypeName="EssentialAspDotNet2.DataBinding.Movie"      DeleteMethod="InsertMovie"      InsertMethod="InsertMovie"      SelectMethod="GetMovies"      UpdateMethod="UpdateMovie"      EnablePaging="true" MaximumRowsParameterName="maxRows"      SelectCountMethod="GetMovieCount"      StartRowIndexParameterName="startRowIndex" /> 

You can similarly implement sorting in your Select method by providing a SortExpression parameter which you then specify in the ObjectDataSource declaration using the SortParameterName property. This will make sorting available on controls like the GridView that support it. The samples available for download for this book contain a complete implementation of a data access layer for both the movies and reviews tables, complete with custom paging, sorting, and even filtering.

Typed DataSets

Typed DataSets remain relatively unchanged from their 1.1 incarnation. One significant exception is that the .xsd file now can contain information about a TableAdapter as well as the schema for the typed DataSet. This means that there is now a strongly typed class that is capable of populating the associated typed DataSet, as well as propagating updates, inserts, and deletes.

The easiest way to bind a typed DataSet to a data-bound control is to expose it using an ObjectDataSource. The object data source will pick up on its public properties, notice that its methods return DataTables and DataRows, and enable all of the functionality exposed by the DataSet automatically. It creates an autogenerated type-safe class that acts as an intermediary between the database and the controls on the form. However, it also exposes the database's underlying schema directly to the presentation layer so that changes in the database will directly affect the controls on your forms; hence, it does not have all of the advantages of a custom data access layer (although it is certainly much less work to create).




Essential ASP. NET 2.0
Essential ASP.NET 2.0
ISBN: 0321237706
EAN: 2147483647
Year: 2006
Pages: 104

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net