ASP.NET 1.x has an extremely flexible and generic data-binding architecture that gives developers full control of the page life cycle. Developers can link data-bound controls such as the DataGrid to any enumerable collection of data. While this approach represents a quantum leap from classic ASP, it still requires page developers to learn a lot of architectural details to create even relatively simple read-only pages. This is a problem for Web developers with limited skills because they soon get into trouble if left alone to decide how (or whether) to implement paging, sorting, updates, or perhaps a master/detail view. But this is a (different) problem for experienced developers, as they have to continually reimplement the same pattern to access data sources, get data, and make the data consistent with the programming interface of data controls.
The key issue with ASP.NET 1.x data binding is a lack of a higher-level and possibly declarative model for data fetching and data manipulation. As a result, an ASP.NET 1.x data access layer is boring to write and requires hundreds of lines of code even for relatively simple scenarios. Enter ASP.NET 2.0 data source components.
A data source component is a server control designed to interact with data-bound controls and hide the complexity of the manual data-binding pattern. Data source components not only provide data to controls, but they also support data-bound controls in the execution of other common operations such as insertions, deletions, sorting, and updates. Each data source component wraps a particular data provider relational databases, XML documents, or custom classes. The support for custom classes means that you can now directly bind your controls to existing classes for example, classes in your business or data access layer. (I'll say more about this later.)
Existing ASP.NET 1.x controls have been extended in ASP.NET 2.0 to support binding to data source controls as far as data retrieval is concerned. The DataSourceID property represents the point of contact between old-style data-bound controls and the new data source components. In ASP.NET 2.0, you can successfully bind a DataGrid to a data source control without writing a single line of code not even the ritual call to DataBind. However, achieving codeless programming is not the primary goal of data source controls. Think of data source controls as the natural tool to achieve a less complex and semi-automatic interaction between a variety of data sources and controls.
Existing controls such as DataGrid and Repeater don't take full advantage of data source components. Only ASP.NET 2.0-specific controls such as GridView, FormView, and DetailsView benefit from the true power of data source controls. This is because new controls have a different internal structure specifically designed to deal with data source controls and share with them the complexity of the data-binding pattern.
To understand the primary goal of data source components, consider what happens when the user performs an action on some data displayed through a DataGrid. Imagine that you display an editable grid that is, a grid that contains an edit column. Users click a cell to edit the corresponding row; the DataGrid posts back and fires an event. Page authors handle the event by writing some code to turn on the control in edit mode. A pair of OK/Cancel buttons replaces the edit button. The user edits the contents of the row and then clicks to save or cancel changes. What happens at this point?
The DataGrid control captures the event, validates, and then fires the UpdateCommand event. The page author is in charge of handling the event, collecting new data, and building and running any required command against the data source. All these steps require code. The same happens if you need to sort data, view a new page, or drill down into the currently selected record.
Let's see what happens if you use the successor to the DataGrid control the GridView control which is specifically designed to adhere to the data source model. Let's assume the same scenario: the user clicks, and the control enters edit mode. The first difference is that you don't need to write any code to turn on edit mode. If you click on a cell within an edit column, the control "knows" what you want to do and intelligently takes the next step and executes the requested action turning on the edit mode.
When the user clicks to save changes, again the GridView control anticipates the user's next action and talks to the data source control to have it perform the requested operation (update) on the data source. All this requires no code from the page author; only a few settings, such as the command text and the connection string, are required and can be set declaratively.
The combination of data source controls and new, smarter data-bound controls demonstrates its true power when your code addresses relatively common scenarios, which is probably 70 to 80 percent of the time. If you need to have things done in a particular way, just work the old way and take full control of the page life cycle. This said, in data source controls you find much more than just a deep understanding of the page life cycle. Data source controls support declarative parameters, transparent data caching, server-side paging ability, hierarchical data support, and the ability to work asynchronously. Implementing all these features manually would require quite a bit of code.
A data source control represents one or more named views of data. Each view manages a collection of data. The data associated with a data source control is managed through SQL-like operations such as SELECT, INSERT, DELETE, and COUNT and through capabilities such as sorting and paging. Data source controls come in two flavors tabular and hierarchical. Tabular controls are described in Table 9-6.
Class | Description |
---|---|
AccessDataSource | Represents a connection to a Microsoft Office Access database. Inherits from the SqlDataSource control but points to an MDB file and uses the Jet 4.0 OLE DB provider to connect to the database. |
ObjectDataSource | Allows binding to a custom .NET business object that returns data. The class is expected to follow a specific design pattern and include, for example, a parameterless constructor and methods that behave in a certain way. |
SqlDataSource | Represents a connection to an ADO.NET data provider that returns SQL data, including data sources accessible through OLE DB and ODBC. The name of the provider and the connection string are specified through properties. |
Note that the SqlDataSource class is not specific to SQL Server. It can connect to any ADO.NET provider that manages relational data. Hierarchical data source controls are listed in Table 9-7.
Class | Description |
---|---|
SiteMapDataSource | Allows binding to any provider that supplies site map information. The default provider supplies site map data through an XML file in the root folder of the application. |
XmlDataSource | Allows binding to XML files and strings with or without schema information. |
Note that data source controls have no visual rendering. They are implemented as controls to allow for "declarative persistence" (automatic instantiation during the request processing) as a native part of the .aspx source code and to gain access to the page view state.
A named view is represented by a data source view object an instance of the DataSourceView class. These classes represent a customized view of data in which special settings for sorting, filtering, and other data operations have been defined. The DataSourceView class is the base class for all views associated with a data source control. The number of views in a data source control depends on the connection string, characteristics, and actual contents of the underlying data source. In ASP.NET 2.0, built-in controls support only one view, the default view. Table 9-8 lists the properties of the DataSourceView class.
Property | Description |
---|---|
CanDelete | Indicates whether deletions are allowed on the underlying data source. The deletion is performed by invoking the Delete method. |
CanInsert | Indicates whether insertions are allowed on the underlying data source. The insertion is performed by invoking the Insert method. |
CanPage | Indicates whether the data in the view can be paged. |
CanRetrieveTotalRowCount | Indicates whether information about the total row count is available. |
CanSort | Indicates whether the data in the view can be sorted. |
CanUpdate | Indicates whether updates are allowed on the underlying data source. The update is performed by invoking the Update method. |
Name | Returns the name of the current view. |
The CanXXX properties indicate not only whether the data source control is capable of performing the specified operation but also whether that operation is appropriate given the current status of the data. Table 9-9 lists all the methods supported by the class.
Method | Description |
---|---|
Delete | Performs a delete operation on the data associated with the view |
Insert | Performs an insert operation on the data associated with the view |
Select | Returns an enumerable object filled with the data contained in the underlying data storage |
Update | Performs an update operation on the data associated with the view |
All data source view objects support data retrieval through the Select method. The method returns an object that implements the IEnumerable interface. The real type of the object depends on the data source control and the attributes set on it.
Figure 9-10 shows the interaction between a data source control and data-bound control in ASP.NET 2.0.
Figure 9-10: The data-bound control gets a view object and talks about capabilities and operations.
ASP.NET 2.0 controls are aware of the full potential of the data source control and, through the data source control, they use the methods of IDataSource to connect to the underlying data repository. Implementing the interface is the only official requirement for a control that intends to behave like a data source control. Once it gets hold of a data source view object, the control can call the properties and methods shown in Table 9-8 and Table 9-9 to perform required tasks.
Unlike tabular data source controls, which typically have only one named view, hierarchical data source controls support a view for each level of data that the data source control represents. Hierarchical and tabular data source controls share the same conceptual specification of a consistent and common programming interface for data-bound controls. The only difference is the nature of the data they work with hierarchical versus flat and tabular.
The view class is different and is named HierarchicalDataSourceView. The class features only one method Select which returns an enumerable hierarchical object. Hierarchical data source controls are, therefore, read-only.
The SqlDataSource control is a data source control that represents a connection to a relational data store such as SQL Server or Oracle or any data source accessible through OLE DB and ODBC bridges.
You set up the connection to the data store using two main properties, ConnectionString and ProviderName. The former represents the connection string and contains enough information to open a session with the underlying engine. The latter specifies the namespace of the ADO.NET managed provider to use for the operation. The ProviderName property defaults to System.Data.SqlClient, which means that the default data store is SQL Server. For example, to target an OLE DB provider, use the System.Data.OleDb string instead.
The control can retrieve data using either a data adapter or a command object. Depending on your choice, fetched data will be packed in a DataSet object or a data reader. The following code snippet shows the minimal code necessary to activate a SQL data source control bound to a SQL Server database:
<asp:SqlDataSource runat="server" ProviderName='<%$ ConnectionStrings:LocalNWind.ProviderName %>' ConnectionString='<%$ ConnectionStrings:LocalNWind %>' SelectCommand="SELECT * FROM employees" /> <asp:DataGrid runat="server" DataSource />
The data operations supported by the associated view class are provided by the property groups listed in Table 9-10.
Property Group | Description |
---|---|
DeleteCommand, DeleteParameters, DeleteCommandType | Gets or sets the SQL statement, related parameters, and type (text or stored procedure) used to delete rows in the underlying data store. |
FilterExpression, FilterParameters | Gets or sets the string (and related parameters) to create a filter on top of the data retrieved using the Select command. Only works if the control manages data through a DataSet. |
InsertCommand, InsertParameters, InsertCommandType | Gets or sets the SQL statement, related parameters, and type (text or stored procedure) used to insert new rows in the underlying data store. |
SelectCommand, SelectParameters, SelectCommandType | Gets or sets the SQL statement, related parameters, and type (text or stored procedure) used to retrieve data from the underlying data store. |
SortParameterName | Gets or sets the name of an input parameter that a command's stored procedure will use to sort data. (The command in this case must be a stored procedure.) It raises an exception if the parameter is missing. |
UpdateCommand, UpdateParameters, UpdateCommandType | Gets or sets the SQL statement, related parameters, and type (text or stored procedure) used to update rows in the underlying data store. |
Each command property is a string that contains the SQL text to be used. The command can optionally contain the parameters listed in the associated parameter collection. The managed provider and its underlying relational engine determine the exact syntax of the SQL to use and the syntax of the embedded parameters. For example, if the data source control points to SQL Server, command parameter names must be prefixed with the @ symbol. If the target data source is an OLE DB provider, parameters are unnamed, identified with a ? placeholder symbol, and located by position. The following code snippet shows a more complex data source control in which parametric delete and update commands have been enabled:
<asp:SqlDataSource runat="server" ConnectionString='<%$ ConnectionStrings:LocalNWind %>' SelectCommand="SELECT * FROM employees" UpdateCommand="UPDATE employees SET lastname=@lname" DeleteCommand="DELETE FROM employees WHERE employeeid=@TheEmp" FilterExpression="employeeid > 3"> <!-- parameters go here --> </asp:SqlDataSource>
The syntax used for the FilterExpression property is the same as the syntax used for the RowFilter property of the DataView class, which in turn is similar to that used with the SQL WHERE clause. If the FilterExpression property needs to be parametric, you can indicate parameters through the FilterParameters collection. Filtering is enabled only when DataSourceMode is set to DataSet.
Note | Note the difference between filter expressions and parameters on the Select command. Parameters on the command influence the result set returned by the data store; a filter expression restricts for display the result set returned through the Select command. |
Table 9-11 details other operational properties defined on the SqlDataSource class. The list doesn't include cache-related properties, which we'll cover in a moment.
Property | Description |
---|---|
CancelSelectOnNullParameter | Indicates whether a data-retrieval operation is cancelled if a parameter evaluates to null. The default value is true. |
ConflictDetection | Determines how the control should handle data conflicts during a delete or update operation. By default, changes that occurred in the meantime are overwritten. |
ConnectionString | The connection string to connect to the database. |
DataSourceMode | Indicates how data should be returned via a DataSet or data reader. |
OldValuesParameterFormatString | Gets or sets a format string to apply to the names of any parameters passed to the Delete or Update method. |
ProviderName | Indicates the namespace of the ADO.NET managed provider to use. |
It is interesting to note that many of these properties mirror identical properties defined on the actual view class, as illustrated earlier in Figure 9-10.
The SqlDataSource object features a few methods and events, which in most cases are common to all data source components. The methods are Delete, Insert, Select, and Update, and they're implemented as mere wrappers around the corresponding methods of the underlying data source view class. Events exist in pairs Deleting/Deleted, Inserting/Inserted, Selecting/Selected, and Updating/Updated and fire before and after any of the methods just mentioned. The beginning of a filtering operation is signaled through the Filtering event.
As mentioned, ASP.NET 2.0-specific controls are the only ones to really take advantage of the capabilities of data source controls. For this reason, in the next two chapters devoted to GridView, DetailsView, and FormView controls, we'll see a lot of sample code showing how to use the SqlDataSource control for selecting, updating, paging, and sorting. In this chapter, we'll need to spend more time discussing other features of the control that can be particularly useful in real-world applications.
Each command property has its own collection of parameters an instance of a collection class named ParameterCollection. ASP.NET 2.0 supports quite a few parameter types, which are listed in Table 9-12.
Parameter | Description |
---|---|
ControlParameter | Gets the parameter value from any public property of a server control |
CookieParameter | Sets the parameter value based on the content of the specified HTTP cookie |
FormParameter | Gets the parameter value from the specified input field in the HTTP request form |
Parameter | Gets the parameter value assigned by the code |
ProfileParameter | Gets the parameter value from the specified property name in the profile object created from the application's personalization scheme |
QueryStringParameter | Gets the parameter value from the specified variable in the request query string |
SessionParameter | Sets the parameter value based on the content of the specified session state slot |
Each parameter class has a Name property and a set of properties specific to its role and implementation. To understand declarative parameters in data source controls, take a look at the following code:
<asp:SqlDataSource runat="server" ConnectionString='<%$ ConnectionStrings:LocalNWind %>' SelectCommand="SELECT * FROM employees WHERE employeeid > @MinID"> <SelectParameters> <asp:ControlParameter Name="MinID" Control PropertyName="Text" /> </SelectParameters> </asp:SqlDataSource>
The query contains a placeholder named @MinID. The data source control automatically populates the placeholder with the information returned by the ControlParameter object. The value of the parameter is determined by the value of a given property on a given control. The name of the property is specified by the PropertyName attribute. The ID of the control is in the ControlId attribute. For the previous code to work, page developers must guarantee that the page contains a control with a given ID and property; otherwise, an exception is thrown. In the example, the value of the property Text on the EmpID control is used as the value for the matching parameter.
The binding between formal parameters (the placeholders in the command text) and actual values depends on how the underlying managed provider handles and recognizes parameters. If the provider type supports named parameters as is the case with SQL Server and Oracle the binding involves matching the names of placeholders with the names of the parameters. Otherwise, the matching is based on the position. Hence, the first placeholder is bound to the first parameter, and so on. This is what happens if OLE DB is used to access the data.
The SqlDataSource control can optionally perform database-intrusive operations (deletions and updates) in either of two ways. The data source control is associated with a data-bound control, so it is not a far-fetched idea that data is read at the same time, perhaps modified on the client, and then updated. In a situation in which multiple users have read/write access to the database, what should be the behavior of the update/delete methods if the record they attempt to work on has been modified in the meantime?
The SqlDataSource control uses the ConflictDetection property to determine what to do when performing update and delete operations. The property is declared as type ConflictOptions an enum type. The default value is OverwriteChanges, which means that any intrusive operation happens regardless of whether values in the row have changed since they were last read. The alternative is the CompareAllValues value, which simply ensures that the SqlDataSource control passes the original data read from the database to the Delete or Update method of the underlying view class.
It is important to note that changing the value of ConflictDetection doesn't produce any significant effect unless you write your delete or update statements in such a way that the command fails if the data in the row doesn't match the data that was initially read. To get this, you should define the command as follows:
UPDATE employees SET firstname=@firstname WHERE employeeid=@employeeid AND firstname=@original_firstname
In other words, you must explicitly add to the command an extra clause to check whether the current value of the field being modified still matches the value initially read. In this way, intermediate changes entered by concurrent users make the WHERE clause fail and make the command fail. You are in charge of tweaking the command text yourself; setting ConflictDetection to CompareAllValues is not enough.
How would you format the name of the parameters that represent old values? The SqlDataSource control uses the OldValuesParameterFormatString property to format these parameter names. The default value is original_{0}.
When you use the CompareAllValues option, you can handle the Deleted or Updated event on the data source control to check how many rows are affected. If no rows are affected by the operation, a concurrency violation might have occurred:
void OnUpdated(object sender, SqlDataSourceStatusEventArgs e) { if (e.AffectedRows == 0) { ... } }
The data binding between a data-bound control and its data source component is automatic and takes place on each postback caused by the data-bound control. Imagine a page with grid, a data source control, and a button. If you turn on the grid in edit mode, the Select command is run; if you click the button (outside the boundaries of the data-bound control) the UI of the grid is rebuilt from the view state and no Select statement is run.
To save a query on each postback, you can ask the data source control to cache the result set for a given duration. While data is cached, the Select method retrieves data from the cache rather than from the underlying database. When the cache expires, the Select method retrieves data from the underlying database, and stores the fresh data back to the cache. The caching behavior of the SqlDataSource control is governed by the properties in Table 9-13.
Property | Description |
---|---|
CacheDuration | Indicates in seconds how long the data should be maintained in the cache. |
CacheExpirationPolicy | Indicates if the cache duration is absolute or sliding. If absolute, data is invalidated after the specified number of seconds. If sliding, data is invalidated if not used for the specified duration. |
CacheKeyDependency | Indicates the name of a user-defined cache key that is linked to all cache entries created by the data source control. By expiring the key, you can clear the control's cache. |
EnableCaching | Enables or disables caching support. |
SqlCacheDependency | Gets or sets a semicolon-delimited string that indicates which databases and tables to use for the SQL Server cache dependency. |
A single cache entry is created for each distinct combination of SelectCommand, ConnectionString, and SelectParameters. Multiple SqlDataSource controls can share the same cache entries if they happen to load the same data from the same database. You can take control of cache entries managed by the data source control through the CacheKeyDependency property. If set to a non-null string, the property forces the SqlDataSource control to create a dependency between that key and all cache entries created by the control. At this point, to clear the control's cache, you only have to assign a new value to the dependency key:
Cache["ClearAll"] = anyInitializationValue; SqlDataSource1.CacheKeyDependency = "ClearAll"; ... Cache["ClearAll"] = anyOtherValue;
The SqlDataSource control can cache data only when working in DataSet mode. You get an exception if DataSourceMode is set to DataReader and caching is enabled.
Finally, the SqlCacheDependency property links the SqlDataSource cached data with the contents of the specified database table (typically, the same table where the cached data comes from):
<asp:SqlDataSource runat="server" CacheDuration="1200" ConnectionString="<%$ ConnectionStrings:LocalNWind %>" EnableCaching="true" SelectCommand="SELECT * FROM employees" SqlCacheDependency="Northwind:Employees"> </asp:SqlDataSource>
Whenever the underlying table changes, the cached data is automatically flushed. We'll cover SQL cache dependencies in detail in Chapter 14.
The AccessDataSource control is a data source control that represents a connection to an Access database. It is based on the SqlDataSource control and provides a simpler, made-to-measure programming interface. As a derived class, AccessDataSource inherits all members defined on its parent and overrides a few of them. In particular, the control replaces the ConnectionString and ProviderName properties with a more direct DataFile property. You set this property to the .mdb database file of choice. The data source control resolves the file path at run time and uses the Microsoft Jet 4 OLE DB provider to connect to the database.
Note | AccessDataSource actually inherits from SqlDataSource and for this reason can't make base members disappear, as hinted at earlier. AccessDataSource doesn't really replace the ConnectionString and ProviderName properties; it overrides them so that an exception is thrown whenever someone attempts to set their value. Another property overridden only to throw exceptions is SqlCacheDependency. This feature, of course, is not supported. |
The following code shows how to use the AccessDataSource control to open an .mdb file and bind its content to a drop-down list control. Note that the control opens Access database files in read-only mode by default:
<asp:AccessDataSource runat="server" DataFile="nwind.mdb" SelectCommand="SELECT * FROM Customers" /> Select a Customer: <asp:DropDownList runat="server" DataSource />
Several features of the AccessDataSource control are inherited from the base class, SqlDataSource. In fact, the Access data source control is basically a SQL data source control optimized to work with Access databases. Like its parent control, the AccessDataSource control supports two distinct data source modes DataSet and DataReader, depending on the ADO.NET classes used to retrieve data. Filtering can be applied to the selected data only if the fetch operation returns a DataSet. Caching works as on the parent class except for the SqlCacheDependency feature.
The AccessDataSource can also be used to perform insert, update, or delete operations against the associated database. This is done using ADO.NET commands and parameter collections. Updates are problematic for Access databases when performed from within an ASP.NET application because an Access database is a plain file and the default account of the ASP.NET process (ASPNET or NetworkService, depending on the host operating system) might not have sufficient permission to write to the database file. For the data source updates to work, you should grant write permission to the ASP.NET account on the database file. Alternatively, you can use a different account with adequate permission.
Note | Most Internet service providers (ISPs) normally give you one directory in which ASPNET and NetworkService accounts have been granted write permission. In this case, you just place your Access file in this directory and you can read and write seamlessly. In general, though, Access databases are plain files and, as such, are subject to the security rules of ASP.NET. |
The ObjectDataSource class enables user-defined classes to associate the output of their methods to data-bound controls. Like other data source controls, ObjectDataSource supports declarative parameters to allow developers to pass page-level variables to the object's methods. The ObjectDataSource class makes some assumptions about the objects it wraps. As a consequence, an arbitrary class can't be used with this data source control. In particular, bindable classes are expected to have a default constructor, to be stateless, and to have methods that easily map to select, update, insert, and delete semantics. Also, the object must perform updates one item at a time; objects that update their states using batch operations are not supported. The bottom line is that managed objects that work well with ObjectDataSource are designed with this data source class in mind.
The ObjectDataSource component provides nearly the same programmatic interface (events, methods, and properties, and associated behaviors) as the SqlDataSource, with the addition of three new events and a few properties. The events are related to the lifetime of the underlying business object ObjectCreating, ObjectCreated, and ObjectDisposing. Table 9-14 lists other key properties of ObjectDataSource.
Property | Description |
---|---|
ConvertNullToDBNull | Indicates whether null parameters passed to insert, delete, or update operations are converted to System.DBNull. False by default. |
DataObjectTypeName | Gets or sets the name of a class that is to be used as a parameter for a Select, Insert, Update, or Delete operation. |
DeleteMethod, DeleteParameters | Gets or sets the name of the method and related parameters used to perform a delete operation. |
EnablePaging | Indicates whether the control supports paging. |
FilterExpression, FilterParameters | Indicates the filter expression (and parameters) to filter the output of a select operation. |
InsertMethod, InsertParameters | Gets or sets the name of the method and related parameters used to perform an insert operation. |
MaximumRowsParameterName | If the EnablePaging property is set to true, indicates the parameter name of the Select method that accepts the value for the number of records to retrieve. |
OldValuesParameterFormatString | Gets or sets a format string to apply to the names of any parameters passed to the Delete or Update methods. |
SelectCountMethod | Gets or sets the name of the method used to perform a select count operation. |
SelectMethod, SelectParameters | Gets or sets the name of the method and related parameters used to perform a select operation. |
SortParameterName | Gets or sets the name of an input parameter used to sort retrieved data. It raises an exception if the parameter is missing. |
StartRowIndexParameterName | If the EnablePaging property is set to true, indicates the parameter name of the Select method that accepts the value for the starting record to retrieve. |
UpdateMethod, UpdateParameters | Gets or sets the name of the method and related parameters used to perform an update operation. |
The ObjectDataSource control uses reflection to locate and invoke the method to handle the specified operation. The TypeName property returns the fully qualified name of the assembly that defines the class to call. If the class is defined in the App_Code directory, you don't need to indicate the assembly name. Otherwise, you use a comma-separated string in the form of [classname, assembly]. Let's see an example.
Warning | Having too many classes in the App_Code directory can become a nightmare at development time because any changes to any files will cause Visual Studio .NET to recompile the whole set of files in the project. |
The following code snippet illustrates a class that can be used with an object data source. Architected according to the Table Data Gateway (TDG) pattern, the class represents employees and takes advantage of two other helper classes (at the very minimum): Employee and EmployeeCollection. The class Employee contains information about the entity being represented; the class EmployeeCollection represents a collection of employees. The behavior of the entity "employee" is codified in a bunch of methods exposed out of the gateway class Employees:
public class Employees { public static string ConnectionString { ... } public static void Load(int employeeID) { ... } public static EmployeeCollection LoadAll() { ... } public static EmployeeCollection LoadByCountry(string country) { ... } public static void Save(Employee emp) { ... } public static void Insert(Employee emp) { ... } public static void Delete(int employeeID) { ... } ... }
The TDG pattern requires the gateway to be shared among instances of the entity class in the following example, I implemented the class with static methods. If you don't use static methods, the worker class you use with ObjectDataSource must have a default parameterless constructor. Furthermore, the class should not maintain any state.
Warning | Using static methods in the context of a TDG pattern is fine from an architectural viewpoint, but it might pose practical problems with unit testing. What if you test a business class that calls the Data Access Layer (DAL) internally and the DAL fails? Can you figure out what really happened? Does the business class work or not? To effectively test a business layer that calls into a DAL, you need to focus on the object under test. Mock objects come to the rescue. Mock objects are programmable polymorphic objects that present themselves as others and can wrap DAL anomalies and signal them out clearly, making the test succeed if nothing else happens. The point is that mocking toolkits typically don't like static methods. That's why instance methods might be preferable in real-world implementations of the TDG pattern. |
The worker class must be accessible from within the .aspx page and can be bound to the ObjectDataSource control, as shown here:
<asp:ObjectDataSource runat="server" TypeName="ProAspNet20.DAL.Employees" SelectMethod="LoadAll" />
When the HTTP runtime encounters a similar block in a Web page, it generates code that calls the LoadAll method on the specified class. The returned data an instance of the EmployeeCollection is bound to any control that links to MyObjectSource via the DataSourceID property. Let's take a brief look at the implementation of the LoadAll method:
public static EmployeeCollection LoadAll() { EmployeeCollection coll = new EmployeeCollection(); using (SqlConnection conn = new SqlConnection(ConnectionString)) { SqlCommand cmd = new SqlCommand("SELECT * FROM employees", conn); conn.Open(); SqlDataReader reader = cmd.ExecuteReader(); HelperMethods.FillEmployeeList(coll, reader); reader.Close(); conn.Close(); } return coll; }
A bit oversimplified to fit in the section, the preceding code remains quite clear: you execute a command, fill in a custom collection class, and return it to the data-bound control. The only piece of code you need to write is the worker class you don't need to put any code in the code-behind class of the page:
<asp:DataGrid runat="server" DataSource />
The DataGrid receives a collection of Employee objects defined as follows:
public class EmployeeCollection : List<Employee> { }
Binding is totally seamless, even without ADO.NET container objects. (See the companion code for full details.)
The method associated with the SelectMethod property must return any of the following: an IEnumerable object such as a collection, DataSet, DataTable, or Object. Preferably, the Select method is not overloaded, although ObjectDataSource doesn't prevent you from using overloading in your business classes.
In most cases, methods require parameters. SelectParameters is the collection you use to add input parameters to the select method. Imagine you have a method to load employees by country. Here's the code you need to come up with:
<asp:ObjectDataSource runat="server" TypeName="ProAspNet20.DAL.Employees" SelectMethod="LoadByCountry"> <SelectParameters> <asp:ControlParameter Name="country" Control PropertyName="SelectedValue" /> </SelectParameters> </asp:ObjectDataSource>
The preceding code snippet is the declarative version of the following pseudocode, where Countries is expected to be a drop-down list filled with country names:
string country = Countries.SelectedValue; EmployeeCollection coll = Employees.LoadByCountry(country);
The ControlParameter class automates the retrieval of the actual parameter value and the binding to the parameter list of the method. What if you add an [All Countries] entry to the drop-down list? In this case, if the All Countries option is selected, you need to call LoadAll without parameters; otherwise, if a particular country is selected, you need to call LoadByCountry with a parameter. Declarative programming works great in the simple scenarios; otherwise, you just write code:
void Page_Load(object sender, EventArgs e) { // Must be cleared every time (or disable the viewstate) ObjectDataSource1.SelectParameters.Clear(); if (Countries.SelectedIndex == 0) ObjectDataSource1.SelectMethod = "LoadAll"; else { ObjectDataSource1.SelectMethod = "LoadByCountry"; ControlParameter cp = new ControlParameter("country", "Countries", "SelectedValue"); ObjectDataSource1.SelectParameters.Add(cp); } }
Note that data source controls are like ordinary server controls and can be programmatically configured and invoked. In the code just shown, you first check the selection the user made and if it matches the first option (All Countries), configure the data source control to make a parameterless call to the LoadAll method.
You must clean up the content of the SelectParameters collection upon page loading. The data source control (more precisely, the underlying view control) caches most of its properties to the view state. As a result, SelectParameters is not empty when you refresh the page after changing the drop-down list selection. The preceding code clears only the SelectParameters collection; performance-wise, it could be preferable to disable the view state altogether on the data source control. However, if you disable the view state, all collections will be empty on the data source control upon loading.
Important | ObjectDataSource allows data retrieval and update while keeping data access and business logic separate from user interface. The use of the ObjectDataSource class doesn't automatically transform your system into a well-designed, effective n-tiered system. Data source controls are mostly a counterpart to data-bound controls so that the latter can work more intelligently. To take full advantage of ObjectDataSource, you need to have your DAL already in place. It doesn't work the other way around. ObjectDataSource doesn't necessarily have to be bound to the root of the DAL, which could be on a remote location and perhaps behind a firewall. In this case, you write a local intermediate object and connect it to ObjectDataSource on one end and to the DAL on the other end. The intermediate object acts as an application-specific proxy and works according to the application's specific rules. ObjectDataSource doesn't break n-tiered systems, nor does it transform existing systems into truly n-tier systems. It greatly benefits, instead, from existing business and data layers. |
The ObjectDataSource component supports caching only when the specified select method returns a DataSet or DataTable object. If the wrapped object returns a custom collection (as in the example we're considering), an exception is thrown.
ObjectDataSource is designed to work with classes in the business layer of the application. An instance of the business class is created for each operation performed and destroyed shortly after the operation is complete. This model is the natural offspring of the stateless programming model that ASP.NET promotes. In case of business objects that are particularly expensive to initialize, you can resort to static classes or static methods in instance classes. (If you do so, bear in mind what we said earlier regarding unit testing classes with static methods.)
Instances of the business object are not automatically cached or pooled. Both options, though, can be manually implemented by properly handling the ObjectCreating and ObjectDisposing events on an ObjectDataSource control. The ObjectCreating event fires when the data source control needs to get an instance of the business class. You can write the handler to retrieve an existing instance of the class and return that to the data source control:
// Handle the ObjectCreating event on the data source control public void BusinessObjectBeingCreated(object sender, ObjectDataSourceEventArgs e) { BusinessObject bo = RetrieveBusinessObjectFromPool(); if (bo == null) bo = new BusinessObject(); e.ObjectInstance = bo; }
Likewise, in ObjectDisposing you store the instance again and cancel the disposing operation being executed:
// Handle the ObjectDisposing event on the data source control public void BusinessObjectBeingDisposed(object sender, ObjectDataSourceDisposingEventArgs e) { ReturnBusinessObjectToPool(e.ObjectInstance); e.Cancel = true; }
It is not only object instances that aren't cached. In some cases, even retrieved data is not persisted in memory for the specified duration. More precisely, the ObjectDataSource control does support caching just as SqlDataSource does, except that caching is enabled only if the select method returns a DataTable or DataSet object. A DataView, and in general a simple enumerable collection of data, isn't cached; if you enable caching in this scenario, an exception will be thrown.
Note | Just as with caching, filtering is also not permitted when the return value is not an ADO.NET container class |
Unlike SqlDataSource, ObjectDataSource also supports paging. Three properties in Table 9-14 participate in paging EnablePaging, StartRowIndexParameterName, and MaximumRowsParameterName.
As the name clearly suggests, EnablePaging toggles support for paging on and off. The default value is false, meaning that paging is not turned on automatically. ObjectDataSource provides an infrastructure for paging, but actual paging must be implemented in the class bound to ObjectDataSource. In the following code snippet, the Customers class has a method, LoadByCountry, that takes two additional parameters to indicate the page size and the index of the first record in the page. The names of these two parameters must be assigned to MaximumRowsParameterName and StartRowIndexParameterName, respectively:
<asp:ObjectDataSource runat="server" TypeName="ProAspNet20.DAL.Customers" StartRowIndexParameterName="firstRow" MaximumRowsParameterName="totalRows" SelectMethod="LoadByCountry"> <SelectParameters> <asp:ControlParameter Name="country" Control PropertyName="SelectedValue" /> <asp:ControlParameter Name="totalRows" Control PropertyName="Text" /> <asp:ControlParameter Name="firstRow" Control PropertyName="Text" /> </SelectParameters> </asp:ObjectDataSource>
The implementation of paging is up to the method and must be coded manually. LoadByCountry provides two overloads, one of which supports paging. Internally, paging is actually delegated to FillCustomerList:
public static CustomerCollection LoadByCountry(string country) { return LoadByCountry(country, -1, 0); } public static CustomerCollection LoadByCountry(string country, int totalRows, int firstRow) { CustomerCollection coll = new CustomerCollection(); using (SqlConnection conn = new SqlConnection(ConnectionString)) { SqlCommand cmd; cmd = new SqlCommand(CustomerCommands.cmdLoadByCountry, conn); cmd.Parameters.AddWithValue("@country", country); conn.Open(); SqlDataReader reader = cmd.ExecuteReader(); HelperMethods.FillCustomerList(coll, reader, totalRows, firstRow); reader.Close(); conn.Close(); } return coll; }
As you can see in the companion source code, FillCustomerList doesn't use a particularly smart approach. It simply scrolls the whole result set using a reader and discards all the records that don't belong in the requested range. You could improve upon this approach to make paging smarter. What's important here is that paging is built into your business object and exposed by data source controls to the pageable controls through a well-known interface.
To update underlying data using ObjectDataSource, you need to define an update/insert/ delete method. All the actual methods you use must have semantics that are well-suited to implement such operations. Again, this requirement is easily met if you employ the TDG pattern in the design of your DAL. Here are some good prototypes for the update operations:
public static void Save(Employee emp) public static void Insert(Employee emp) public static void Delete(int id)
More than select operations, update operations require parameters. To update a record, you need to pass new values and one or more old values to make sure the right record to update is located and to take into account the possibility of data conflicts. To delete a record, you need to identify it by matching a supplied primary key parameter. To specify input parameters, you can use command collections such as UpdateParameters, InsertParameters, or DeleteParameters. Let's examine update/insert scenarios first.
To update an existing record or insert a new one, you need to pass new values. This can be done in either of two ways listing parameters explicitly or aggregating all parameters in an allencompassing data structure. The prototypes shown previously for Save and Insert follow the latter approach. An alternative might be the following:
public static void Save(int id, string firstName, string lastName, ...) public static void Insert(string firstName, string lastName, ...)
You can use command parameter collections only if the types involved are simple types numbers, strings, dates. If your DAL implements the TDG pattern (or a similar one, such as Data Mapper), your update/insert methods are likely to accept a custom aggregate data object as a parameter the Employee class seen earlier. To make a custom class such as Employee acceptable to the ObjectDataSource control, you need to set the DataObjectTypeName property:
<asp:ObjectDataSource runat="server" TypeName="ProAspNet20.DAL.Employees" SelectMethod="Load" UpdateMethod="Save" DataObjectTypeName="ProAspNet20.DAL.Employee"> <SelectParameters> <asp:ControlParameter Name="id" Control PropertyName="SelectedValue" /> </SelectParameters> </asp:ObjectDataSource>
The preceding ObjectDataSource control saves rows through the Save method, which takes an Employee object. Note that when you set the DataObjectTypeName property, the UpdateParameters collection is ignored. The ObjectDataSource instantiates a default instance of the object before the operation is performed and then attempts to fill its public members with the values of any matching input fields found around the bound control. Because this work is performed using reflection, the names of the input fields in the bound control must match the names of public properties exposed by the object in the DataObjectTypeName property. A practical limitation you must be aware of is that you can't define the Employee class using complex data types, as follows:
public class Employee { public string LastName { } public string FirstName { } ... public Address HomeAddress { } }
Representing individual values (strings in the sample), the LastName and FirstName members have good chances to match an input field in the bound control. The same can't be said for the HomeAddress member, which is declared of a custom aggregate type like Address. If you go with this schema, all the members in Address will be ignored; any related information won't be carried into the Save method, with resulting null parameters. All the members in the Address data structure should become members of the Employee class.
Note | Recall that data source controls work at their fullest only with a few ASP.NET 2.0 controls, such as GridView (Chapter 10) and DetailsView (Chapter 11). We'll return to the topic of the internal mechanism of parameter binding later in the book. For now, it suffices to say that as a page author you're responsible for making input fields and member names match in the following way: columns in GridView and rows in DetailsView have a DataField attribute pointing to the data source field to use (that is, lastname, where lastname is typically a database column retrieved by the select operation). The data field name must match (case-insensitive) a public property in the class used as a parameter in the update/insert operation in this case, the Employee class. |
Unlike the insert operation, the update operation also requires a primary key value to identify uniquely the record being updated. If you use an explicit parameter listing, you just append an additional parameter to the list to represent the ID, as follows:
<asp:ObjectDataSource runat="server" TypeName="ProAspNet20.SimpleBusinessObject" SelectMethod="GetEmployees" UpdateMethod="SetEmployee"> <UpdateParameters> <asp:Parameter Name="employeeid" Type="Int32" /> <asp:Parameter Name="firstname" Type="string" /> <asp:Parameter Name="lastname" Type="string" /> <asp:Parameter Name="country" Type="string" DefaultValue="null" /> </UpdateParameters> </asp:ObjectDataSource>
Note that by setting the DefaultValue attribute to null, you can make a parameter optional. A null value for a parameter must then be gracefully handled by the method to implement the update.
There's an alternative method to set the primary key through the DataKeyNames property of GridView and DetailsView controls. I'll briefly mention it here and cover it in much greater detail in the next two chapters:
<asp:GridView runat="server" DataKeyNames="employeeid" DataSource AutoGenerateEditButton="true"> ... </asp:GridView>
When DataKeyNames is set on the bound control, data source controls automatically add a parameter to the list of parameters for update and delete commands. The default name of the parameter is original_XXX, where XXX stands for the value of DataKeyNames. For the operation to succeed, the method (or the SQL command if you're using SqlDataSource) must handle a parameter with the same name. Here's an example:
UPDATE employees SET lastname=@lastname WHERE employeeid=@original_employeeid
The name format of the key parameter can be changed at will through the OldValuesParameterFormatString property. For example, a value of "{0}" assigned to the property would make the following command acceptable:
UPDATE employees SET lastname=@lastname WHERE employeeid=@employeeid
Setting the DataKeyNames property on the bound control (hold on, it's not a property on the data source control) is also the simplest way to configure a delete operation. For a delete operation, in fact, you don't need to specify a whole record with all its fields; the key is sufficient.
Note | In ASP.NET 2.0 data-bound controls such as GridView and DetailsView, the DataKeyNames property replaces DataKeyField, which we found on DataGrid and DataList controls in ASP.NET 1.x. The difference between the two lies in the fact that DataKeyNames support keys based on multiple fields. If DataKeyNames is set to multiple fields (for example, id,name), two parameters are added: original_id and original_name. |
When using ObjectDataSource with an ASP.NET 2.0 made-to-measure control (for example, GridView), most of the time the binding is totally automatic and you don't have to deal with it. If you need it, though, there's a back door you can use to take control of the update process the Updating event:
protected void Updating(object sender, ObjectDataSourceMethodEventArgs e) { Employee emp = (Employee) e.InputParameters[0]; emp.LastName = "Whosthisguy"; }
The event fires before the update operation climaxes. The InputParameters collection lists the parameters being passed to the update method. The collection is read-only, meaning that you can't add or delete elements. However, you can modify objects being transported, as the preceding code snippet demonstrates.
This technique is useful when, for whatever reasons, the ObjectDataSource control doesn't load all the data its method needs to perform the update. A similar approach can be taken for deletions and insertions as well.
Site maps are a common feature of cutting-edge Web sites. A site map is the graph that represents all the pages and directories found in a Web site. Site map information is used to show users the logical coordinates of the page they are visiting, allow users to access site locations dynamically, and render all the navigation information in a graphical fashion (as shown in Figure 9-11).
Figure 9-11: The graphical layout that the MSDN Magazine Web site uses to represent the location of a page in the site hierarchy.
ASP.NET 2.0 contains a rich navigation infrastructure that allows developers to specify the site structure. I cover site navigation in detail in Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics. For now, it suffices to say that the site map is a hierarchical piece of information that can be used as input for a hierarchical data source control such as SiteMapDataSource. The output of SiteMapDataSource can bound to hierarchical data-bound controls such as Menu.
The site map information can appear in many ways, the simplest of which is an XML file named web.sitemap located in the root of the application. To give you the essence of site maps and site map data sources, let's briefly review a few usage scenarios. Suppose you're writing a Web site and your client asks for a sequence of hyperlinks that indicate the location of the page in the site map. In ASP.NET 1.x, you have to create your own infrastructure to hold site map information and render the page location. (Typically, you would use a configuration file and a user control.) ASP.NET 2.0 provides richer support for site maps. You start by creating a configuration file named web.sitemap in the root of the Web application. The file describes the relationship between pages on the site. Your next step will depend on the expected output.
If the common representation shown in Figure 9-11 (a sequence of hyperlinks with a separator) is what you need, add a SiteMapPath control to the page. This control retrieves the site map and produces the necessary HTML markup. In this simple case, there is no need to resort to a site map data source control. If you need to build a more complex hierarchical layout for example, a tree-based representation you need the SiteMapDataSource control.
The SiteMapDataSource control pumps site map information to a hierarchical data-bound control (for example, the new TreeView control) so that it can display the site's structure. Here's a quick example:
<%@ Page Language="C#" %> <html> <body> <form runat="server"> <asp:SiteMapDataSource runat="server" /> <asp:TreeView runat="server" DataSource /> </form> </body> </html>
Figure 9-12 shows the final output as it appears to the end user.
Figure 9-12: The site map information rendered through a TreeView control.
The site map information might look like the following:
<siteMap> <siteMapNode title="Home" url="default.aspx" > <siteMapNode title="Acknowledgements" url="ack.aspx"/> <siteMapNode title="References" url="ref.aspx" /> <siteMapNode title="Samples"> <siteMapNode title="Part 1"> <siteMapNode title="Chapter 1" /> <siteMapNode title="Chapter 2" /> <siteMapNode title="Chapter 3"> <siteMapNode title="Dynamic Controls" url=" /dynctls.aspx" /> <siteMapNode title="ViewState" url=" /viewstate.aspx" /> </siteMapNode> <siteMapNode title="Chapter 4" /> </siteMapNode> <siteMapNode title="Part 2"> <siteMapNode title="Chapter 9"> <siteMapNode title="Site map" url=" /sitemapinfo.aspx" /> </siteMapNode> </siteMapNode> <siteMapNode title="Part 3" url="samples.aspx?partid=3" /> </siteMapNode> </siteMapNode> </siteMap>
Note that the url attribute is optional. If not defined, the node is intended to be an inert container and won't be made clickable.
Note | As mentioned, ASP.NET 2.0 introduces a new type of data-bound control that was completely unsupported in previous versions the hierarchical data-bound control. A new base class is defined to provide a minimum set of capabilities: HierarchicalDataBoundControl. The TreeView and Menu controls fall into this category. |
Table 9-15 details the properties available in the SiteMapDataSource class.
Property | Description |
---|---|
Provider | Indicates the site map provider object associated with the data source control. |
ShowStartingNode | True by default, indicates whether the starting node is retrieved and displayed. |
SiteMapProvider | Gets and sets the name of the site map provider associated with the instance of the control. |
StartFromCurrentNode | False by default, indicates whether the node tree is retrieved relative to the current page. |
StartingNodeOffset | Gets and sets a positive or negative offset from the starting node that determines the root hierarchy exposed by the control. Set to 0 by default. |
StartingNodeUrl | Indicates the URL in the site map in which the node tree is rooted. |
By default, the starting node is the root node of the hierarchy, but you can change the starting node through a pair of mutually exclusive properties StartFromCurrentNode and StartingNodeUrl. If you explicitly indicate the URL of the page that should appear as the root of the displayed hierarchy, make sure the StartFromCurrentNode property is false. Likewise, if you set StartFromCurrentNode to true, ensure the StartingNodeUrl property evaluates to the empty string.
Properly used, the StartingNodeOffset property lets you restrict the nodes of the site map that are actually displayed. The default value of 0 indicates that the root hierarchy exposed by the SiteMapDataSource control is the same as the starting node. A value greater than 0 goes as many levels down in the hierarchy proceeding from the root to the requested node and uses the node found as the root. Look at the sample site map we considered earlier. If you request sitemapinfo.aspx with an offset of 1, the displayed hierarchy will be rooted in the Samples node that is, one level down the real root. If you set it to 2, the effective root will be the Part 2 node. A negative offset, on the other hand, ensures that the specified number of child levels will be displayed if possible.
The SiteMapDataSource class features a couple of properties that relate to the site map provider: SiteMapProvider and Provider. The former specifies the name of the site map provider to use; the latter returns a reference to the object.
The XmlDataSource control is a special type of data source control that supports both tabular and hierarchical views of data. The tabular view of XML data is just a list of nodes at a given level of hierarchy, whereas the hierarchical view shows the complete hierarchy. An XML node is an instance of the XmlNode class; the complete hierarchy is an instance of the XmlDocument class. The XML data source supports only read-only scenarios.
Important | The XmlDataSource control is unique in that it is the only built-in data source control to implement both IDataSource and IHierarchicalDataSource interfaces. For both interfaces, though, the control doesn't go further than implementing the Select method. Hence, the XmlDataSource control is not suitable for Web applications using read/write XML data stores, as it doesn't support methods such as Delete, Insert, and Update. |
Table 9-16 details the properties of the XmlDataSource control.
Property | Description |
---|---|
CacheDuration | Indicates in seconds how long the data should be maintained in the cache. |
CacheExpirationPolicy | Indicates whether the cache duration is absolute or sliding. If absolute, data is invalidated after the specified number of seconds. If sliding, data is invalidated if not used for the specified duration. |
CacheKeyDependency | Indicates the name of a user-defined cache key that is linked to all cache entries created by the data source control. By expiring the key, you can clear the control's cache. |
Data | Contains a block of XML text for the data source control to bind. |
DataFile | Indicates the path to the file that contains data to display. |
EnableCaching | Enables or disables caching support. |
Transform | Contains a block of XSLT text that will be used to transform the XML data bound to the control. |
TransformArgumentList | A list of input parameters for the XSLT transformation to apply to the source XML. |
TransformFile | Indicates the path to the .xsl file that defines an XSLT transformation to be performed on the source XML data. |
XPath | Indicates an XPath query to be applied to the XML data. |
The XmlDataSource control can accept XML input data as a relative or absolute filename assigned to the DataFile property or as a string containing the XML content assigned to the Data property. If both properties are set, DataFile takes precedence. Note that the Data property can also be set declaratively through the <Data> tag. Furthermore, the contents assigned to Data a potentially large chunk of text are stored in the view state regardless of the caching settings you might have. If you bind the control to static text, the risk is that you move the XML data back and forth with the page view state while keeping it also stored in the cache for faster access. If you use Data and enable caching, consider disabling the view state for the control. (It should be noted, though, that disabling the view state on a control usually affects more than one property.)
If caching is enabled and you change the value of the DataFile or Data property, the cache is discarded. The DataSourceChanged event notifies pages of the event.
The XmlDataSource control is commonly bound to a hierarchical control, such as the TreeView or Menu. (These are the only two built-in hierarchical controls we have in ASP.NET 2.0, but others can be created by developers and third-party vendors.)
To understand how the XML data source works, consider a file that is a kind of XML representation of a DataSet the Employees table of Northwind:
<MyDataSet> <NorthwindEmployees> <Employee> <employeeid>1</employeeid> <lastname>Davolio</lastname> <firstname>Nancy</firstname> <title>Sales Representative</title> </Employee> ... <NorthwindEmployees> <MyDataSet>
Next you bind this file to an instance of the XmlDataSource control and the data source to a tree view:
<asp:XmlDataSource runat="server" DataFile="employees.xml" /> <asp:TreeView runat="server" DataSource> </asp:TreeView>
The result (which is not as useful as it could be) is shown in Figure 9-13.
Figure 9-13: The layout (rather than contents) of the bound XML file displayed using a TreeView control.
To display data in a way that is really helpful to users, you need to configure node bindings in the tree view:
<asp:TreeView runat="server" DataSource> <DataBindings> <asp:TreeNodeBinding Depth="3" DataMember="employeeid" TextField="#innertext" /> <asp:TreeNodeBinding Depth="3" DataMember="lastname" TextField="#innertext" /> <asp:TreeNodeBinding Depth="3" DataMember="firstname" TextField="#innertext" /> <asp:TreeNodeBinding Depth="3" DataMember="title" TextField="#innertext" /> </DataBindings> </asp:TreeView>
The <DataBindings> section of the TreeView control lets you control the layout and the contents of the tree nodes. The <TreeNodeBinding> node indicates the 0-based depth (attribute Depth) of the specified XML node (attribute DataMember), as well as which attributes determine the text displayed for the node in the tree view and value associated with the node. The TextField attribute can be set to the name of the attribute to bind or #innertext if you want to display the body of the node. Figure 9-14 provides a preview.
Figure 9-14: XML data bound to a TreeView control.
There's a lot more to know about the TreeView configuration. I delve into that in Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics.
The contents returned by the XmlDataSource control can be filtered using XPath expressions:
<asp:xmldatasource runat="server" DataFile="employees.xml" XPath="MyDataSet/NorthwindEmployees/Employee" />
The preceding expression displays only the <Employee> nodes, with no unique root node in the tree view. XPath expressions are case-sensitive.
Note | The XmlDataSource control automatically caches data as the EnableCaching property is set to true by default. Note also that by default the cache duration is set to 0, which means an infinite stay for data. In other words, the data source will cache data until the XML file that it depends on is changed. |
The XmlDataSource class can also transform its data using Extensible Stylesheet Language Transformations (XSLT). You set the transform file by using the TransformFile property or by assigning the XSLT content to the string property named Transform. Using the TransformArgumentList property, you can also pass arguments to the style sheet to be used during the XSL transformation. An XSL transformation is often used when the structure of an XML document does not match the structure needed to process the XML data. Note that once the data is transformed, the XmlDataSource becomes read-only and the data cannot be modified or saved back to the original source document.