Sie sind auf Seite 1von 76

3-TIER ARCHITECTURE ASP.NET 2.

0 TUTORIALS

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

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

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

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.

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

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

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.

Figure 5. Choose the Northwind Database from the Drop-Down List After selecting the database and clicking Next, you'll be asked if you want to save the connection string in the Web.config file. By saving the connection string you'll avoid having it hard coded in the TableAdapter classes, which simplifies things if the connection string information changes in the future. If you opt to save the connection string in the configuration file it's placed in the <connectionStrings> section, which can be optionally encrypted for improved security or modified later through the new ASP.NET 2.0 Property Page within the IIS GUI Admin Tool, which is more ideal for administrators.

Figure 6. Save the Connection String to Web.config Next, we need to define the schema for the first strongly-typed DataTable and provide the first method for our TableAdapter to use when populating the strongly-typed DataSet. These two steps are accomplished simultaneously by creating a query that returns the columns from the table that we want reflected in our DataTable. At the end of the wizard we'll give a method name to this query. Once that's been accomplished, this method can be invoked from our presentation layer. The method will execute the defined query and populate a strongly-typed DataTable. To get started defining the SQL query we must first indicate how we want the TableAdapter to issue the query. We can use an adhoc 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.

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:

Figure 8. Enter the SQL Query Into the Textbox Alternatively, use the Query Builder and graphically construct the query, as shown in Figure 9.

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.

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.

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

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

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

System; System.Data; System.Configuration; System.Collections; System.Web; System.Web.Security; System.Web.UI; System.Web.UI.WebControls; System.Web.UI.WebControls.WebParts; System.Web.UI.HtmlControls; 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(); } }

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.

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.

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

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.

Figure 17. Choose the Names for the TableAdapter Methods After completing the wizard, the DataSet Designer includes the new TableAdapter methods.

Figure 18. The Products Can Now be Queried by Category

Take a moment to add a GetProductByProductID(productID) method using the same technique. These parameterized queries can be tested directly from the DataSet Designer. Right-click on the method in the TableAdapter and choose Preview Data. Next, enter the values to use for the parameters and click Preview.

Figure 19. Those Products Belonging to the Beverages Category are Shown With the GetProductsByCategoryID(categoryID) method in our DAL, we can now create an ASP.NET page that displays only those products in a specified category. The following example shows all products that are in the Beverages category, which have a CategoryID of 1. Beverages.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Beverages.aspx.cs" Inherits="Beverages" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> <link href="Styles.css" rel="stylesheet" type="text/css" /> </head> <body> <form id="form1" runat="server"> <div> <h1>Beverages</h1>

<p> <asp:GridView ID="GridView1" runat="server" CssClass="DataWebControlStyle"> <HeaderStyle CssClass="HeaderStyle" /> <AlternatingRowStyle CssClass="AlternatingRowStyle" /> </asp:GridView> &nbsp;</p> </div> </form> </body> </html>
Beverages.aspx.cs

using using using using using using using using using using using

System; System.Data; System.Configuration; System.Collections; System.Web; System.Web.Security; System.Web.UI; System.Web.UI.WebControls; System.Web.UI.WebControls.WebParts; System.Web.UI.HtmlControls; NorthwindTableAdapters;

public partial class Beverages : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ProductsTableAdapter productsAdapter = new ProductsTableAdapter(); GridView1.DataSource = productsAdapter.GetProductsByCategoryID(1); GridView1.DataBind(); } }

Figure 20. Those Products in the Beverages Category are Displayed

Step 4: Inserting, Updating, and Deleting Data


There are two patterns commonly used for inserting, updating, and deleting data. The first pattern, which I'll call the database direct pattern, involves creating methods that, when invoked, issue an INSERT, UPDATE, or DELETE command to the database that operates on a single database record. Such methods 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

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

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.

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

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

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.

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.

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,

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.

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

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

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.

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.

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

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:

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

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

System; System.Data; System.Configuration; System.Collections; System.Web; System.Web.Security; System.Web.UI; System.Web.UI.WebControls; System.Web.UI.WebControls.WebParts; System.Web.UI.HtmlControls; 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(); } }

Figure 35. The Supplier's Company Name Is Listed in the Left Column, Their Products in the Right

Summary
When building a web application creating the DAL should be one of your first steps, occurring before you start creating your presentation layer. With Visual Studio, creating a DAL based on Typed DataSets is a task that can be accomplished in 10-15 minutes without writing a line of code. The tutorials moving forward will build upon this DAL. In the next tutorial we'll define a number of business rules and see how to implement them in a separate Business Logic Layer. Happy Programming!

Tutorial 2: Creating a Business Logic Layer


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.

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.

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

System; System.Data; System.Configuration; System.Web; System.Web.Security; System.Web.UI; System.Web.UI.WebControls; System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls; using NorthwindTableAdapters; [System.ComponentModel.DataObject] public class ProductsBLL { private ProductsTableAdapter _productsAdapter = null; protected ProductsTableAdapter Adapter { get { if (_productsAdapter == null) _productsAdapter = new ProductsTableAdapter(); return _productsAdapter; } } [System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.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) {

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

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

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) GetSuppliers() GetSupplierBySupplierID(supplierID) GetSuppliersByCountry(country) UpdateSupplierAddress(supplierID, address, city, country) GetEmployees() GetEmployeeByEmployeeID(employeeID) GetEmployeesByManager(managerID)

SuppliersBLL.cs

EmployeesBLL.cs

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

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.

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

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.

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

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

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

As we'll see in future tutorials, handling exceptions that bubble up from the BLL when using a data Web control for inserting, updating, or deleting data can be handled directly in an event handler as opposed to having to wrap code in try...catch blocks.

Summary
A well-architected application is crafted into distinct layers, each of which encapsulates a particular role. In the first tutorial of this article series we created a Data Access Layer using Typed DataSets; in this tutorial we built a Business Logic Layer as a series of classes in our application's App_Code folder that call down into our DAL. The BLL implements the field-level and business-level logic for our application. In addition to creating a separate BLL, as we did in this tutorial, another option is to extend the TableAdapters' methods through the use of partial classes. However, using this technique does not allow us to override existing methods nor does it separate our DAL and our BLL as cleanly as the approach we've taken in this article. With the DAL and BLL complete, we're ready to start on our presentation layer. In the next tutorial we'll take a brief detour from data access topics and define a consistent page layout for use throughout the tutorials. Happy Programming!

Tutorial 3: Master Pages and Site Navigation

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.

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.

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

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.

Figure 4. The Master Page, When Viewed Through the Design View

Step 2: Adding a Homepage to the Web Site


With the master page defined, we're ready to add the ASP.NET pages for the website. Let's start by adding Default.aspx, our website's homepage. Right-click on the project name in the Solution Explorer and choose Add New Item. Pick the Web Form option from the template list and name the file Default.aspx. Also, check the "Select master page" checkbox.

Figure 5. Add a New Web Form, Checking the "Select master page" Checkbox After clicking the OK button, we're asked to choose what master page this new ASP.NET page should use. While you can have multiple master pages in your project, we have only one.

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

After picking the master page, the new ASP.NET pages will contain the following markup: Default.aspx

<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Title="Untitled Page" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> </asp:Content>
In the @Page directive there's a reference to the master page file used (MasterPageFile="~/Site.master"), and the ASP.NET page's markup contains a Content control for each of the ContentPlaceHolder controls defined in the master page, with the control's ContentPlaceHolderID mapping the Content control to a specific ContentPlaceHolder. The Content control is where you place the markup you want to appear in the corresponding ContentPlaceHolder. Set the @Page directive's Title attribute to Home and add some welcoming content to the Content control: Default.aspx

<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Title="Home" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> <h1>Welcome to the Working with Data Tutorial Site</h1> <p>This site is being built as part of a set of tutorials that illustrate some of the new data access and databinding features in ASP.NET 2.0 and Visual Web Developer.</p> <p>Over time, it will include a host of samples that demonstrate:</p> <ul> <li>Building a DAL (data access layer),</li> <li>Using strongly typed TableAdapters and DataTables</li> <li>Master-Detail reports</li> <li>Filtering</li> <li>Paging,</li> <li>Two-way databinding,</li> <li>Editing,</li> <li>Deleting,</li> <li>Inserting,</li> <li>Hierarchical data browsing,</li> <li>Hierarchical drill-down,</li> <li>Optimistic concurrency,</li> <li>And more!</li> </ul> </asp:Content>
The Title attribute in the @Page directive allows us to set the page's title from the ASP.NET page, even though the <title> element is defined in the master page. We can also set the title programmatically, using Page.Title. Also note that the master page's references to stylesheets (such as Style.css) are automatically updated so that they work in any ASP.NET page, regardless of what directory the ASP.NET page is in relative to the master page. Switching to the Design view we can see how our page will look in a browser. Note that in the Design view for the ASP.NET page that only the content editable regions are editable the non-ContentPlaceHolder markup defined in the master page is grayed out.

Figure 7. The Design View for the ASP.NET Page Shows Both the Editable and Non-Editable Regions When the Default.aspx page is visited by a browser, the ASP.NET engine automatically merges the page's master page content and the ASP.NET's content, and renders the merged content into the final HTML that is sent down to the requesting browser. When the master page's content is updated, all ASP.NET pages that use this master page will have their content remerged with the new master page content the next time they are requested. In short, the master page model allows for a single page layout template to be defined (the master page) whose changes are immediately reflected across the entire site.

Adding Additional ASP.NET Pages to the Web Site


Let's take a moment to add additional ASP.NET page stubs to the site that will eventually hold the various reporting demos. There will be more than 35 demos in total, so rather than creating all of the stub pages let's just create the first few. Since there will also be many categories of demos, to better manage the demos add a folder for the categories. Add the following three folders for now:

BasicReporting Filtering CustomFormatting

Finally, add new files as shown in the Solution Explorer in Figure 8. When adding each file, remember to check the "Select master page" checkbox.

Figure 8. Add the Following Files

Step 2: Creating a Site Map


One of the challenges of managing a website composed of more than a handful of pages is providing a straightforward way for visitors to navigate through the site. To begin with, the site's navigational structure must be defined. Next, this structure must be translated into navigable user interface elements, such as menus or breadcrumbs. Finally, this whole process needs to be maintained and updated as new pages are added to the site and existing ones removed. Prior to ASP.NET 2.0, developers were on their own for creating the site's navigational structure, maintaining it, and translating it into navigable user interface elements. With ASP.NET 2.0, however, developers can utilize the very flexible built in site navigation system. The ASP.NET 2.0 site navigation system provides a means for a developer to define a site map and to then access this information through a programmatic API. ASP.NET ships with a site map provider that expects site map data to be stored in an XML file formatted in a particular way. But, since the site navigation system is built on the provider model it can be extended to support alternative ways for serializing the site map information. Jeff Prosise's article, The SQL Site Map Provider You've Been Waiting For shows how to create a site map provider that stores the site map in a SQL Server database; another option is to create a site map provider based on the file system structure. For this tutorial, however, let's use the default site map provider that ships with ASP.NET 2.0. To create the site map, simply rightclick on the project name in the Solution Explorer, choose Add New Item, and choose the Site Map option. Leave the name as Web.sitemap and click the Add button.

Figure 9. Add a Site Map to Your Project The site map file is an XML file. Note that Visual Studio provides IntelliSense for the site map structure. The site map file must have the <siteMap> node as its root node, which must contain precisely one <siteMapNode> child element. That first <siteMapNode> element can then contain an arbitrary number of descendent <siteMapNode> elements. Define the site map to mimic the file system structure. That is, add a <siteMapNode> element for each of the three folders, and child <siteMapNode> elements for each of the ASP.NET pages in those folders, like so: Web.sitemap:

<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="~/Default.aspx" title="Home" description="Home"> <siteMapNode title="Basic Reporting" url="~/BasicReporting/Default.aspx" description="Basic Reporting Samples"> <siteMapNode url="~/BasicReporting/SimpleDisplay.aspx" title="Simple Display" description="Displays the complete contents of a database table." /> <siteMapNode url="~/BasicReporting/DeclarativeParams.aspx" title="Declarative Parameters" description="Displays a subset of the contents of a database table using parameters." /> <siteMapNode url="~/BasicReporting/ProgrammaticParams.aspx" title="Setting Parameter Values" description="Shows how to set parameter values programmatically." /> </siteMapNode> <siteMapNode title="Filtering Reports" url="~/Filtering/Default.aspx" description="Samples of Reports that Support Filtering">

<siteMapNode url="~/Filtering/FilterByDropDownList.aspx" title="Filter by Drop-Down List" description="Filter results using a dropdown 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/enus/library/system.web.sitemap.aspx). This class has a CurrentNode property, which returns information about the section the user is

currently visiting; the RootNode property returns the root of the site map (Home, in our site map). Both the CurrentNode and RootNode properties return SiteMapNode!href(http://msdn2.microsoft.com/en-us/library/system.web.sitemapnode.aspx) instances, which have properties like ParentNode, ChildNodes, NextSibling, PreviousSibling, and so on, that allow for the site map hierarchy to be walked.

Step 3: Displaying a Menu Based on the Site Map


Accessing data in ASP.NET 2.0 can be accomplished programmatically, like in ASP.NET 1.x, or declaratively, through the new data source controls. There are several built-in data source controls such as the SqlDataSource control, for accessing relational database data, the ObjectDataSource control, for accessing data from classes, and others. You can even create your own custom data source controls. The data source controls serve as a proxy between your ASP.NET page and the underlying data. In order to display a data source control's retrieved data, we'll typically add another Web control to the page and bind it to the data source control. To bind a Web control to a data source control, simply set the Web control's DataSourceID property to the value of the data source control's ID property. To aid in working with the site map's data, ASP.NET includes the SiteMapDataSource control, which allows us to bind a Web control against our website's site map. Two Web controls the TreeView and Menu are commonly used to provide a navigation user interface. To bind the site map data to one of these two controls, simply add a SiteMapDataSource to the page along with a TreeView or Menu control whose DataSourceID property is set accordingly. For example, we could add a Menu control to the master page using the following markup:

<div id="navigation"> <asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"> </asp:Menu> <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" /> </div>
For a finer degree of control over the emitted HTML, we can bind the SiteMapDataSource control to the Repeater control, like so:

<div id="navigation"> <ul> <li><asp:HyperLink runat="server" ID="lnkHome" NavigateUrl="~/Default.aspx">Home</asp:HyperLink></li> <asp:Repeater runat="server" ID="menu" DataSourceID="SiteMapDataSource1"> <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

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

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

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.

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

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

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

System; System.Data; System.Configuration; System.Collections; System.Web; System.Web.Security; System.Web.UI; System.Web.UI.WebControls; System.Web.UI.WebControls.WebParts; 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.

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

Figure 15. The Basic Reporting Tutorials are Listed

Summary
With the site map defined and the master page complete, we now have a consistent page layout and navigation scheme for our data-related tutorials. Regardless of how many pages we add to our site, updating the site-wide page layout or site navigation information is a quick and simple process due to this information being centralized. Specifically, the page layout information is defined in the master page Site.master and the site map in Web.sitemap. We didn't need to write any code to achieve this site-wide page layout and navigation mechanism, and we retain full WYSIWYG designer support in Visual Studio. Having completed the Data Access Layer and Business Logic Layer and having a consistent page layout and site navigation defined, we're ready to begin exploring common reporting patterns. In the next three tutorials we'll look at basic reporting tasks displaying data retrieved from the BLL in the GridView, DetailsView, and FormView controls. Happy Programming!

Das könnte Ihnen auch gefallen