+ All Categories
Home > Documents > Data Access and Business Layers

Data Access and Business Layers

Date post: 08-Apr-2015
Category:
Upload: puspala-manojkumar
View: 90 times
Download: 4 times
Share this document with a friend
82
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
Transcript
Page 1: Data Access and Business Layers

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

Page 2: Data Access and Business Layers

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

Page 3: Data Access and Business Layers

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

Page 4: Data Access and Business Layers

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

Page 5: Data Access and Business Layers

• 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

Page 6: Data Access and Business Layers

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

Page 7: Data Access and Business Layers

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>

Page 8: Data Access and Business Layers

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

Page 9: Data Access and Business Layers

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

Page 10: Data Access and Business Layers

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

Page 11: Data Access and Business Layers

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

Page 12: Data Access and Business Layers

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

Page 13: Data Access and Business Layers

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

Page 14: Data Access and Business Layers

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

Page 15: Data Access and Business Layers

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>

&nbsp;</p>

</div>

</form>

</body>

15

Page 16: Data Access and Business Layers

</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

Page 17: Data Access and Business Layers

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

Page 18: Data Access and Business Layers

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

Page 19: Data Access and Business Layers

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

Page 20: Data Access and Business Layers

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

Page 21: Data Access and Business Layers

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

Page 22: Data Access and Business Layers

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

Page 23: Data Access and Business Layers

<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>

&nbsp;</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

Page 24: Data Access and Business Layers

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

Page 25: Data Access and Business Layers

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

Page 26: Data Access and Business Layers

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.

Page 27: Data Access and Business Layers

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

Page 28: Data Access and Business Layers

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

Page 29: Data Access and Business Layers

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

Page 30: Data Access and Business Layers

Figure 26. Augment the Query to Return the SCOPE_IDENTITY() Value

Finally, name the new method InsertProduct.

30

Page 31: Data Access and Business Layers

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

Page 32: Data Access and Business Layers

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

Page 33: Data Access and Business Layers

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.

Page 34: Data Access and Business Layers

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

Page 35: Data Access and Business Layers

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

Page 36: Data Access and Business Layers

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

Page 37: Data Access and Business Layers

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

Page 38: Data Access and Business Layers

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

Page 39: Data Access and Business Layers

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.

Page 40: Data Access and Business Layers

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

Page 41: Data Access and Business Layers

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

Page 42: Data Access and Business Layers

Northwind.ProductsDataTable products = supplier.GetProducts();

foreach (Northwind.ProductsRow product in products)

Response.Write("<li>" + product.ProductName + "</li>");

Response.Write("</ul><p>&nbsp;</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

Page 43: Data Access and Business Layers

<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>

&nbsp;</p>

</div>

</form>

</body>

</html>

SuppliersAndProducts.aspx.cs

using System;

using System.Data;

using System.Configuration;

using System.Collections;

using System.Web;

43

Page 44: Data Access and Business Layers

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

Page 45: Data Access and Business Layers

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

Page 46: Data Access and Business Layers

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

Page 47: Data Access and Business Layers

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

Page 48: Data Access and Business Layers

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

Page 49: Data Access and Business Layers

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

Page 50: Data Access and Business Layers

{

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

Page 51: Data Access and Business Layers

{

// 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

Page 52: Data Access and Business Layers

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

Page 53: Data Access and Business Layers

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

Page 54: Data Access and Business Layers

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

Page 55: Data Access and Business Layers

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

Page 56: Data Access and Business Layers

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

Page 57: Data Access and Business Layers

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

Page 58: Data Access and Business Layers

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

Page 59: Data Access and Business Layers

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

Page 60: Data Access and Business Layers

{

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

Page 61: Data Access and Business Layers

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

Page 62: Data Access and Business Layers

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

Page 63: Data Access and Business Layers

// 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

Page 64: Data Access and Business Layers

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

Page 65: Data Access and Business Layers

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

Page 66: Data Access and Business Layers

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.

Page 67: Data Access and Business Layers

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

Page 68: Data Access and Business Layers

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

Page 69: Data Access and Business Layers

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

Page 70: Data Access and Business Layers

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

Page 71: Data Access and Business Layers

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,

Page 72: Data Access and Business Layers

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

Page 73: Data Access and Business Layers

<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&apos;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

Page 74: Data Access and Business Layers

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

Page 75: Data Access and Business Layers

<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

Page 76: Data Access and Business Layers

<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

Page 77: Data Access and Business Layers

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

Page 78: Data Access and Business Layers

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

Page 79: Data Access and Business Layers

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">

Page 80: Data Access and Business Layers

<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

Page 81: Data Access and Business Layers

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,

Page 82: Data Access and Business Layers

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


Recommended