76
3-TIER ARCHITECTURE ASP.NET 2.0 TUTORIALS

3 Tier Architecture

Embed Size (px)

Citation preview

Page 1: 3 Tier Architecture

3-TIER ARCHITECTURE

ASP.NET 2.0 TUTORIALS

Page 2: 3 Tier Architecture

Contents Tutorial 1: Creating a Data Access Layer............................................................................................. 4

Introduction ........................................................................................................................................... 4

Step 1: Creating a Web Project and Connecting to the Database ............................................ 4

Using a Database in the App_Data Folder ................................................................................. 5

Connecting to the Database in a Microsoft SQL Server 2000 or 2005 Database Server . 6

Step 2: Creating the Data Access Layer ........................................................................................... 6

Creating a Typed DataSet and Table Adapter ........................................................................... 7

Step 3: Adding Parameterized Methods to the Data Access Layer ........................................ 18

Step 4: Inserting, Updating, and Deleting Data .......................................................................... 25

Creating Custom Insert, Update, and Delete Methods ......................................................... 29

Step 5: Completing the Data Access Layer ................................................................................... 32

Adding the Remaining TableAdapters ...................................................................................... 34

Adding Custom Code to the DAL................................................................................................ 36

Summary ............................................................................................................................................... 42

Tutorial 2: Creating a Business Logic Layer ...................................................................................... 43

Introduction ......................................................................................................................................... 43

Step 1: Creating the BLL Classes ..................................................................................................... 44

Adding the Other Classes ............................................................................................................. 49

Step 2: Accessing the Typed DataSets Through the BLL Classes ............................................ 50

Step 3: Adding Field-Level Validation to the DataRow Classes .............................................. 51

Step 4: Adding Custom Business Rules to the BLL's Classes .................................................... 54

Responding to Validation Errors in the Presentation Tier .................................................... 55

Summary ............................................................................................................................................... 56

Tutorial 3: Master Pages and Site Navigation ................................................................................. 57

Introduction ......................................................................................................................................... 57

Step 1: Creating the Master Page ................................................................................................... 58

Step 2: Adding a Homepage to the Web Site .............................................................................. 61

Adding Additional ASP.NET Pages to the Web Site .............................................................. 64

Page 3: 3 Tier Architecture

Step 2: Creating a Site Map .............................................................................................................. 65

Step 3: Displaying a Menu Based on the Site Map..................................................................... 68

Disabling ViewState ....................................................................................................................... 71

Step 4: Adding Breadcrumb Navigation ....................................................................................... 72

Step 5: Adding the Default Page for Each Section ..................................................................... 73

Summary ............................................................................................................................................... 76

Page 4: 3 Tier Architecture

Tutorial 1: Creating a Data Access Layer

Scott Mitchell

June 2006

Download the ASPNET_Data_Tutorial_1_CS.exe sample code.

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, theApp_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 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#.

Page 5: 3 Tier Architecture

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.

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.

Page 6: 3 Tier Architecture

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

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

Page 7: 3 Tier Architecture

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

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.

Figure 3. All Data Access Code is Relegated to the DAL

Creating a Typed DataSet and Table Adapter

Page 8: 3 Tier Architecture

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 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 theProducts 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 ourProducts 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.

Page 9: 3 Tier Architecture

Figure 5. Choose the Northwind Database from the Drop-Down List

After selecting the database and clicking Next, you'll be asked if you want to save the connection string in the Web.config file.

By saving the connection string you'll avoid having it hard coded in the TableAdapter classes, which simplifies things if the

connection string information changes in the future. If you opt to save the connection string in the configuration file it's placed in

the <connectionStrings> section, which can be optionally encrypted for improved security or modified later through the

new ASP.NET 2.0 Property Page within the IIS GUI Admin Tool, which is more ideal for administrators.

Page 10: 3 Tier Architecture

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.

Page 11: 3 Tier Architecture

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:

Page 12: 3 Tier Architecture

Figure 8. Enter the SQL Query Into the Textbox

Alternatively, use the Query Builder and graphically construct the query, as shown in Figure 9.

Page 13: 3 Tier Architecture

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.

Page 14: 3 Tier Architecture

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 GetDatamethod to GetProducts.

If checked, the final checkbox, "GenerateDBDirectMethods," creates Insert(), Update(), andDelete() 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.

Page 15: 3 Tier Architecture

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 theProducts DataTable (ProductID, ProductName, and so on), as

well as the methods of theProductsTableAdapter (Fill() and GetProducts()).

Page 16: 3 Tier Architecture

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 aGetProducts() 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 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" %>

Page 17: 3 Tier Architecture

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

</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();

}

}

Page 18: 3 Tier Architecture

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

Page 19: 3 Tier Architecture

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.

Page 20: 3 Tier Architecture

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 fromGetProducts(), 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).

Page 21: 3 Tier Architecture

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 toFillByCategoryID and for the return a DataTable return pattern (the GetX methods),

let's useGetProductsByCategoryID.

Page 22: 3 Tier Architecture

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 23: 3 Tier Architecture

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

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

Page 24: 3 Tier Architecture

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

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();

}

}

Page 25: 3 Tier Architecture

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

theUnitsOnStock, and so on.

Figure 21. Each Insert, Update, and Delete Request Is Sent to the Database Immediately

Page 26: 3 Tier Architecture

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

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,

andDeleteCommand 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 ProductsTableAdapterobject is the one selected in the drop-down list in the Properties window.)

Page 27: 3 Tier Architecture

Figure 23. The TableAdapter has InsertCommand, UpdateCommand, and DeleteCommandProperties

To examine or modify any of these database command properties, click on the CommandTextsubproperty, which will bring up

the Query Builder.

Page 28: 3 Tier Architecture

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

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

Page 29: 3 Tier Architecture

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.

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 addingSELECT

SCOPE_IDENTITY() at the end of the query, which will return the last identity value inserted into an IDENTITY column in

Page 30: 3 Tier Architecture

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.

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

Finally, name the new method InsertProduct.

Page 31: 3 Tier Architecture

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 theINSERT 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 InsertProductmethod's ExecuteMode property to Scalar.

Page 32: 3 Tier Architecture

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 Categoriestable 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 andCompanyName column values, which will update the strongly-typed DataTable to include these new

columns as well.

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 theGetProducts() method in

the ProductsTableAdapter and choose Configure. Then, adjust theSELECT clause so that it looks like:

SELECT ProductID, ProductName, SupplierID, CategoryID,

Page 33: 3 Tier Architecture

QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,

Discontinued,

(SELECT CategoryName FROM Categories WHERE Categories.CategoryID =

Products.CategoryID 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: 3 Tier Architecture

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 theInsertProduct method earlier in this tutorial. Furthermore, you'll manually have to provide

theInsertCommand, 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 ProductsTableAdapter class's GetProducts() andGetProductsByCategoryID(categoryID)

methods.

ProductsTableAdapter

GetProducts:

SELECT ProductID, ProductName, SupplierID, CategoryID,

QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,

Discontinued , (SELECT CategoryName FROM Categories WHERE

Page 35: 3 Tier Architecture

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

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

Page 36: 3 Tier Architecture

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

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.

Page 37: 3 Tier Architecture

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.

Page 38: 3 Tier Architecture

Figure 33. Inspect the Auto-Generated Code by Selecting Go To Definition from the Class View

While auto-generated code can be a great time saver, the code is often very generic and needs to be customized to meet the unique

needs of an application. The risk of extending auto-generated code, though, is that the tool that generated the code might decide

it's time to "regenerate" and overwrite your customizations. With .NET 2.0's new partial class concept, it's easy to split a class across

multiple files. This enables us to add our own methods, properties, and events to the auto-generated classes without having to worry

about Visual Studio overwriting our customizations.

To demonstrate how to customize the DAL, let's add a GetProducts() method to theSuppliersRow class.

The SuppliersRow class represents a single record in the Suppliers table; each supplier can provider zero to many

Page 39: 3 Tier Architecture

products, so GetProducts() will return those products of the specified supplier. To accomplish this create a new class file in

the App_Code folder namedSuppliersRow.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.

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:

Page 40: 3 Tier Architecture

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

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

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

Page 41: 3 Tier Architecture

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

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();

}

}

Page 42: 3 Tier Architecture

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!

Page 43: 3 Tier Architecture

Tutorial 2: Creating a Business Logic Layer 231 out of 316 rated this helpful Rate this topic

Scott Mitchell

June 2006

Download the ASPNET_Data_Tutorial_2_CS.exe sample code.

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

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

Page 44: 3 Tier Architecture

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 theApp_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 theBLL subfolder, choose Add a New

Item, and choose the Class template. Name the four classesProductsBLL, CategoriesBLL, SuppliersBLL,

and EmployeesBLL.

Page 45: 3 Tier Architecture

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

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;

Page 46: 3 Tier Architecture

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

jectMethodType.Select, true)]

public Northwind.ProductsDataTable GetProducts()

{

return Adapter.GetProducts();

}

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

jectMethodType.Select, false)]

public Northwind.ProductsDataTable GetProductByProductID(int productID)

{

return Adapter.GetProductByProductID(productID);

}

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

jectMethodType.Select, false)]

public Northwind.ProductsDataTable GetProductsByCategoryID(int

categoryID)

{

return Adapter.GetProductsByCategoryID(categoryID);

}

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

jectMethodType.Select, false)]

public Northwind.ProductsDataTable GetProductsBySupplierID(int

supplierID)

{

return Adapter.GetProductsBySupplierID(supplierID);

}

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

jectMethodType.Insert, true)]

public bool AddProduct(string productName, int? supplierID, int?

categoryID, string quantityPerUnit,

decimal? unitPrice, short? unitsInStock, short?

unitsOnOrder, short? reorderLevel,

bool discontinued)

{

Page 47: 3 Tier Architecture

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

jectMethodType.Update, true)]

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;

Page 48: 3 Tier Architecture

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;

}

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

jectMethodType.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, andUnitPrice, 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 DELETEstatement issued to the database will have no affect and therefore the DeleteProduct method

will return false.

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 ProductsRowinstance. This approach was chosen because the ProductsRow class

derives from the ADO.NETDataRow class, which doesn't have a default parameterless constructor. In order to create a

newProductsRow instance, we must first create a ProductsDataTable instance and then invoke

itsNewProductRow() 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.

Page 49: 3 Tier Architecture

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 usingGetProductByProductID(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 DataObjectattribute 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

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 (usingGetSupplierBySupplierID), sets its address-related properties, and then calls

down into theSupplierDataTable's Update method. The UpdateSupplierAddress method follows:

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataOb

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

Page 50: 3 Tier Architecture

return false;

else

{

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,

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

Page 51: 3 Tier Architecture

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

theProductName 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 constraintsexist 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 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 theProductsDataTable 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.

Page 52: 3 Tier Architecture

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.

Page 53: 3 Tier Architecture

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

{

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

Page 54: 3 Tier Architecture

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 could not mark X as discontinued; if, however,

supplier Ysupplied 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 ifDiscontinued 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

Page 55: 3 Tier Architecture

Northwind.ProductsDataTable productsBySupplier =

Adapter.GetProductsBySupplierID(product.SupplierID);

if (productsBySupplier.Count == 1)

// this is the only product we buy from this supplier

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 theHttpApplication'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();

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

}

Page 56: 3 Tier Architecture

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!

Page 57: 3 Tier Architecture

Tutorial 3: Master Pages and Site Navigation

Download the ASPNET_Data_Tutorial_3_CS.exe sample code.

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.

Page 58: 3 Tier Architecture

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

Page 59: 3 Tier Architecture

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

Page 60: 3 Tier Architecture

positioning and styles with the CSS settings defined in the external file Style.css. While you cannot tell from the markup

shown below, the CSS rules are defined such that the navigation <div>'s content is absolutely positioned so that it appears on the

left and has a fixed width of 200 pixels.

Site.master

<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Site.master.cs"

Inherits="Site" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

<title>Working with Data Tutorials</title>

<link href="Styles.css" rel="stylesheet" type="text/css" />

</head>

<body>

<div id="wrapper">

<form id="form1" runat="server">

<div id="header">

<span class="title">Working with Data Tutorials</span>

<span class="breadcrumb">TODO: Breadcrumb will go

here...</span>

</div>

<div id="content">

<asp:contentplaceholder id="MainContent" runat="server">

<!-- Page-specific content will go here... -->

</asp:contentplaceholder>

</div>

<div id="navigation">

TODO: Menu will go here...

</div>

</form>

</div>

</body>

</html>

A master page defines both the static page layout and the regions that can be edited by the ASP.NET pages that use the master

page. These content editable regions are indicated by the ContentPlaceHolder control, which can be seen within the

content <div>. Our master page has a single ContentPlaceHolder (MainContent), but master page's may have multiple

ContentPlaceHolders.

With the markup entered above, switching to the Design view shows the master page's layout. Any ASP.NET pages that use this

master page will have this uniform layout, with the ability to specify the markup for the MainContent region.

Page 61: 3 Tier Architecture

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.

Page 62: 3 Tier Architecture

Figure 5. Add a New Web Form, Checking the "Select master page" Checkbox

After clicking the OK button, we're asked to choose what master page this new ASP.NET page should use. While you can have

multiple master pages in your project, we have only one.

Figure 6. Choose the Master Page this ASP.NET Page Should Use

Page 63: 3 Tier Architecture

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>

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

Page 64: 3 Tier Architecture

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

Page 65: 3 Tier Architecture

Figure 8. Add the Following Files

Step 2: Creating a Site Map

One of the challenges of managing a website composed of more than a handful of pages is providing a straightforward way for

visitors to navigate through the site. To begin with, the site's navigational structure must be defined. Next, this structure must be

translated into navigable user interface elements, such as menus or breadcrumbs. Finally, this whole process needs to be maintained

and updated as new pages are added to the site and existing ones removed. Prior to ASP.NET 2.0, developers were on their own for

creating the site's navigational structure, maintaining it, and translating it into navigable user interface elements. With ASP.NET 2.0,

however, developers can utilize the very flexible built in site navigation system.

The ASP.NET 2.0 site navigation system provides a means for a developer to define a site map and to then access this information

through a programmatic API. ASP.NET ships with a site map provider that expects site map data to be stored in an XML file

formatted in a particular way. But, since the site navigation system is built on the provider model it can be extended to support

alternative ways for serializing the site map information. Jeff Prosise's article, The SQL Site Map Provider You've Been Waiting

For shows how to create a site map provider that stores the site map in a SQL Server database; another option is to create a site map

provider based on the file system structure.

For this tutorial, however, let's use the default site map provider that ships with ASP.NET 2.0. To create the site map, simply right-

click on the project name in the Solution Explorer, choose Add New Item, and choose the Site Map option. Leave the name

as Web.sitemap and click the Add button.

Page 66: 3 Tier Architecture

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

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

Page 67: 3 Tier Architecture

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

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

Page 68: 3 Tier Architecture

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

<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

Page 69: 3 Tier Architecture

eachSiteMapNode 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 FormattingSiteMapNodes, 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>

<FooterTemplate>

</ul>

</FooterTemplate>

</asp:Repeater>

</li>

</ItemTemplate>

</asp:Repeater>

These two Repeaters result in the following markup (some markup has been removed for brevity):

Page 70: 3 Tier Architecture

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

Page 71: 3 Tier Architecture

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

Page 72: 3 Tier Architecture

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 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 EnableViewStateproperty 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 tofalse. 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).

Page 73: 3 Tier Architecture

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.aspxpage. 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 SiteMapclass and a data Web control to display this information based upon the site map defined

inWeb.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" %>

Page 74: 3 Tier Architecture

<asp:Repeater ID="TutorialList" runat="server" EnableViewState="False">

<HeaderTemplate><ul></HeaderTemplate>

<ItemTemplate>

<li><asp:HyperLink runat="server" NavigateUrl='<%# Eval("Url") %>'

Text='<%# Eval("Title") %>'></asp:HyperLink>

- <%# Eval("Description") %></li>

</ItemTemplate>

<FooterTemplate></ul></FooterTemplate>

</asp:Repeater>

SectionLevelTutorialListing.ascx.cs

using System;

using System.Data;

using System.Configuration;

using System.Collections;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

public partial class UserControls_SectionLevelTutorialListing :

System.Web.UI.UserControl

{

protected void Page_Load(object sender, EventArgs e)

{

// If SiteMap.CurrentNode is not null,

// bind CurrentNode's ChildNodes to the GridView

if (SiteMap.CurrentNode != null)

{

TutorialList.DataSource = SiteMap.CurrentNode.ChildNodes;

TutorialList.DataBind();

}

}

}

In the previous Repeater example we bound the SiteMap data to the Repeater declaratively;

theSectionLevelTutorialListing User Control, however, does so programmatically. In thePage_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 theDefault.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.

Page 75: 3 Tier Architecture

Figure 14. The User Control has Been Added to Default.aspx

Figure 15. The Basic Reporting Tutorials are Listed

Page 76: 3 Tier Architecture

Summary

With the site map defined and the master page complete, we now have a consistent page layout and navigation scheme for our

data-related tutorials. Regardless of how many pages we add to our site, updating the site-wide page layout or site navigation

information is a quick and simple process due to this information being centralized. Specifically, the page layout information is

defined in the master page Site.master and the site map in Web.sitemap. We didn't need to write any code to achieve

this site-wide page layout and navigation mechanism, and we retain full WYSIWYG designer support in Visual Studio.

Having completed the Data Access Layer and Business Logic Layer and having a consistent page layout and site navigation defined,

we're ready to begin exploring common reporting patterns. In the next three tutorials we'll look at basic reporting tasks – displaying

data retrieved from the BLL in the GridView, DetailsView, and FormView controls.

Happy Programming!