Tutorial 1: Creating a Data Access Layer
Download the ASPNET_Data_Tutorial_1_CS.exe sample code.
Contents of Tutorial 1 (Visual C#)
Introduction
Step 1: Creating a Web Project and Connecting to the Database
Step 2: Creating the Data Access Layer
Step 3: Adding Parameterized Methods to the Data Access Layer
Step 4: Inserting, Updating, and Deleting Data
Step 5: Completing the Data Access Layer
Summary
Introduction
As Web developers, our lives revolve around working with data. We create databases to store the data,
code to retrieve and modify it, and web pages to collect and summarize it. This is the first tutorial in a
lengthy series that will explore techniques for implementing these common patterns in ASP.NET 2.0. We'll
start with creating a software architecture composed of a Data Access Layer (DAL) using Typed DataSets,
a Business Logic Layer (BLL) that enforces custom business rules, and a presentation layer composed of
ASP.NET pages that share a common page layout. Once this backend groundwork has been laid, we'll
move into reporting, showing how to display, summarize, collect, and validate data from a web
application. These tutorials are geared to be concise and provide step-by-step instructions with plenty of
screen shots to walk you through the process visually. Each tutorial is available in C# and Visual Basic
versions and includes a download of the complete code used. (This first tutorial is quite lengthy, but the
rest are presented in much more digestible chunks.)
For these tutorials we'll be using a Microsoft SQL Server 2005 Express Edition version of the Northwind
database placed in the App_Data directory. In addition to the database file, the App_Data folder also
contains the SQL scripts for creating the database, in case you want to use a different database version.
These scripts can be also be downloaded directly from Microsoft, if you'd prefer. If you use a different SQL
Server version of the Northwind database, you will need to update the NORTHWNDConnectionString
setting in the application's Web.config file. The web application was built using Visual Studio 2005
Professional Edition as a file system-based Web site project. However, all of the tutorials will work equally
well with the free version of Visual Studio 2005, Visual Web
Developerhttp://msdn.microsoft.com/vstudio/express/vwd/.
In this tutorial we'll start from the very beginning and create the Data Access Layer (DAL), followed by
creating the Business Logic Layer (BLL) in the second tutorial, and working on page layout and navigation
1
in the third. The tutorials after the third one will build upon the foundation laid in the first three. We've got
a lot to cover in this first tutorial, so fire up Visual Studio and let's get started!
Step 1: Creating a Web Project and Connecting to the Database
Before we can create our Data Access Layer (DAL), we first need to create a web site and setup our
database. Start by creating a new file system-based ASP.NET web site. To accomplish this, go to the File
menu and choose New Web Site, displaying the New Web Site dialog box. Choose the ASP.NET Web Site
template, set the Location drop-down list to File System, choose a folder to place the web site, and set the
language to C#.
Figure 1. Create a New File System-Based Web Site
This will create a new web site with a Default.aspx ASP.NET page and an App_Data folder.
With the web site created, the next step is to add a reference to the database in Visual Studio's Server
Explorer. By adding a database to the Server Explorer you can add tables, stored procedures, views, and
so on all from within Visual Studio. You can also view table data or create your own queries either by hand
or graphically via the Query Builder. Furthermore, when we build the Typed DataSets for the DAL we'll
need to point Visual Studio to the database from which the Typed DataSets should be constructed. While
we can provide this connection information at that point in time, Visual Studio automatically populates a
drop-down list of the databases already registered in the Server Explorer.
2
The steps for adding the Northwind database to the Server Explorer depend on whether you want to use
the SQL Server 2005 Express Edition database in the App_Data folder or if you have a Microsoft SQL
Server 2000 or 2005 database server setup that you want to use instead.
Using a Database in the App_Data Folder
If you do not have a SQL Server 2000 or 2005 database server to connect to, or you simply want to avoid
having to add the database to a database server, you can use the SQL Server 2005 Express Edition
version of the Northwind database that is located in the downloaded website's App_Data folder
(NORTHWND.MDF).
A database placed in the App_Data folder is automatically added to the Server Explorer. Assuming you
have SQL Server 2005 Express Edition installed on your machine you should see a node named
NORTHWND.MDF in the Server Explorer, which you can expand and explore its tables, views, stored
procedure, and so on (see Figure 2).
The App_Data folder can also hold Microsoft Access .mdb files, which, like their SQL Server
counterparts, are automatically added to the Server Explorer. If you don't want to use any of the SQL
Server options, you can always download a Microsoft Access version of the Northwind database file and
drop into the App_Data directory. Keep in mind, however, that Access databases aren't as feature-rich
as SQL Server, and aren't designed to be used in web site scenarios. Furthermore, a couple of the 35+
tutorials will utilize certain database-level features that aren't supported by Access.
Connecting to the Database in a Microsoft SQL Server 2000 or 2005 Database Server
Alternatively, you may connect to a Northwind database installed on a database server. If the database
server does not already have the Northwind database installed, you first must add it to database server by
running the installation script included in this tutorial's download or by downloading the SQL Server 2000
version of Northwind and installation script directly from Microsoft's Web site.
Once you have the database installed, go to the Server Explorer in Visual Studio, right-click on the Data
Connections node, and choose Add Connection. If you don't see the Server Explorer go to the View /
Server Explorer, or hit Ctrl+Alt+S. This will bring up the Add Connection dialog box, where you can specify
the server to connect to, the authentication information, and the database name. Once you have
successfully configured the database connection information and clicked the OK button, the database will
be added as a node underneath the Data Connections node. You can expand the database node to explore
its tables, views, stored procedures, and so on.
3
Figure 2. Add a Connection to Your Database Server's Northwind Database
Step 2: Creating the Data Access Layer
When working with data one option is to embed the data-specific logic directly into the presentation layer
(in a web application, the ASP.NET pages make up the presentation layer). This may take the form of
writing ADO.NET code in the ASP.NET page's code portion or using the SqlDataSource control from the
markup portion. In either case, this approach tightly couples the data access logic with the presentation
layer. The recommended approach, however, is to separate the data access logic from the presentation
layer. This separate layer is referred to as the Data Access Layer, DAL for short, and is typically
implemented as a separate Class Library project. The benefits of this layered architecture are well
documented (see the "Further Readings" section at the end of this tutorial for information on these
advantages) and is the approach we will take in this series.
All code that is specific to the underlying data source – such as creating a connection to the database,
issuing SELECT, INSERT, UPDATE, and DELETE commands, and so on – should be located in the DAL.
The presentation layer should not contain any references to such data access code, but should instead
make calls into the DAL for any and all data requests. Data Access Layers typically contain methods for
accessing the underlying database data. The Northwind database, for example, has Products and
Categories tables that record the products for sale and the categories to which they belong. In our
DAL we will have methods like:
• GetCategories(), which will return information about all of the categories
4
• GetProducts(), which will return information about all of the products
• GetProductsByCategoryID(categoryID), which will return all products that belong to
a specified category
• GetProductByProductID(productID), which will return information about a particular
product
These methods, when invoked, will connect to the database, issue the appropriate query, and return the
results. How we return these results is important. These methods could simply return a DataSet or
DataReader populated by the database query, but ideally these results should be returned using strongly-
typed objects. A strongly-typed object is one whose schema is rigidly defined at compile time, whereas the
opposite, a loosely-typed object, is one whose schema is not known until runtime.
For example, the DataReader and the DataSet (by default) are loosely-typed objects since their schema is
defined by the columns returned by the database query used to populate them. To access a particular
column from a loosely-typed DataTable we need to use syntax like:
DataTable.Rows[index]["columnName"]. The DataTable's loose typing in this example is
exhibited by the fact that we need to access the column name using a string or ordinal index. A strongly-
typed DataTable, on the other hand, will have each of its columns implemented as properties, resulting in
code that looks like: DataTable.Rows[index].columnName.
To return strongly-typed objects, developers can either create their own custom business objects or use
Typed DataSets. A business object is implemented by the developer as a class whose properties typically
reflect the columns of the underlying database table the business object represents. A Typed DataSet is a
class generated for you by Visual Studio based on a database schema and whose members are strongly-
typed according to this schema. The Typed DataSet itself consists of classes that extend the ADO.NET
DataSet, DataTable, and DataRow classes. In addition to strongly-typed DataTables, Typed DataSets now
also include TableAdapters, which are classes with methods for populating the DataSet's DataTables and
propagating modifications within the DataTables back to the database.
Note For more information on the advantages and disadvantages of using Typed DataSets versus custom business objects, refer to Designing Data Tier Components and Passing Data Through Tiers.
We'll use strongly-typed DataSets for these tutorials' architecture. Figure 3 illustrates the workflow
between the different layers of an application that uses Typed DataSets.
5
6
Figure 3. All Data Access Code is Relegated to the DAL
Creating a Typed DataSet and Table Adapter
To begin creating our DAL, we start by adding a Typed DataSet to our project. To accomplish this, right-
click on the project node in the Solution Explorer and choose Add a New Item. Select the DataSet option
from the list of templates and name it Northwind.xsd.
Figure 4. Choose to Add a New DataSet to Your Project
After clicking Add, when prompted to add the DataSet to the App_Code folder, choose Yes. The Designer
for the Typed DataSet will then be displayed, and the TableAdapter Configuration Wizard will start,
allowing you to add your first TableAdapter to the Typed DataSet.
A Typed DataSet serves as a strongly-typed collection of data; it is composed of strongly-typed DataTable
instances, each of which is in turn composed of strongly-typed DataRow instances. We will create a
7
strongly-typed DataTable for each of the underlying database tables that we need to work with in this
tutorials series. Let's start with creating a DataTable for the Products table.
Keep in mind that strongly-typed DataTables do not include any information on how to access data from
their underlying database table. In order to retrieve the data to populate the DataTable, we use a
TableAdapter class, which functions as our Data Access Layer. For our Products DataTable, the
TableAdapter will contain the methods – GetProducts(),
GetProductByCategoryID(categoryID), and so on – that we'll invoke from the presentation
layer. The DataTable's role is to serve as the strongly-typed objects used to pass data between the layers.
The TableAdapter Configuration Wizard begins by prompting you to select which database to work with.
The drop-down list shows those databases in the Server Explorer. If you did not add the Northwind
database to the Server Explorer, you can click the New Connection button at this time to do so.
Figure 5. Choose the Northwind Database from the Drop-Down List
After selecting the database and clicking Next, you'll be asked if you want to save the connection string in
the Web.config file. By saving the connection string you'll avoid having it hard coded in the
TableAdapter classes, which simplifies things if the connection string information changes in the future. If
you opt to save the connection string in the configuration file it's placed in the <connectionStrings>
section, which can be optionally encrypted for improved security or modified later through the new
ASP.NET 2.0 Property Page within the IIS GUI Admin Tool, which is more ideal for administrators.
Figure 6. Save the Connection String to Web.config
Next, we need to define the schema for the first strongly-typed DataTable and provide the first method for
our TableAdapter to use when populating the strongly-typed DataSet. These two steps are accomplished
simultaneously by creating a query that returns the columns from the table that we want reflected in our
DataTable. At the end of the wizard we'll give a method name to this query. Once that's been
accomplished, this method can be invoked from our presentation layer. The method will execute the
defined query and populate a strongly-typed DataTable.
To get started defining the SQL query we must first indicate how we want the TableAdapter to issue the
query. We can use an ad-hoc SQL statement, create a new stored procedure, or use an existing stored
procedure. For these tutorials we'll use ad-hoc SQL statements. Refer to Brian Noyes's article, Build a Data
Access Layer with the Visual Studio 2005 DataSet Designer for an example of using stored procedures.
8
Figure 7. Query the Data Using an Ad-Hoc SQL Statement
At this point we can type in the SQL query by hand. When creating the first method in the TableAdapter
you typically want to have the query return those columns that need to be expressed in the corresponding
DataTable. We can accomplish this by creating a query that returns all columns and all rows from the
Products table:
9
Figure 8. Enter the SQL Query Into the Textbox
Alternatively, use the Query Builder and graphically construct the query, as shown in Figure 9.
10
Figure 9. Create the Query Graphically, through the Query Editor
After creating the query, but before moving onto the next screen, click the Advanced Options button. In
Web Site Projects, "Generate Insert, Update, and Delete statements" is the only advanced option selected
by default; if you run this wizard from a Class Library or a Windows Project the "Use optimistic
concurrency" option will also be selected. Leave the "Use optimistic concurrency" option unchecked for
now. We'll examine optimistic concurrency in future tutorials.
11
Figure 10. Select Only the "Generate Insert, Update, and Delete statements" Option
After verifying the advanced options, click Next to proceed to the final screen. Here we are asked to select
which methods to add to the TableAdapter. There are two patterns for populating data:
• Fill a DataTable – with this approach a method is created that takes in a DataTable as a
parameter and populates it based on the results of the query. The ADO.NET DataAdapter class, for
example, implements this pattern with its Fill() method.
• Return a DataTable – with this approach the method creates and fills the DataTable for you and
returns it as the methods return value.
You can have the TableAdapter implement one or both of these patterns. You can also rename the
methods provided here. Let's leave both checkboxes checked, even though we'll only be using the latter
pattern throughout these tutorials. Also, let's rename the rather generic GetData method to
GetProducts.
If checked, the final checkbox, "GenerateDBDirectMethods," creates Insert(), Update(), and
Delete() methods for the TableAdapter. If you leave this option unchecked, all updates will need to be
done through the TableAdapter's sole Update() method, which takes in the Typed DataSet, a
DataTable, a single DataRow, or an array of DataRows. (If you've unchecked the "Generate Insert, Update,
and Delete statements" option from the advanced properties in Figure 9 this checkbox's setting will have
no effect.) Let's leave this checkbox selected.
12
Figure 11. Change the Method Name from GetData to GetProducts
Complete the wizard by clicking Finish. After the wizard closes we are returned to the DataSet Designer,
which shows the DataTable we just created. You can see the list of columns in the Products DataTable
(ProductID, ProductName, and so on), as well as the methods of the ProductsTableAdapter
(Fill() and GetProducts()).
13
Figure 12. The Products DataTable and ProductsTableAdapter have been Added to the
Typed DataSet
At this point we have a Typed DataSet with a single DataTable (Northwind.Products) and a
strongly-typed DataAdapter class (NorthwindTableAdapters.ProductsTableAdapter) with a
GetProducts() method. These objects can be used to access a list of all products from code like:
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new
NorthwindTableAdapters.ProductsTableAdapter();
Northwind.ProductsDataTable products;
products = productsAdapter.GetProducts();
foreach (Northwind.ProductsRow productRow in products)
Response.Write("Product: " + productRow.ProductName + "<br />");
This code did not require us to write one bit of data access-specific code. We did not have to instantiate
any ADO.NET classes, we didn't have to refer to any connection strings, SQL queries, or stored
procedures. Instead, the TableAdapter provides the low-level data access code for us.
Each object used in this example is also strongly-typed, allowing Visual Studio to provide IntelliSense and
compile-time type checking. And best of all the DataTables returned by the TableAdapter can be bound to
14
ASP.NET data Web controls, such as the GridView, DetailsView, DropDownList, CheckBoxList, and several
others. The following example illustrates binding the DataTable returned by the GetProducts()
method to a GridView in just a scant three lines of code within the Page_Load event handler.
AllProducts.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="AllProducts.aspx.cs" Inherits="AllProducts" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>View All Products in a GridView</title>
<link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
All Products</h1>
<p>
<asp:GridView ID="GridView1" runat="server" CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
</asp:GridView>
</p>
</div>
</form>
</body>
15
</html>
AllProducts.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
public partial class AllProducts : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
}
}
16
Figure 13. The List of Products is Displayed in a GridView
While this example required that we write three lines of code in our ASP.NET page's Page_Load event
handler, in future tutorials we'll examine how to use the ObjectDataSource to declaratively retrieve the
data from the DAL. With the ObjectDataSource we'll not have to write any code and will get paging and
sorting support as well!
Step 3: Adding Parameterized Methods to the Data Access Layer
At this point our ProductsTableAdapter class has but one method, GetProducts(), which
returns all of the products in the database. While being able to work with all products is definitely useful,
there are times when we'll want to retrieve information about a specific product, or all products that
belong to a particular category. To add such functionality to our Data Access Layer we can add
parameterized methods to the TableAdapter.
Let's add the GetProductsByCategoryID(categoryID) method. To add a new method to the
DAL, return to the DataSet Designer, right-click in the ProductsTableAdapter section, and choose
Add Query.
17
Figure 14. Right-Click the TableAdapter and Choose Add Query
We are first prompted about whether we want to access the database using an ad-hoc SQL statement or a
new or existing stored procedure. Let's choose to use an ad-hoc SQL statement again. Next, we are asked
what type of SQL query we'd like to use. Since we want to return all products that belong to a specified
category, we want to write a SELECT statement which returns rows.
18
Figure 15. Choose to Create a SELECT Statement Which Returns Rows
The next step is to define the SQL query used to access the data. Since we want to return only those
products that belong to a particular category, I use the same SELECT statement from
GetProducts(), but add the following WHERE clause: WHERE CategoryID = @CategoryID.
The @CategoryID parameter indicates to the TableAdapter wizard that the method we're creating will
require an input parameter of the corresponding type (namely, a nullable integer).
19
Figure 16. Enter a Query to Only Return Products in a Specified Category
In the final step we can choose which data access patterns to use, as well as customize the names of the
methods generated. For the Fill pattern, let's change the name to FillByCategoryID and for the
return a DataTable return pattern (the GetX methods), let's use GetProductsByCategoryID.
20
21
Figure 17. Choose the Names for the TableAdapter Methods
After completing the wizard, the DataSet Designer includes the new TableAdapter methods.
Figure 18. The Products Can Now be Queried by Category
Take a moment to add a GetProductByProductID(productID) method using the same
technique.
These parameterized queries can be tested directly from the DataSet Designer. Right-click on the method
in the TableAdapter and choose Preview Data. Next, enter the values to use for the parameters and click
Preview.
Figure 19. Those Products Belonging to the Beverages Category are Shown
With the GetProductsByCategoryID(categoryID) method in our DAL, we can now create an
ASP.NET page that displays only those products in a specified category. The following example shows all
products that are in the Beverages category, which have a CategoryID of 1.
Beverages.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Beverages.aspx.cs" Inherits="Beverages" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
22
<head runat="server">
<title>Untitled Page</title>
<link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>Beverages</h1>
<p>
<asp:GridView ID="GridView1" runat="server" CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
</asp:GridView>
</p>
</div>
</form>
</body>
</html>
Beverages.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
23
24
using NorthwindTableAdapters;
public partial class Beverages : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProductsByCategoryID(1);
GridView1.DataBind();
}
}
Figure 20. Those Products in the Beverages Category are Displayed
Step 4: Inserting, Updating, and Deleting Data
There are two patterns commonly used for inserting, updating, and deleting data. The first pattern, which
I'll call the database direct pattern, involves creating methods that, when invoked, issue an INSERT,
UPDATE, or DELETE command to the database that operates on a single database record. Such methods
25
are typically passed in a series of scalar values (integers, strings, Booleans, DateTimes, and so on) that
correspond to the values to insert, update, or delete. For example, with this pattern for the Products
table the delete method would take in an integer parameter, indicating the ProductID of the record to
delete, while the insert method would take in a string for the ProductName, a decimal for the
UnitPrice, an integer for the UnitsOnStock, and so on.
Figure 21. Each Insert, Update, and Delete Request Is Sent to the Database Immediately
The other pattern, which I'll refer to as the batch update pattern, is to update an entire DataSet,
DataTable, or collection of DataRows in one method call. With this pattern a developer deletes, inserts,
and modifies the DataRows in a DataTable and then passes those DataRows or DataTable into an update
method. This method then enumerates the DataRows passed in, determines whether or not they've been
modified, added, or deleted (via the DataRow's RowState property value), and issues the appropriate
database request for each record.
Figure 22. All Changes are Synchronized with the Database When the Update Method is Invoked
26
The TableAdapter uses the batch update pattern by default, but also supports the DB direct pattern. Since
we selected the "Generate Insert, Update, and Delete statements" option from the Advanced Properties
when creating our TableAdapter, the ProductsTableAdapter contains an Update() method, which
implements the batch update pattern. Specifically, the TableAdapter contains an Update() method that
can be passed the Typed DataSet, a strongly-typed DataTable, or one or more DataRows. If you left the
"GenerateDBDirectMethods" checkbox checked when first creating the TableAdapter the DB direct pattern
will also be implemented via Insert(), Update(), and Delete() methods.
Both data modification patterns use the TableAdapter's InsertCommand, UpdateCommand, and
DeleteCommand properties to issue their INSERT, UPDATE, and DELETE commands to the database.
You can inspect and modify the InsertCommand, UpdateCommand, and DeleteCommand
properties by clicking on the TableAdapter in the DataSet Designer and then going to the Properties
window. (Make sure you have selected the TableAdapter, and that the ProductsTableAdapter
object is the one selected in the drop-down list in the Properties window.)
Figure 23. The TableAdapter has InsertCommand, UpdateCommand, and DeleteCommand
Properties
To examine or modify any of these database command properties, click on the CommandText
subproperty, which will bring up the Query Builder.
Figure 24. Configure the INSERT, UPDATE, and DELETE Statements in the Query Builder
The following code example shows how to use the batch update pattern to double the price of all products
that are not discontinued and that have 25 units in stock or less:
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new
NorthwindTableAdapters.ProductsTableAdapter();
// For each product, double its price if it is not discontinued and
// there are 25 items in stock or less
Northwind.ProductsDataTable products = productsAdapter.GetProducts();
foreach (Northwind.ProductsRow product in products)
if (!product.Discontinued && product.UnitsInStock <= 25)
product.UnitPrice *= 2;
// Update the products
27
productsAdapter.Update(products);
The code below illustrates how to use the DB direct pattern to programmatically delete a particular
product, then update one, and then add a new one:
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter();
// Delete the product with ProductID 3
productsAdapter.Delete(3);
// Update Chai (ProductID of 1), setting the UnitsOnOrder to 15
productsAdapter.Update("Chai", 1, 1, "10 boxes x 20 bags", 18.0m, 39,
15, 10, false, 1);
// Add a new product
productsAdapter.Insert("New Product", 1, 1, "12 tins per carton",
14.95m, 15, 0, 10, false);
Creating Custom Insert, Update, and Delete Methods
The Insert(), Update(), and Delete() methods created by the DB direct method can be a bit
cumbersome, especially for tables with many columns. Looking at the previous code example, without
IntelliSense's help it's not particularly clear what Products table column maps to each input parameter
to the Update() and Insert() methods. There may be times when we only want to update a single
column or two, or want a customized Insert() method that will, perhaps, return the value of the newly
inserted record's IDENTITY (auto-increment) field.
To create such a custom method, return to the DataSet Designer. Right-click on the TableAdapter and
choose Add Query, returning to the TableAdapter wizard. On the second screen we can indicate the type of
query to create. Let's create a method that adds a new product and then returns the value of the newly
added record's ProductID. Therefore, opt to create an INSERT query.
28
Figure 25. Create a Method to Add a New Row to the Products Table
On the next screen the InsertCommand's CommandText appears. Augment this query by adding
SELECT SCOPE_IDENTITY() at the end of the query, which will return the last identity value inserted
into an IDENTITY column in the same scope. (See the technical documentation for more information
about SCOPE_IDENTITY() and why you probably want to use SCOPE_IDENTITY() in lieu of
@@IDENTITY.) Make sure that you end the INSERT statement with a semi-colon before adding the
SELECT statement.
29
Figure 26. Augment the Query to Return the SCOPE_IDENTITY() Value
Finally, name the new method InsertProduct.
30
Figure 27. Set the New Method Name to InsertProduct
When you return to the DataSet Designer you'll see that the ProductsTableAdapter contains a new
method, InsertProduct. If this new method doesn't have a parameter for each column in the
Products table, chances are you forgot to terminate the INSERT statement with a semi-colon.
Configure the InsertProduct method and ensure you have a semi-colon delimiting the INSERT and
SELECT statements.
By default, insert methods issue non-query methods, meaning that they return the number of affected
rows. However, we want the InsertProduct method to return the value returned by the query, not
the number of rows affected. To accomplish this, adjust the InsertProduct method's ExecuteMode
property to Scalar.
31
Figure 28. Change the ExecuteMode Property to Scalar
The following code shows this new InsertProduct method in action:
NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter();
// Add a new product
int new_productID = Convert.ToInt32(productsAdapter.InsertProduct("New Product", 1, 1, "12 tins per carton", 14.95m, 10, 0, 10, false));
// On second thought, delete the product
productsAdapter.Delete(new_productID);
Step 5: Completing the Data Access Layer
Note that the ProductsTableAdapters class returns the CategoryID and SupplierID values
from the Products table, but doesn't include the CategoryName column from the Categories
table or the CompanyName column from the Suppliers table, although these are likely the columns
we want to display when showing product information. We can augment the TableAdapter's initial method,
GetProducts(), to include both the CategoryName and CompanyName column values, which will
update the strongly-typed DataTable to include these new columns as well.
32
33
This can present a problem, however, as the TableAdapter's methods for inserting, updating, and deleting
data are based off of this initial method. Fortunately, the auto-generated methods for inserting, updating,
and deleting are not affected by subqueries in the SELECT clause. By taking care to add our queries to
Categories and Suppliers as subqueries, rather than JOINs, we'll avoid having to rework those
methods for modifying data. Right-click on the GetProducts() method in the
ProductsTableAdapter and choose Configure. Then, adjust the SELECT clause so that it looks like:
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories WHERE Categories.CategoryID =
Products.ProductID) as CategoryName, (SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID) as SupplierName
FROM Products
Figure 29. Update the SELECT Statement for the GetProducts() Method
After updating the GetProducts() method to use this new query the DataTable will include two new
columns: CategoryName and SupplierName.
Figure 30. The Products DataTable has Two New Columns
Take a moment to update the SELECT clause in the GetProductsByCategoryID(categoryID)
method as well.
If you update the GetProducts() SELECT using JOIN syntax the DataSet Designer won't be able to
auto-generate the methods for inserting, updating, and deleting database data using the DB direct
pattern. Instead, you'll have to manually create them much like we did with the InsertProduct
method earlier in this tutorial. Furthermore, you'll manually have to provide the InsertCommand,
UpdateCommand, and DeleteCommand property values if you want to use the batch updating
pattern.
Adding the Remaining TableAdapters
Up until now, we've only looked at working with a single TableAdapter for a single database table.
However, the Northwind database contains several related tables that we'll need to work with in our web
application. A Typed DataSet can contain multiple, related DataTables. Therefore, to complete our DAL we
need to add DataTables for the other tables we'll be using in these tutorials. To add a new TableAdapter to
a Typed DataSet, open the DataSet Designer, right-click in the Designer, and choose Add / TableAdapter.
This will create a new DataTable and TableAdapter and walk you through the wizard we examined earlier
in this tutorial.
Take a few minutes to create the following TableAdapters and methods using the following queries. Note
that the queries in the ProductsTableAdapter include the subqueries to grab each product's
category and supplier names. Additionally, if you've been following along, you've already added the
34
ProductsTableAdapter class's GetProducts() and
GetProductsByCategoryID(categoryID) methods.
ProductsTableAdapter
GetProducts:
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,
Discontinued , (SELECT CategoryName FROM Categories WHERE
Categories.CategoryID = Products.ProductID) as CategoryName, (SELECT
CompanyName FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
GetProductsByCategoryID:
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,
Discontinued , (SELECT CategoryName FROM Categories WHERE
Categories.CategoryID = Products.ProductID) as CategoryName, (SELECT
CompanyName FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
WHERE CategoryID = @CategoryID
GetProductsBySupplierID
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,
Discontinued , (SELECT CategoryName FROM Categories WHERE
Categories.CategoryID = Products.ProductID) as CategoryName, (SELECT
CompanyName FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
WHERE SupplierID = @SupplierID
35
GetProductByProductID
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,
Discontinued , (SELECT CategoryName FROM Categories WHERE
Categories.CategoryID = Products.ProductID) as CategoryName, (SELECT
CompanyName FROM Suppliers WHERE Suppliers.SupplierID =
Products.SupplierID) as SupplierName
FROM Products
WHERE ProductID = @ProductID
CategoriesTableAdapter
GetCategories
SELECT CategoryID, CategoryName, Description
FROM Categories
GetCategoryByCategoryID
SELECT CategoryID, CategoryName, Description
FROM Categories
WHERE CategoryID = @CategoryID
SuppliersTableAdapter
GetSuppliers
SELECT SupplierID, CompanyName, Address, City, Country, Phone
FROM Suppliers
GetSuppliersByCountry
SELECT SupplierID, CompanyName, Address, City, Country, Phone
FROM Suppliers
WHERE Country = @Country
GetSupplierBySupplierID
SELECT SupplierID, CompanyName, Address, City, Country, Phone
FROM Suppliers
36
WHERE SupplierID = @SupplierID
EmployeesTableAdapter
GetEmployees
SELECT EmployeeID, LastName, FirstName, Title, HireDate, ReportsTo, Country
FROM Employees
GetEmployeesByManager
SELECT EmployeeID, LastName, FirstName, Title, HireDate, ReportsTo, Country
FROM Employees
WHERE ReportsTo = @ManagerID
GetEmployeeByEmployeeID
SELECT EmployeeID, LastName, FirstName, Title, HireDate, ReportsTo, Country
FROM Employees
WHERE EmployeeID = @EmployeeID
Figure 31. The DataSet Designer After the Four TableAdapters Have Been Added
Adding Custom Code to the DAL
The TableAdapters and DataTables added to the Typed DataSet are expressed as an XML Schema
Definition file (Northwind.xsd). You can view this schema information by right-clicking on the
Northwind.xsd file in the Solution Explorer and choosing View Code.
37
Figure 32. The XML Schema Definition (XSD) File for the Northwinds Typed DataSet
This schema information is translated into C# or Visual Basic code at design time when compiled or at
runtime (if needed), at which point you can step through it with the debugger. To view this auto-generated
code go to the Class View and drill down to the TableAdapter or Typed DataSet classes. If you don't see
the Class View on your screen, go to the View menu and select it from there, or hit Ctrl+Shift+C. From the
Class View you can see the properties, methods, and events of the Typed DataSet and TableAdapter
classes. To view the code for a particular method, double-click the method name in the Class View or
right-click on it and choose Go To Definition.
38
39
Figure 33. Inspect the Auto-Generated Code by Selecting Go To Definition from the Class View
While auto-generated code can be a great time saver, the code is often very generic and needs to be
customized to meet the unique needs of an application. The risk of extending auto-generated code,
though, is that the tool that generated the code might decide it's time to "regenerate" and overwrite your
customizations. With .NET 2.0's new partial class concept, it's easy to split a class across multiple files.
This enables us to add our own methods, properties, and events to the auto-generated classes without
having to worry about Visual Studio overwriting our customizations.
To demonstrate how to customize the DAL, let's add a GetProducts() method to the
SuppliersRow class. The SuppliersRow class represents a single record in the Suppliers table;
each supplier can provider zero to many products, so GetProducts() will return those products of the
specified supplier. To accomplish this create a new class file in the App_Code folder named
SuppliersRow.cs and add the following code:
using System;
using System.Data;
using NorthwindTableAdapters;
public partial class Northwind
{
public partial class SuppliersRow
{
public Northwind.ProductsDataTable GetProducts()
{
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
return productsAdapter.GetProductsBySupplierID(this.SupplierID);
}
}
}
This partial class instructs the compiler that when building the Northwind.SuppliersRow class to
include the GetProducts() method we just defined. If you build your project and then return to the
Class View you'll see GetProducts() now listed as a method of Northwind.SuppliersRow.
40
Figure 34. The GetProducts() Method Is Now Part of the Northwind.SuppliersRow Class
The GetProducts() method can now be used to enumerate the set of products for a particular
supplier, as the following code shows:
NorthwindTableAdapters.SuppliersTableAdapter suppliersAdapter = new
NorthwindTableAdapters.SuppliersTableAdapter();
// Get all of the suppliers
Northwind.SuppliersDataTable suppliers = suppliersAdapter.GetSuppliers();
// Enumerate the suppliers
foreach (Northwind.SuppliersRow supplier in suppliers)
{
Response.Write("Supplier: " + supplier.CompanyName);
Response.Write("<ul>");
// List the products for this supplier
41
Northwind.ProductsDataTable products = supplier.GetProducts();
foreach (Northwind.ProductsRow product in products)
Response.Write("<li>" + product.ProductName + "</li>");
Response.Write("</ul><p> </p>");
}
This data can also be displayed in any of ASP.NET's data Web controls. The following page uses a GridView
control with two fields:
• A BoundField that displays the name of each supplier, and
• A TemplateField that contains a BulletedList control that is bound to the results returned by the
GetProducts() method for each supplier.
We'll examine how to display such master-detail reports in future tutorials. For now, this example is
designed to illustrate using the custom method added to the Northwind.SuppliersRow class.
SuppliersAndProducts.aspx
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="SuppliersAndProducts.aspx.cs" Inherits="SuppliersAndProducts" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
Suppliers and Their Products</h1>
42
<p>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
<Columns>
<asp:BoundField DataField="CompanyName" HeaderText="Supplier" />
<asp:TemplateField HeaderText="Products">
<ItemTemplate>
<asp:BulletedList ID="BulletedList1" runat="server" DataSource='<%# ((Northwind.SuppliersRow)((System.Data.DataRowView) Container.DataItem).Row).GetProducts() %>'
DataTextField="ProductName">
</asp:BulletedList>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</p>
</div>
</form>
</body>
</html>
SuppliersAndProducts.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
43
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
public partial class SuppliersAndProducts : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
SuppliersTableAdapter suppliersAdapter = new SuppliersTableAdapter();
GridView1.DataSource = suppliersAdapter.GetSuppliers();
GridView1.DataBind();
}
}
44
Figure 35. The Supplier's Company Name Is Listed in the Left Column, Their Products in the
Right
Summary
When building a web application creating the DAL should be one of your first steps, occurring before you
start creating your presentation layer. With Visual Studio, creating a DAL based on Typed DataSets is a
task that can be accomplished in 10-15 minutes without writing a line of code. The tutorials moving
forward will build upon this DAL. In the next tutorial we'll define a number of business rules and see how
to implement them in a separate Business Logic Layer.
Happy Programming!
Tutorial 2: Creating a Business Logic Layer
45
Contents of Tutorial 2 (Visual C#)
Introduction
Step 1: Creating the BLL Classes
Step 2: Accessing the Typed DataSets Through the BLL Classes
Step 3: Adding Field-Level Validation to the DataRow Classes
Step 4: Adding Custom Business Rules to the BLL's Classes
Summary
Introduction
The Data Access Layer (DAL) created in the first tutorial cleanly separates the data access logic from the
presentation logic. However, while the DAL cleanly separates the data access details from the presentation
layer, it does not enforce any business rules that may apply. For example, for our application we may want
to disallow the CategoryID or SupplierID fields of the Products table to be modified when the
Discontinued field is set to 1, or we might want to enforce seniority rules, prohibiting situations in
which an employee is managed by someone who was hired after them. Another common scenario is
authorization – perhaps only users in a particular role can delete products or can change the UnitPrice
value.
In this tutorial we'll see how to centralize these business rules into a Business Logic Layer (BLL) that
serves as an intermediary for data exchange between the presentation layer and the DAL. In a real-world
application, the BLL should be implemented as a separate Class Library project; however, for these
tutorials we'll implement the BLL as a series of classes in our App_Code folder in order to simplify the
project structure. Figure 1 illustrates the architectural relationships among the presentation layer, BLL,
and DAL.
46
Figure 1. The BLL Separates the Presentation Layer from the Data Access Layer and Imposes
Business Rules
Step 1: Creating the BLL Classes
Our BLL will be composed of four classes, one for each TableAdapter in the DAL; each of these BLL classes
will have methods for retrieving, inserting, updating, and deleting from the respective TableAdapter in the
DAL, applying the appropriate business rules.
To more cleanly separate the DAL- and BLL-related classes, let's create two subfolders in the App_Code
folder, DAL and BLL. Simply right-click on the App_Code folder in the Solution Explorer and choose New
Folder. After creating these two folders, move the Typed DataSet created in the first tutorial into the DAL
subfolder.
Next, create the four BLL class files in the BLL subfolder. To accomplish this, right-click on the BLL
subfolder, choose Add a New Item, and choose the Class template. Name the four classes
ProductsBLL, CategoriesBLL, SuppliersBLL, and EmployeesBLL.
47
Figure 2. Add Four New Classes to the App_Code Folder
Next, let's add methods to each of the classes to simply wrap the methods defined for the TableAdapters
from the first tutorial. For now, these methods will just call directly into the DAL; we'll return later to add
any needed business logic.
Note If you are using Visual Studio Standard Edition or above (that is, you're not using Visual Web Developer), you can optionally design your classes visually using the Class Designer. Refer to the Class Designer Blog for more information on this new feature in Visual Studio.
For the ProductsBLL class we need to add a total of seven methods:
• GetProducts() – returns all products
• GetProductByProductID(productID) – returns the product with the specified product
ID
• GetProductsByCategoryID(categoryID) – returns all products from the specified
category
• GetProductsBySupplier(supplierID) – returns all products from the specified supplier
• AddProduct(productName, supplierID, categoryID, quantityPerUnit,
unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued) –
inserts a new product into the database using the values passed-in; returns the ProductID value of
the newly inserted record
• UpdateProduct(productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
discontinued, productID) – updates an existing product in the database using the passed-in
values; returns true if precisely one row was updated, false otherwise
• DeleteProduct(productID) – deletes the specified product from the database
48
ProductsBLL.cs
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsBLL
{
private ProductsTableAdapter _productsAdapter = null;
protected ProductsTableAdapter Adapter
{
get {
if (_productsAdapter == null)
_productsAdapter = new ProductsTableAdapter();
return _productsAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, true)]
public Northwind.ProductsDataTable GetProducts()
49
{
return Adapter.GetProducts();
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductByProductID(int productID)
{
return Adapter.GetProductByProductID(productID);
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
{
return Adapter.GetProductsByCategoryID(categoryID);
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
{
return Adapter.GetProductsBySupplierID(supplierID);
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Insert, true)]
public bool AddProduct(string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder, short? reorderLevel,
bool discontinued)
50
{
// Create a new ProductRow instance
Northwind.ProductsDataTable products = new Northwind.ProductsDataTable();
Northwind.ProductsRow product = products.NewProductsRow();
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull(); else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull(); else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull(); else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull(); else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull(); else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull(); else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Add the new product
products.AddProductsRow(product);
int rowsAffected = Adapter.Update(products);
// Return true if precisely one row was inserted, otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update, true)]
51
public bool UpdateProduct(string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder, short? reorderLevel,
bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull(); else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull(); else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull(); else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull(); else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull(); else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull(); else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
52
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct(int productID)
{
int rowsAffected = Adapter.Delete(productID);
// Return true if precisely one row was deleted, otherwise false
return rowsAffected == 1;
}
}
The methods that simply return data – GetProducts, GetProductByProductID,
GetProductsByCategoryID, and GetProductBySuppliersID – are fairly straightforward as
they simply call down into the DAL. While in some scenarios there may be business rules that need to be
implemented at this level (such as authorization rules based on the currently logged on user or the role to
which the user belongs), we'll simply leave these methods as-is. For these methods, then, the BLL serves
merely as a proxy through which the presentation layer accesses the underlying data from the Data Access
Layer.
The AddProduct and UpdateProduct methods both take in as parameters the values for the various
product fields and add a new product or update an existing one, respectively. Since many of the Product
table's columns can accept NULL values (CategoryID, SupplierID, and UnitPrice, to name a
few), those input parameters for AddProduct and UpdateProduct that map to such columns use
use nullable types. Nullable types are new to .NET 2.0 and provide a technique for indicating whether a
value type should, instead, be null. In C# you can flag a value type as a nullable type by adding ? after
the type (like int? x;). Refer to the Nullable Types section in the C# Programming Guide for more
information.
All three methods return a Boolean value indicating whether a row was inserted, updated, or deleted since
the operation may not result in an affected row. For example, if the page developer calls
DeleteProduct passing in a ProductID for a non-existent product, the DELETE statement issued
to the database will have no affect and therefore the DeleteProduct method will return false.
53
Note that when adding a new product or updating an existing one we take in the new or modified product's
field values as a list of scalars as opposed to accepting a ProductsRow instance. This approach was
chosen because the ProductsRow class derives from the ADO.NET DataRow class, which doesn't have
a default parameterless constructor. In order to create a new ProductsRow instance, we must first
create a ProductsDataTable instance and then invoke its NewProductRow() method (which we
do in AddProduct). This shortcoming rears its head when we turn to inserting and updating products
using the ObjectDataSource. In short, the ObjectDataSource will try to create an instance of the input
parameters. If the BLL method expects a ProductsRow instance, the ObjectDataSource will try to
create one, but fail due to the lack of a default parameterless constructor. For more information on this
problem, refer to the following two ASP.NET Forums posts: Updating ObjectDataSources with Strongly-
Typed DataSets and Problem With ObjectDataSource and Strongly-Typed DataSet.
Next, in both AddProduct and UpdateProduct, the code creates a ProductsRow instance and
populates it with the values just passed in. When assigning values to DataColumns of a DataRow various
field-level validation checks can occur. Therefore, manually putting the passed in values back into a
DataRow helps ensure the validity of the data being passed to the BLL method. Unfortunately the strongly-
typed DataRow classes generated by Visual Studio do not use nullable types. Rather, to indicate that a
particular DataColumn in a DataRow should correspond to a NULL database value we must use the
SetColumnNameNull() method.
In UpdateProduct we first load in the product to update using
GetProductByProductID(productID). While this may seem like an unnecessary trip to the
database, this extra trip will prove worthwhile in future tutorials that explore optimistic concurrency.
Optimistic concurrency is a technique to ensure that two users who are simultaneously working on the
same data don't accidentally overwrite one another's changes. Grabbing the entire record also makes it
easier to create update methods in the BLL that only modify a subset of the DataRow's columns. When we
explore the SuppliersBLL class we'll see such an example.
Finally, note that the ProductsBLL class has the DataObject attribute applied to it (the
[System.ComponentModel.DataObject] syntax right before the class statement near the top of
the file) and the methods have DataObjectMethodAttribute attributes. The DataObject attribute marks
the class as being an object suitable for binding to an ObjectDataSource control, whereas the
DataObjectMethodAttribute indicates the purpose of the method. As we'll see in future tutorials,
ASP.NET 2.0's ObjectDataSource makes it easy to declaratively access data from a class. To help filter the
list of possible classes to bind to in the ObjectDataSource's wizard, by default only those classes marked
as DataObjects are shown in the wizard's drop-down list. The ProductsBLL class will work just as
well without these attributes, but adding them makes it easier to work with in the ObjectDataSource's
wizard.
Adding the Other Classes
54
With the ProductsBLL class complete, we still need to add the classes for working with categories,
suppliers, and employees. Take a moment to create the following classes and methods using the concepts
from the example above:
• CategoriesBLL.cs
• GetCategories()
• GetCategoryByCategoryID(categoryID)
• SuppliersBLL.cs
• GetSuppliers()
• GetSupplierBySupplierID(supplierID)
• GetSuppliersByCountry(country)
• UpdateSupplierAddress(supplierID, address, city, country)
• EmployeesBLL.cs
• GetEmployees()
• GetEmployeeByEmployeeID(employeeID)
• GetEmployeesByManager(managerID)
The one method worth noting is the SuppliersBLL class's UpdateSupplierAddress method. This
method provides an interface for updating just the supplier's address information. Internally, this method
reads in the SupplierDataRow object for the specified supplierID (using
GetSupplierBySupplierID), sets its address-related properties, and then calls down into the
SupplierDataTable's Update method. The UpdateSupplierAddress method follows:
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateSupplierAddress(int supplierID, string address, string city, string country)
{
Northwind.SuppliersDataTable suppliers = Adapter.GetSupplierBySupplierID(supplierID);
if (suppliers.Count == 0)
// no matching record found, return false
return false;
else
{
55
Northwind.SuppliersRow supplier = suppliers[0];
if (address == null) supplier.SetAddressNull(); else supplier.Address = address;
if (city == null) supplier.SetCityNull(); else supplier.City = city;
if (country == null) supplier.SetCountryNull(); else supplier.Country = country;
// Update the supplier Address-related information
int rowsAffected = Adapter.Update(supplier);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
}
Refer to this article's download for my complete implementation of the BLL classes.
Step 2: Accessing the Typed DataSets Through the BLL Classes
In the first tutorial we saw examples of working directly with the Typed DataSet programmatically, but
with the addition of our BLL classes, the presentation tier should work against the BLL instead. In the
AllProducts.aspx example from the first tutorial, the ProductsTableAdapter was used to bind
the list of products to a GridView, as shown in the following code:
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
To use the new BLL classes, all that needs to be changed is the first line of code – simply replace the
ProductsTableAdapter object with a ProductBLL object:
ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();
The BLL classes can also be accessed declaratively (as can the Typed DataSet) by using the
ObjectDataSource. We'll be discussing the ObjectDataSource in greater detail in the following tutorials.
56
Figure 3. The List of Products Is Displayed in a GridView
Step 3: Adding Field-Level Validation to the DataRow Classes
Field-level validation are checks that pertains to the property values of the business objects when inserting
or updating. Some field-level validation rules for products include:
• The ProductName field must be 40 characters or less in length
• The QuantityPerUnit field must be 20 characters or less in length
• The ProductID, ProductName, and Discontinued fields are required, but all other fields
are optional
• The UnitPrice, UnitsInStock, UnitsOnOrder, and ReorderLevel fields must be
greater than or equal to zero
These rules can and should be expressed at the database level. The character limit on the ProductName
and QuantityPerUnit fields are captured by the data types of those columns in the Products table
(nvarchar(40) and nvarchar(20), respectively). Whether fields are required and optional are
expressed by if the database table column allows NULLs. Four check constraints exist that ensure that
only values greater than or equal to zero can make it into the UnitPrice, UnitsInStock,
UnitsOnOrder, or ReorderLevel columns.
In addition to enforcing these rules at the database they should also be enforced at the DataSet level. In
fact, the field length and whether a value is required or optional are already captured for each DataTable's
57
set of DataColumns. To see the existing field-level validation automatically provided, go to the DataSet
Designer, select a field from one of the DataTables and then go to the Properties window. As Figure 4
shows, the QuantityPerUnit DataColumn in the ProductsDataTable has a maximum length of
20 characters and does allow NULL values. If we attempt to set the ProductsDataRow's
QuantityPerUnit property to a string value longer than 20 characters an ArgumentException
will be thrown.
Figure 4. The DataColumn Provides Basic Field-Level Validation
Unfortunately, we can't specify bounds checks, such as the UnitPrice value must be greater than or
equal to zero, through the Properties window. In order to provide this type of field-level validation we need
to create an event handler for the DataTable's ColumnChanging Event. As mentioned in the preceding
tutorial, the DataSet, DataTables, and DataRow objects created by the Typed DataSet can be extended
through the use of partial classes. Using this technique we can create a ColumnChanging event handler
for the ProductsDataTable class. Start by creating a class in the App_Code folder named
ProductsDataTable.ColumnChanging.cs.
58
Figure 5. Add a New Class to the App_Code Folder
Next, create an event handler for the ColumnChanging event that ensures that the UnitPrice,
UnitsInStock, UnitsOnOrder, and ReorderLevel column values (if not NULL) are greater than
or equal to zero. If any such column is out of range, throw an ArgumentException.
ProductsDataTable.ColumnChanging.cs
public partial class Northwind
{
public partial class ProductsDataTable
{
public override void BeginInit()
{
this.ColumnChanging += ValidateColumn;
}
void ValidateColumn(object sender, DataColumnChangeEventArgs e)
{
if(e.Column.Equals(this.UnitPriceColumn))
59
{
if(!Convert.IsDBNull(e.ProposedValue) && (decimal)e.ProposedValue < 0)
{
throw new ArgumentException("UnitPrice cannot be less than zero", "UnitPrice");
}
}
else if (e.Column.Equals(this.UnitsInStockColumn) ||
e.Column.Equals(this.UnitsOnOrderColumn) ||
e.Column.Equals(this.ReorderLevelColumn))
{
if (!Convert.IsDBNull(e.ProposedValue) && (short)e.ProposedValue < 0)
{
throw new ArgumentException(string.Format("{0} cannot be less than zero", e.Column.ColumnName), e.Column.ColumnName);
}
}
}
}
}
Step 4: Adding Custom Business Rules to the BLL's Classes
In addition to field-level validation, there may be high-level custom business rules that involve different
entities or concepts not expressible at the single column level, such as:
• If a product is discontinued, its UnitPrice cannot be updated
• An employee's country of residence must be the same as their manager's country of residence
• A product cannot be discontinued if it is the only product provided by the supplier
The BLL classes should contain checks to ensure adherence to the application's business rules. These
checks can be added directly to the methods to which they apply.
Imagine that our business rules dictate that a product could not be marked discontinued if it was the only
product from a given supplier. That is, if product X was the only product we purchased from supplier Y, we
60
could not mark X as discontinued; if, however, supplier Y supplied us with three products, A, B, and C,
then we could mark any and all of these as discontinued. An odd business rule, but business rules and
common sense aren't always aligned!
To enforce this business rule in the UpdateProducts method we'd start by checking if
Discontinued was set to true and, if so, we'd call GetProductsBySupplierID to determine
how many products we purchased from this product's supplier. If only one product is purchased from this
supplier, we throw an ApplicationException.
public bool UpdateProduct(string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder, short? reorderLevel,
bool discontinued, int productID)
{
Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
if (products.Count == 0)
// no matching record found, return false
return false;
Northwind.ProductsRow product = products[0];
// Business rule check - cannot discontinue a product that's supplied by only
// one supplier
if (discontinued)
{
// Get the products we buy from this supplier
Northwind.ProductsDataTable productsBySupplier = Adapter.GetProductsBySupplierID(product.SupplierID);
if (productsBySupplier.Count == 1)
// this is the only product we buy from this supplier
61
throw new ApplicationException("You cannot mark a product as discontinued if its the only product purchased from a supplier");
}
product.ProductName = productName;
if (supplierID == null) product.SetSupplierIDNull(); else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull(); else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull(); else product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null) product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value;
if (unitsInStock == null) product.SetUnitsInStockNull(); else product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null) product.SetUnitsOnOrderNull(); else product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null) product.SetReorderLevelNull(); else product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
// Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
Responding to Validation Errors in the Presentation Tier
When calling the BLL from the presentation tier we can decide whether to attempt to handle any
exceptions that might be raised or let them bubble up to ASP.NET (which will raise the
HttpApplication's Error event). To handle an exception when working with the BLL
programmatically, we can use a Try...Catch block, as the following example shows:
ProductsBLL productLogic = new ProductsBLL();
62
// Update ProductID 1's information
try
{
// This will fail since we're attempting to use a
// UnitPrice value less than 0.
productLogic.UpdateProduct("Scott's Tea", 1, 1, null, -14m, 10, null, null, false, 1);
}
catch (ArgumentException ae)
{
Response.Write("There was a problem: " + ae.Message);
}
As we'll see in future tutorials, handling exceptions that bubble up from the BLL when using a data Web
control for inserting, updating, or deleting data can be handled directly in an event handler as opposed to
having to wrap code in try...catch blocks.
Summary
A well-architected application is crafted into distinct layers, each of which encapsulates a particular role. In
the first tutorial of this article series we created a Data Access Layer using Typed DataSets; in this tutorial
we built a Business Logic Layer as a series of classes in our application's App_Code folder that call down
into our DAL. The BLL implements the field-level and business-level logic for our application. In addition to
creating a separate BLL, as we did in this tutorial, another option is to extend the TableAdapters' methods
through the use of partial classes. However, using this technique does not allow us to override existing
methods nor does it separate our DAL and our BLL as cleanly as the approach we've taken in this article.
With the DAL and BLL complete, we're ready to start on our presentation layer. In the next tutorial we'll
take a brief detour from data access topics and define a consistent page layout for use throughout the
tutorials.
Happy Programming!
Tutorial 3: Master Pages and Site Navigation
Scott Mitchell
63
June 2006
Download the ASPNET_Data_Tutorial_3_CS.exe sample code.
Contents of Tutorial 3 (Visual C#)
Introduction Step 1: Creating the Master Page Step 2: Adding a Homepage to the Web Site Step 2: Creating a Site Map Step 3: Displaying a Menu Based on the Site Map Step 4: Adding Breadcrumb Navigation Step 5: Adding the Default Page for Each Section Summary
Introduction
One common characteristic of user-friendly websites is that they have a consistent, site-wide page layout and navigation scheme. ASP.NET 2.0 introduces two new features that greatly simplify implementing both a site-wide page layout and navigation scheme: master pages and site navigation. Master pages allow for developers to create a site-wide template with designated editable regions. This template can then be applied to ASP.NET pages in the site. Such ASP.NET pages need only provide content for the master page's specified editable regions – all other markup in the master page is identical across all ASP.NET pages that use the master page. This model allows developers to define and centralize a site-wide page layout, thereby making it easier to create a consistent look and feel across all pages that can easily be updated.
The site navigation system provides both a mechanism for page developers to define a site map and an API for that site map to be programmatically queried. The new navigation Web controls – the Menu, TreeView, and SiteMapPath – make it easy to render all or part of the site map in a common navigation user interface element. We'll be using the default site navigation provider, meaning that our site map will be defined in an XML-formatted file.
To illustrate these concepts and make our tutorials website more usable, let's spend this lesson defining a site-wide page layout, implementing a site map, and adding the navigation UI. By the end of this tutorial we'll have a polished website design for building our tutorial web pages.
64
Figure 1. The End Result of This Tutorial
Step 1: Creating the Master Page
The first step is to create the master page for the site. Right now our website consists of only the Typed
DataSet (Northwind.xsd, in the App_Code folder), the BLL classes
(ProductsBLL.cs, CategoriesBLL.cs, and so on, all in the App_Code folder), the
database (NORTHWND.MDF, in the App_Data folder), the configuration file (Web.config),
and a CSS stylesheet file (Styles.css). I cleaned out those pages and files demonstrating using the DAL and BLL from the first two tutorials since we will be reexamining those examples in greater detail in future tutorials.
65
66
Figure 2. The Files in Our Project
To create a master page, right-click on the project name in the Solution Explorer and choose Add New
Item. Then select the Master Page type from the list of templates and name it Site.master.
Figure 3. Add a New Master Page to the Website
Define the site-wide page layout here in the master page. You can use the Design view and add whatever Layout or Web controls you need, or you can manually add the markup by hand in the Source view. In my master page I use cascading style sheets for positioning and styles with the CSS settings defined in the
external file Style.css. While you cannot tell from the markup shown below, the CSS rules are
defined such that the navigation <div>'s content is absolutely positioned so that it appears on the left and has a fixed width of 200 pixels.
Site.master
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs" Inherits="Site" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Working with Data Tutorials</title> <link href="Styles.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="wrapper"> <form id="form1" runat="server"> <div id="header"> <span class="title">Working with Data Tutorials</span> <span class="breadcrumb">TODO: Breadcrumb will go here...</span> </div> <div id="content"> <asp:contentplaceholder id="MainContent" runat="server"> <!-- Page-specific content will go here... --> </asp:contentplaceholder> </div> <div id="navigation"> TODO: Menu will go here... </div> </form> </div> </body> </html>
A master page defines both the static page layout and the regions that can be edited by the ASP.NET pages that use the master page. These content editable regions are indicated by the ContentPlaceHolder
control, which can be seen within the content <div>. Our master page has a single ContentPlaceHolder
(MainContent), but master page's may have multiple ContentPlaceHolders.
With the markup entered above, switching to the Design view shows the master page's layout. Any ASP.NET pages that use this master page will have this uniform layout, with the ability to specify the
markup for the MainContent region.
67
68
Figure 4. The Master Page, When Viewed Through the Design View
Step 2: Adding a Homepage to the Web Site
With the master page defined, we're ready to add the ASP.NET pages for the website. Let's start by adding
Default.aspx, our website's homepage. Right-click on the project name in the Solution Explorer and choose Add New Item. Pick the Web Form option from the template list and name the file
Default.aspx. Also, check the "Select master page" checkbox.
Figure 5. Add a New Web Form, Checking the "Select master page" Checkbox
After clicking the OK button, we're asked to choose what master page this new ASP.NET page should use. While you can have multiple master pages in your project, we have only one.
Figure 6. Choose the Master Page this ASP.NET Page Should Use
After picking the master page, the new ASP.NET pages will contain the following markup:
Default.aspx
<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Title="Untitled Page" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> </asp:Content>
In the @Page directive there's a reference to the master page file used
(MasterPageFile="~/Site.master"), and the ASP.NET page's markup contains a Content control for each of the ContentPlaceHolder controls defined in the master page, with the control's
ContentPlaceHolderID mapping the Content control to a specific ContentPlaceHolder. The Content control is where you place the markup you want to appear in the corresponding
ContentPlaceHolder. Set the @Page directive's Title attribute to Home and add some welcoming content to the Content control:
Default.aspx
<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Title="Home" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> <h1>Welcome to the Working with Data Tutorial Site</h1> <p>This site is being built as part of a set of tutorials that illustrate some of the new data access and databinding features in ASP.NET 2.0 and Visual Web Developer.</p> <p>Over time, it will include a host of samples that demonstrate:</p>
69
70
<ul> <li>Building a DAL (data access layer),</li> <li>Using strongly typed TableAdapters and DataTables</li> <li>Master-Detail reports</li> <li>Filtering</li> <li>Paging,</li> <li>Two-way databinding,</li> <li>Editing,</li> <li>Deleting,</li> <li>Inserting,</li> <li>Hierarchical data browsing,</li> <li>Hierarchical drill-down,</li> <li>Optimistic concurrency,</li> <li>And more!</li> </ul> </asp:Content>
The Title attribute in the @Page directive allows us to set the page's title from the ASP.NET page,
even though the <title> element is defined in the master page. We can also set the title
programmatically, using Page.Title. Also note that the master page's references to stylesheets
(such as Style.css) are automatically updated so that they work in any ASP.NET page, regardless of what directory the ASP.NET page is in relative to the master page.
Switching to the Design view we can see how our page will look in a browser. Note that in the Design view for the ASP.NET page that only the content editable regions are editable – the non-ContentPlaceHolder markup defined in the master page is grayed out.
Figure 7. The Design View for the ASP.NET Page Shows Both the Editable and Non-Editable Regions
When the Default.aspx page is visited by a browser, the ASP.NET engine automatically merges the page's master page content and the ASP.NET's content, and renders the merged content into the final
71
HTML that is sent down to the requesting browser. When the master page's content is updated, all ASP.NET pages that use this master page will have their content remerged with the new master page content the next time they are requested. In short, the master page model allows for a single page layout template to be defined (the master page) whose changes are immediately reflected across the entire site.
Adding Additional ASP.NET Pages to the Web Site
Let's take a moment to add additional ASP.NET page stubs to the site that will eventually hold the various reporting demos. There will be more than 35 demos in total, so rather than creating all of the stub pages let's just create the first few. Since there will also be many categories of demos, to better manage the demos add a folder for the categories. Add the following three folders for now:
• BasicReporting
• Filtering
• CustomFormatting
Finally, add new files as shown in the Solution Explorer in Figure 8. When adding each file, remember to check the "Select master page" checkbox.
Figure 8. Add the Following Files
Step 2: Creating a Site Map
One of the challenges of managing a website composed of more than a handful of pages is providing a straightforward way for visitors to navigate through the site. To begin with, the site's navigational structure must be defined. Next, this structure must be translated into navigable user interface elements,
such as menus or breadcrumbs. Finally, this whole process needs to be maintained and updated as new pages are added to the site and existing ones removed. Prior to ASP.NET 2.0, developers were on their own for creating the site's navigational structure, maintaining it, and translating it into navigable user interface elements. With ASP.NET 2.0, however, developers can utilize the very flexible built in site navigation system.
The ASP.NET 2.0 site navigation system provides a means for a developer to define a site map and to then access this information through a programmatic API. ASP.NET ships with a site map provider that expects site map data to be stored in an XML file formatted in a particular way. But, since the site navigation system is built on the provider model it can be extended to support alternative ways for serializing the site map information. Jeff Prosise's article, The SQL Site Map Provider You've Been Waiting For shows how to create a site map provider that stores the site map in a SQL Server database; another option is to create a site map provider based on the file system structure.
For this tutorial, however, let's use the default site map provider that ships with ASP.NET 2.0. To create the site map, simply right-click on the project name in the Solution Explorer, choose Add New Item, and
choose the Site Map option. Leave the name as Web.sitemap and click the Add button.
Figure 9. Add a Site Map to Your Project
The site map file is an XML file. Note that Visual Studio provides IntelliSense for the site map structure.
The site map file must have the <siteMap> node as its root node, which must contain precisely one
<siteMapNode> child element. That first <siteMapNode> element can then contain an
arbitrary number of descendent <siteMapNode> elements.
Define the site map to mimic the file system structure. That is, add a <siteMapNode> element for
each of the three folders, and child <siteMapNode> elements for each of the ASP.NET pages in those folders, like so:
Web.sitemap:
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="~/Default.aspx" title="Home" description="Home">
72
<siteMapNode title="Basic Reporting" url="~/BasicReporting/Default.aspx" description="Basic Reporting Samples"> <siteMapNode url="~/BasicReporting/SimpleDisplay.aspx" title="Simple Display" description="Displays the complete contents of a database table." /> <siteMapNode url="~/BasicReporting/DeclarativeParams.aspx" title="Declarative Parameters" description="Displays a subset of the contents of a database table using parameters." /> <siteMapNode url="~/BasicReporting/ProgrammaticParams.aspx" title="Setting Parameter Values" description="Shows how to set parameter values programmatically." /> </siteMapNode> <siteMapNode title="Filtering Reports" url="~/Filtering/Default.aspx" description="Samples of Reports that Support Filtering"> <siteMapNode url="~/Filtering/FilterByDropDownList.aspx" title="Filter by Drop-Down List" description="Filter results using a drop-down list." /> <siteMapNode url="~/Filtering/MasterDetailsDetails.aspx" title="Master-Details-Details" description="Filter results two levels down." /> <siteMapNode url="~/Filtering/DetailsBySelecting.aspx" title="Details of Selected Row" description="Show detail results for a selected item in a GridView." /> </siteMapNode> <siteMapNode title="Customized Formatting" url="~/CustomFormatting/Default.aspx" description="Samples of Reports Whose Formats are Customized"> <siteMapNode url="~/CustomFormatting/CustomColors.aspx" title="Format Colors" description="Format the grid's colors based on the underlying data." /> <siteMapNode url="~/CustomFormatting/GridViewTemplateField.aspx" title="Custom Content in a GridView" description="Shows using the TemplateField to customize the contents of a field in a GridView." /> <siteMapNode url="~/CustomFormatting/DetailsViewTemplateField.aspx" title="Custom Content in a DetailsView" description="Shows using the TemplateField to customize the contents of a field in a DetailsView." /> <siteMapNode url="~/CustomFormatting/FormView.aspx" title="Custom Content in a FormView" description="Illustrates using a FormView for a highly customized view." /> <siteMapNode url="~/CustomFormatting/SummaryDataInFooter.aspx" title="Summary Data in Footer" description="Display summary data in the grid's footer." /> </siteMapNode> </siteMapNode> </siteMap>
The site map defines the website's navigational structure, which is a hierarchy that describes the various
sections of the site. Each <siteMapNode> element in Web.sitemap represents a section in the site's navigational structure.
73
Figure 10. The Site Map Represents a Hierarchical Navigational Structure (click image to enlarge)
ASP.NET exposes the site map's structure through the .NET Framework's SiteMap class!href(http://msdn2.microsoft.com/en-us/library/system.web.sitemap.aspx). This class has a CurrentNode property, which returns information about the section the user is currently visiting; the RootNode property returns the root of the site map (Home, in our site map). Both the CurrentNode and RootNode properties return SiteMapNode!href(http://msdn2.microsoft.com/en-
us/library/system.web.sitemapnode.aspx) instances, which have properties like ParentNode, ChildNodes,
NextSibling, PreviousSibling, and so on, that allow for the site map hierarchy to be walked.
Step 3: Displaying a Menu Based on the Site Map
Accessing data in ASP.NET 2.0 can be accomplished programmatically, like in ASP.NET 1.x, or declaratively, through the new data source controls. There are several built-in data source controls such as the SqlDataSource control, for accessing relational database data, the ObjectDataSource control, for accessing data from classes, and others. You can even create your own custom data source controls.
The data source controls serve as a proxy between your ASP.NET page and the underlying data. In order to display a data source control's retrieved data, we'll typically add another Web control to the page and bind it to the data source control. To bind a Web control to a data source control, simply set the Web
control's DataSourceID property to the value of the data source control's ID property.
To aid in working with the site map's data, ASP.NET includes the SiteMapDataSource control, which allows us to bind a Web control against our website's site map. Two Web controls – the TreeView and Menu – are commonly used to provide a navigation user interface. To bind the site map data to one of these two controls, simply add a SiteMapDataSource to the page along with a TreeView or Menu control whose
DataSourceID property is set accordingly. For example, we could add a Menu control to the master page using the following markup:
<div id="navigation"> <asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"> </asp:Menu> <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" /> </div>
For a finer degree of control over the emitted HTML, we can bind the SiteMapDataSource control to the Repeater control, like so:
<div id="navigation"> <ul> <li><asp:HyperLink runat="server" ID="lnkHome" NavigateUrl="~/Default.aspx">Home</asp:HyperLink></li> <asp:Repeater runat="server" ID="menu" DataSourceID="SiteMapDataSource1">
74
<ItemTemplate> <li> <asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>'><%# Eval("Title") %></asp:HyperLink> </li> </ItemTemplate> </asp:Repeater> </ul> <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="false" /> </div>
The SiteMapDataSource control returns the site map hierarchy one level at a time, starting with the root site map node (Home, in our site map), then the next level (Basic Reporting, Filtering Reports, and Customized Formatting), and so on. When binding the SiteMapDataSource to a Repeater, it enumerates
the first level returned and instantiates the ItemTemplate for each SiteMapNode instance in
that first level. To access a particular property of the SiteMapNode, we can use
Eval(propertyName), which is how we get each SiteMapNode's Url and Title properties for the HyperLink control.
The Repeater example above will render the following markup:
<li> <a href="/Code/BasicReporting/Default.aspx">Basic Reporting</a> </li> <li> <a href="/Code/Filtering/Default.aspx">Filtering Reports</a> </li> <li> <a href="/Code/CustomFormatting/Default.aspx">Customized Formatting</a> </li>
These site map nodes (Basic Reporting, Filtering Reports, and Customized Formatting) comprise the second level of the site map being rendered, not the first. This is because the SiteMapDataSource's
ShowStartingNode property is set to False, causing the SiteMapDataSource to bypass the root site map node and instead begin by returning the second level in the site map hierarchy.
To display the children for the Basic Reporting, Filtering Reports, and Customized Formatting
SiteMapNodes, we can add another Repeater to the initial Repeater's ItemTemplate. This
second Repeater will be bound to the SiteMapNode instance's ChildNodes property, like so:
<asp:Repeater runat="server" ID="menu" DataSourceID="SiteMapDataSource1"> <ItemTemplate> <li> <asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>'><%# Eval("Title") %></asp:HyperLink> <asp:Repeater runat="server" DataSource='<%# ((SiteMapNode) Container.DataItem).ChildNodes %>'> <HeaderTemplate> <ul> </HeaderTemplate> <ItemTemplate> <li> <asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>'><%# Eval("Title") %></asp:HyperLink> </li> </ItemTemplate>
75
<FooterTemplate> </ul> </FooterTemplate> </asp:Repeater> </li> </ItemTemplate> </asp:Repeater>
These two Repeaters result in the following markup (some markup has been removed for brevity):
<li> <a href="/Code/BasicReporting/Default.aspx">Basic Reporting</a> <ul> <li> <a href="/Code/BasicReporting/SimpleDisplay.aspx">Simple Display</a> </li> <li> <a href="/Code/BasicReporting/DeclarativeParams.aspx">Declarative Parameters</a> </li> <li> <a href="/Code/BasicReporting/ProgrammaticParams.aspx">Setting Parameter Values</a> </li> </ul> </li> <li> <a href="/Code/Filtering/Default.aspx">Filtering Reports</a> ... </li> <li> <a href="/Code/CustomFormatting/Default.aspx">Customized Formatting</a> ... </li>
Using CSS styles chosen from Rachel Andrew's book The CSS Anthology: 101 Essential Tips, Tricks, and
Hacks, the <ul> and <li> elements are styled such that the markup produces the following visual output:
76
Figure 11. A Menu Composed from Two Repeaters and Some CSS
This menu is in the master page and bound to the site map defined in Web.sitemap, meaning that
any change to the site map will be immediately reflected on all pages that use the Site.master master page.
Disabling ViewState
All ASP.NET controls can optionally persist their state to the view state, which is serialized as a hidden form field in the rendered HTML. View state is used by controls to remember their programmatically-changed state across postbacks, such as the data bound to a data Web control. While view state permits information to be remembered across postbacks, it increases the size of the markup that must be sent to the client and can lead to severe page bloat if not closely monitored. Data Web controls – especially the GridView – are particularly notorious for adding dozens of extra kilobytes of markup to a page. While such an increase may be negligible for broadband or intranet users, view state can add several seconds to the round trip for dial-up users.
To see the impact of view state, visit a page in a browser and then view the source sent by the web page (in Internet Explorer, go to the View menu and choose the Source option). You can also turn on page tracing to see the view state allocation used by each of the controls on the page. The view state
77
information is serialized in a hidden form field named __VIEWSTATE, located in a <div> element
immediately after the opening <form> tag. View state is only persisted when there is a Web Form
being used; if your ASP.NET page does not include a <form runat="server"> in its
declarative syntax there won't be a __VIEWSTATE hidden form field in the rendered markup.
The VIEWSTATE form field generated by the master page adds roughly 1,800 bytes to the page's generated markup. This extra bloat is due primarily to the Repeater control, as the contents of the SiteMapDataSource control are persisted to view state. While an extra 1,800 bytes may not seem like much to get excited about, when using a GridView with many fields and records, the view state can easily swell by a factor of 10 or more.
View state can be disabled at the page or control level by setting the EnableViewState property
to false, thereby reducing the size of the rendered markup. Since the view state for a data Web control persists the data bound to the data Web control across postbacks, when disabling the view state for a data Web control the data must be bound on each and every postback. In ASP.NET version 1.x this responsibility fell on the shoulders of the page developer; with ASP.NET 2.0, however, the data Web controls will rebind to their data source control on each postback if needed.
To reduce the page's view state let's set the Repeater control's EnableViewState property to
false. This can be done through the Properties window in the Designer or declaratively in the Source view. After making this change the Repeater's declarative markup should look like:
<asp:Repeater runat="server" ID="menu" DataSourceID="SiteMapDataSource1" EnableViewState="False"> <ItemTemplate> ... ItemTemplate contents omitted for brevity ... </ItemTemplate> </asp:Repeater>
After this change, the page's rendered view state size has shrunk to a mere 52 bytes, a 97% savings in view state size! In the tutorials throughout this series we'll disable the view state of the data Web controls by default in order to reduce the size of the rendered markup. In the majority of the examples the
EnableViewState property will be set to false and done so without mention. The only time view state will be discussed is in scenarios where it must be enabled in order for the data Web control to provide its expected functionality.
Step 4: Adding Breadcrumb Navigation
To complete the master page, let's add a breadcrumb navigation UI element to each page. The breadcrumb quickly shows users their current position within the site hierarchy. Adding a breadcrumb in ASP.NET 2.0 is easy – just add a SiteMapPath control to the page; no code is needed.
For our site, add this control to the header <div>:
<span class="breadcrumb"> <asp:SiteMapPath ID="SiteMapPath1" runat="server"> </asp:SiteMapPath> </span>
The breadcrumb shows the current page the user is visiting in the site map hierarchy as well as that site map node's "ancestors," all the way up to the root (Home, in our site map).
78
79
Figure 12. The Breadcrumb Displays the Current Page and its Ancestors in the Site Map Hierarchy
Step 5: Adding the Default Page for Each Section
The tutorials in our site are broken down into different categories – Basic Reporting, Filtering, Custom Formatting, and so on – with a folder for each category and the corresponding tutorials as ASP.NET pages
within that folder. Additionally, each folder contains a Default.aspx page. For this default page,
let's display all of the tutorials for the current section. That is, for the Default.aspx in the
BasicReporting folder we'd have links to SimpleDisplay.aspx,
DeclarativeParams.aspx, and ProgrammaticParams.aspx. Here, again, we
can use the SiteMap class and a data Web control to display this information based upon the site map
defined in Web.sitemap.
Let's display an unordered list using a Repeater again, but this time we'll display the title and description of the tutorials. Since the markup and code to accomplish this will need to be repeated for each
Default.aspx page, we can encapsulate this UI logic in a User Control. Create a folder in the
website called UserControls and add to that a new item of type Web User Control named
SectionLevelTutorialListing.ascx, and add the following markup:
Figure 13. Add a New Web User Control to the UserControls Folder
SectionLevelTutorialListing.ascx
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="SectionLevelTutorialListing.ascx.cs" Inherits="UserControls_SectionLevelTutorialListing" %> <asp:Repeater ID="TutorialList" runat="server" EnableViewState="False">
<HeaderTemplate><ul></HeaderTemplate> <ItemTemplate> <li><asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>'></asp:HyperLink> - <%# Eval("Description") %></li> </ItemTemplate> <FooterTemplate></ul></FooterTemplate> </asp:Repeater>
SectionLevelTutorialListing.ascx.cs
using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class UserControls_SectionLevelTutorialListing : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { // If SiteMap.CurrentNode is not null, // bind CurrentNode's ChildNodes to the GridView if (SiteMap.CurrentNode != null) { TutorialList.DataSource = SiteMap.CurrentNode.ChildNodes; TutorialList.DataBind(); } } }
In the previous Repeater example we bound the SiteMap data to the Repeater declaratively; the
SectionLevelTutorialListing User Control, however, does so programmatically. In
the Page_Load event handler, a check is made to ensure that this is the first visit to the page (not a postback) and that this page's URL maps to a node in the site map. If this User Control is used in a page
that does not have a corresponding <siteMapNode> entry, SiteMap.CurrentNode will
return null and no data will be bound to the Repeater. Assuming we have a CurrentNode, we
bind its ChildNodes collection to the Repeater. Since our site map is set up such that the
Default.aspx page in each section is the parent node of all of the tutorials within that section, this code will display links to and descriptions of all of the section's tutorials, as shown in the screen shot below.
Once this Repeater has been created, open the Default.aspx pages in each of the folders, go to the Design view, and simply drag the User Control from the Solution Explorer onto the Design surface where you want the tutorial list to appear.
80
81
Figure 14. The User Control has Been Added to Default.aspx
Figure 15. The Basic Reporting Tutorials are Listed
Summary
With the site map defined and the master page complete, we now have a consistent page layout and navigation scheme for our data-related tutorials. Regardless of how many pages we add to our site,
updating the site-wide page layout or site navigation information is a quick and simple process due to this information being centralized. Specifically, the page layout information is defined in the master page
Site.master and the site map in Web.sitemap. We didn't need to write any code to achieve this site-wide page layout and navigation mechanism, and we retain full WYSIWYG designer support in Visual Studio.
Having completed the Data Access Layer and Business Logic Layer and having a consistent page layout and site navigation defined, we're ready to begin exploring common reporting patterns. In the next three tutorials we'll look at basic reporting tasks – displaying data retrieved from the BLL in the GridView, DetailsView, and FormView controls.
Happy Programming!
82