Вы находитесь на странице: 1из 79

Name:

Published Date: July 15,


2009

Filed Under : Views: 25736


You might have seen many articles explaining ASP.Net GridView Add (Insert), Edit, Update and
Delete functionality, but this is different and how I’ll explain as we progress. My main objective in
this article is to keep it simple and cover multiple aspects in one article.

Concept

Basically I have tried to make the normal Add (Insert), Edit, Update and delete functions in
ASP.Net GridView simple and also combining the powers of ASP.Net AJAX with that of JQuery to
give an elegant and charming user experience.

Database and Connection String         

For this article as usual I have used my favorite NorthWind database which you can get by
clicking on the link below.

Download Northwind Database

Below is the connection string from the web.config

<connectionStrings>
      <add name="conString" connectionString="Data Source=.\SQLExpress;
      database=Northwind;Integrated Security=true"/>
</connectionStrings>

The GridView

Below is the markup of the ASP.Net GridView control that I’ll be using to demonstrate the various
features explained in this article.

<div id = "dvGrid" style ="padding:10px;width:550px">


<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:GridView ID="GridView1" runat="server"  Width = "550px"
AutoGenerateColumns = "false" Font-Names = "Arial"
Font-Size = "11pt" AlternatingRowStyle-BackColor = "#C2D69B" 
HeaderStyle-BackColor = "green" AllowPaging ="true"  ShowFooter =
"true" 
OnPageIndexChanging = "OnPaging" onrowediting="EditCustomer"
onrowupdating="UpdateCustomer"  onrowcancelingedit="CancelEdit"
PageSize = "10" >
<Columns>
<asp:TemplateField ItemStyle-Width = "30px"  HeaderText = "CustomerID">
    <ItemTemplate>
        <asp:Label ID="lblCustomerID" runat="server"
        Text='<%# Eval("CustomerID")%>'></asp:Label>
    </ItemTemplate>
    <FooterTemplate>
        <asp:TextBox ID="txtCustomerID" Width = "40px"
            MaxLength = "5" runat="server"></asp:TextBox>
    </FooterTemplate>
</asp:TemplateField>
<asp:TemplateField ItemStyle-Width = "100px"  HeaderText = "Name">
    <ItemTemplate>
        <asp:Label ID="lblContactName" runat="server"
                Text='<%# Eval("ContactName")%>'></asp:Label>
    </ItemTemplate>
    <EditItemTemplate>
        <asp:TextBox ID="txtContactName" runat="server"
            Text='<%# Eval("ContactName")%>'></asp:TextBox>
    </EditItemTemplate> 
    <FooterTemplate>
        <asp:TextBox ID="txtContactName" runat="server"></asp:TextBox>
    </FooterTemplate>
</asp:TemplateField>
<asp:TemplateField ItemStyle-Width = "150px"  HeaderText = "Company">
    <ItemTemplate>
        <asp:Label ID="lblCompany" runat="server"
            Text='<%# Eval("CompanyName")%>'></asp:Label>
    </ItemTemplate>
    <EditItemTemplate>
        <asp:TextBox ID="txtCompany" runat="server"
            Text='<%# Eval("CompanyName")%>'></asp:TextBox>
    </EditItemTemplate> 
    <FooterTemplate>
        <asp:TextBox ID="txtCompany" runat="server"></asp:TextBox>
    </FooterTemplate>
</asp:TemplateField>
<asp:TemplateField>
    <ItemTemplate>
        <asp:LinkButton ID="lnkRemove" runat="server"
            CommandArgument = '<%# Eval("CustomerID")%>'
         OnClientClick = "return confirm('Do you want to delete?')"
        Text = "Delete" OnClick = "DeleteCustomer"></asp:LinkButton>
    </ItemTemplate>
    <FooterTemplate>
        <asp:Button ID="btnAdd" runat="server" Text="Add"
            OnClick = "AddNewCustomer" />
    </FooterTemplate>
</asp:TemplateField>
<asp:CommandField  ShowEditButton="True" />
</Columns>
<AlternatingRowStyle BackColor="#C2D69B"  />
</asp:GridView>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID = "GridView1" />
</Triggers>
</asp:UpdatePanel>
</div>

The GridView has 3 data columns

1. Customer ID

2. Contact Name

3. Company Name

I have added a LinkButton in 4th column which will act as custom column for delete functionality.
The reason to use a custom button is to provide the JavaScript confirmation box to the user
when he clicks Delete. For Edit and Update I have added a command field which will act as the
5th column.

There’s also a Footer Row with 3 TextBoxes which will be used to add new records to the
database and an Add button which will be used to add the records.

I have enabled pagination and finally wrapped the complete Grid in update panel and the update
panel in a div dvGrid and the reason to that I’ll explain later in the article

Binding the GridView

Below is the code to bind the GridView in the page load event of the page

 
C#
 
 
private String strConnString =
ConfigurationManager.ConnectionStrings["conString"].ConnectionString;
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        BindData();
    }
}

private void BindData()


{
    string strQuery = "select CustomerID,ContactName,CompanyName" +
                       " from customers";
    SqlCommand cmd = new SqlCommand(strQuery);
    GridView1.DataSource = GetData(cmd);
    GridView1.DataBind();
}
 

VB.Net

Private strConnString As String =


ConfigurationManager.ConnectionStrings("conString").ConnectionString
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
Handles Me.Load
   If Not IsPostBack Then
         BindData()
   End If
End Sub

Private Sub BindData()


  Dim strQuery As String = "select CustomerID,ContactName,CompanyName" &
_
                                " from customers"
  Dim cmd As New SqlCommand(strQuery)
  GridView1.DataSource = GetData(cmd)
  GridView1.DataBind()
End Sub

Below is the screenshot of the GridView being populated using the above code
 

Adding new record

As discussed above I have placed 3 textboxes and a button in the Footer Row of the ASP.Net
GridView control in order to add new record to the database. On the onclick event if the button
the records are inserted into the SQL Server Database and the GridView is updated

 
C#

protected void AddNewCustomer(object sender, EventArgs e)


{
  string
CustomerID=((TextBox)GridView1.FooterRow.FindControl("txtCustomerID")).T
ext;
  string Name =
((TextBox)GridView1.FooterRow.FindControl("txtContactName")).Text;
  string Company =
((TextBox)GridView1.FooterRow.FindControl("txtCompany")).Text;
  SqlConnection con = new SqlConnection(strConnString);
  SqlCommand cmd = new SqlCommand();
  cmd.CommandType = CommandType.Text;
  cmd.CommandText = "insert into customers(CustomerID, ContactName,
CompanyName) " +
  "values(@CustomerID, @ContactName, @CompanyName);" +
  "select CustomerID,ContactName,CompanyName from customers";
  cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value =
CustomerID;
  cmd.Parameters.Add("@ContactName", SqlDbType.VarChar).Value = Name;
  cmd.Parameters.Add("@CompanyName", SqlDbType.VarChar).Value = Company;
  GridView1.DataSource = GetData(cmd);
  GridView1.DataBind();
}

VB.Net

Protected Sub AddNewCustomer(ByVal sender As Object, ByVal e As


EventArgs)
   Dim CustomerID As String = DirectCast(GridView1.FooterRow _
                .FindControl("txtCustomerID"), TextBox).Text
   Dim Name As String = DirectCast(GridView1 _
                .FooterRow.FindControl("txtContactName"), TextBox).Text
   Dim Company As String = DirectCast(GridView1 _
                .FooterRow.FindControl("txtCompany"), TextBox).Text
   Dim con As New SqlConnection(strConnString)
   Dim cmd As New SqlCommand()
   cmd.CommandType = CommandType.Text
   cmd.CommandText = "insert into customers(CustomerID, ContactName, " &
_
        "CompanyName) values(@CustomerID, @ContactName, @CompanyName);"
& _
        "select CustomerID,ContactName,CompanyName from customers"
   cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value =
CustomerID
   cmd.Parameters.Add("@ContactName", SqlDbType.VarChar).Value = Name
   cmd.Parameters.Add("@CompanyName", SqlDbType.VarChar).Value = Company
   GridView1.DataSource = GetData(cmd)
   GridView1.DataBind()
End Sub
 

You will notice I am firing two queries one to insert the data and second to select the updated
data and then rebind the GridView. The figure below displays how new records are added.

 
 

Edit and Update existing records

As described above I have used command field in order to provide the Edit functionality. Below is
the code snippet which is used to edit and update the records

C#

protected void EditCustomer(object sender, GridViewEditEventArgs e)


{
    GridView1.EditIndex = e.NewEditIndex;
    BindData();
}
protected void CancelEdit(object sender, GridViewCancelEditEventArgs e)
{
    GridView1.EditIndex = -1;
    BindData();
}
protected void UpdateCustomer(object sender, GridViewUpdateEventArgs e)
{
    string CustomerID = ((Label)GridView1.Rows[e.RowIndex]
                        .FindControl("lblCustomerID")).Text;
    string Name = ((TextBox)GridView1.Rows[e.RowIndex]
                        .FindControl("txtContactName")).Text;
    string Company = ((TextBox)GridView1.Rows[e.RowIndex]
                        .FindControl("txtCompany")).Text;
    SqlConnection con = new SqlConnection(strConnString);
    SqlCommand cmd = new SqlCommand();
    cmd.CommandType = CommandType.Text;
    cmd.CommandText = "update customers set ContactName=@ContactName," +
     "CompanyName=@CompanyName where CustomerID=@CustomerID;" +
     "select CustomerID,ContactName,CompanyName from customers";
    cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value =
CustomerID;
    cmd.Parameters.Add("@ContactName", SqlDbType.VarChar).Value = Name;
    cmd.Parameters.Add("@CompanyName", SqlDbType.VarChar).Value =
Company;
    GridView1.EditIndex = -1;
    GridView1.DataSource = GetData(cmd);
    GridView1.DataBind();
}

 
           
VB.Net

Protected Sub EditCustomer(ByVal sender As Object, ByVal e As


GridViewEditEventArgs)
   GridView1.EditIndex = e.NewEditIndex
   BindData()
End Sub
Protected Sub CancelEdit(ByVal sender As Object, ByVal e As
GridViewCancelEditEventArgs)
   GridView1.EditIndex = -1
   BindData()
End Sub
Protected Sub UpdateCustomer(ByVal sender As Object, ByVal e As
GridViewUpdateEventArgs)
   Dim CustomerID As String = DirectCast(GridView1.Rows(e.RowIndex) _
                                .FindControl("lblCustomerID"),
Label).Text
   Dim Name As String = DirectCast(GridView1.Rows(e.RowIndex) _
                                .FindControl("txtContactName"),
TextBox).Text
   Dim Company As String = DirectCast(GridView1.Rows(e.RowIndex) _
                                .FindControl("txtCompany"),
TextBox).Text
   Dim con As New SqlConnection(strConnString)
   Dim cmd As New SqlCommand()
   cmd.CommandType = CommandType.Text
   cmd.CommandText = "update customers set ContactName=@ContactName," _
   & "CompanyName=@CompanyName where CustomerID=@CustomerID;" _
   & "select CustomerID,ContactName,CompanyName from customers"
   cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value =
CustomerID
   cmd.Parameters.Add("@ContactName", SqlDbType.VarChar).Value = Name
   cmd.Parameters.Add("@CompanyName", SqlDbType.VarChar).Value = Company
   GridView1.EditIndex = -1
   GridView1.DataSource = GetData(cmd)
   GridView1.DataBind()
End Sub
 

You can view above I am simply getting the data from the textboxes in the Footer Row and then
firing an update query along with the select query so that the ASP.Net GridView control is also
updated. The figure below displays the Edit and Update functionality.

Deleting existing record with Confirmation

As said above I am using custom delete button instead of ASP.Net GridView delete command
field and the main reason for that is to add a confirmation.

 
      
C#

protected void DeleteCustomer(object sender, EventArgs e)


{
    LinkButton lnkRemove = (LinkButton)sender;
    SqlConnection con = new SqlConnection(strConnString);
    SqlCommand cmd = new SqlCommand();
    cmd.CommandType = CommandType.Text;
    cmd.CommandText = "delete from  customers where " +
    "CustomerID=@CustomerID;" +
     "select CustomerID,ContactName,CompanyName from customers";
    cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value
        = lnkRemove.CommandArgument;
    GridView1.DataSource = GetData(cmd);
    GridView1.DataBind();
}

VB.Net

Protected Sub DeleteCustomer(ByVal sender As Object, ByVal e As


EventArgs)
   Dim lnkRemove As LinkButton = DirectCast(sender, LinkButton)
   Dim con As New SqlConnection(strConnString)
   Dim cmd As New SqlCommand()
   cmd.CommandType = CommandType.Text
   cmd.CommandText = "delete from customers where " & _
   "CustomerID=@CustomerID;" & _
   "select CustomerID,ContactName,CompanyName from customers"
   cmd.Parameters.Add("@CustomerID", SqlDbType.VarChar).Value _
       = lnkRemove.CommandArgument
   GridView1.DataSource = GetData(cmd)
   GridView1.DataBind()
End Sub

Based on the sender argument I am getting the reference of the LinkButton that is clicked and
with the CommandArgument of the LinkButton I am getting the ID of the record to be deleted.
After the delete query I am firing a select query and the rebinding the GridView.

 
Pagination

For pagination I have added the OnPageIndexChanging event on which I am assigning the
new page index to the ASP.Net GridView control and then rebinding the data.

      
 

C#

 
protected void OnPaging(object sender, GridViewPageEventArgs e)
{
    BindData();
    GridView1.PageIndex = e.NewPageIndex;
    GridView1.DataBind();
}
 

VB.Net

Protected Sub OnPaging(ByVal sender As Object, ByVal e As


GridViewPageEventArgs)
   BindData()
   GridView1.PageIndex = e.NewPageIndex
   GridView1.DataBind()
End Sub

ASP.Net AJAX and JQuery

As you have seen in the start I had added an Update Panel and a DIV along with ASP.Net
GridView Control.

Basically the Update Panel will give the asynchronous calls thus not reloading the complete page
and the JQuery will block the UI until the update panel is refreshed completely. But instead of
blocking the complete page I am blocking only the contents of the DIV dvGrid. To achieve this I
am using the JQuery BlockUI Plugin

<script type = "text/javascript" src = "scripts/jquery-


1.3.2.min.js"></script>
<script type = "text/javascript" src =
"scripts/jquery.blockUI.js"></script>
<script type = "text/javascript">
    function BlockUI(elementID) {
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    prm.add_beginRequest(function() {
    $("#" + elementID).block({ message: '<table align =
"center"><tr><td>' +
     '<img src="images/loadingAnim.gif"/></td></tr></table>',
     css: {},
     overlayCSS: {backgroundColor:'#000000',opacity: 0.6, border:'3px
solid #63B2EB'
    }
    });
    });
    prm.add_endRequest(function() {
        $("#" + elementID).unblock();
    });
    }
    $(document).ready(function() {
 
            BlockUI("dvGrid");
            $.blockUI.defaults.css = {};           
    });
</script>
 

That’s all the scripting required and the following is achieved with the above scripts. It will block
the Grid until the update panel finishes its work. Refer the figure below

 
 

Adding a GridView Column of


Checkboxes
This is the C# tutorial (Switch to the Visual Basic tutorial)

This tutorial looks at how to add a column of check boxes to a GridView control to
provide the user with an intuitive way of selecting multiple rows of the GridView.
Download the code for this tutorial  |  Download the tutorial in PDF format

« Previous Tutorial | Next Tutorial »

Introduction
In the preceding tutorial we examined how to add a column of radio buttons to the
GridView for the purpose of selecting a particular record. A column of radio buttons is a
suitable user interface when the user is limited to choosing at most one item from the
grid. At times, however, we may want to allow the user to pick an arbitrary number of
items from the grid. Web-based email clients, for example, typically display the list of
messages with a column of checkboxes. The user can select an arbitrary number of
messages and then perform some action, such as moving the emails to another folder or
deleting them.

In this tutorial we will see how to add a column of checkboxes and how to determine
what checkboxes were checked on postback. In particular, we ll build an example that
closely mimics the web-based email client user interface. Our example will include a
paged GridView listing the products in the Products database table with a checkbox in
each row (see Figure 1). A Delete Selected Products button, when clicked, will delete
those products selected.
Figure 1: Each Product Row Includes a Checkbox (Click to view full-size image)

&nbsp;

Step 1: Adding a Paged GridView that Lists Product


Information
Before we worry about adding a column of checkboxes, let s first focus on listing the
products in a GridView that supports paging. Start by opening the CheckBoxField.aspx
page in the EnhancedGridView folder and drag a GridView from the Toolbox onto the
Designer, setting its ID to Products. Next, choose to bind the GridView to a new
ObjectDataSource named ProductsDataSource. Configure the ObjectDataSource to use
the ProductsBLL class, calling the GetProducts() method to return the data. Since this
GridView will be read-only, set the drop-down lists in the UPDATE, INSERT, and
DELETE tabs to (None) .
Figure 2: Create a New ObjectDataSource Named ProductsDataSource (Click to view
full-size image)

&nbsp;
Figure 3: Configure the ObjectDataSource to Retrieve Data Using the GetProducts()
Method (Click to view full-size image)

&nbsp;
Figure 4: Set the Drop-Down Lists in the UPDATE, INSERT, and DELETE Tabs to
(None) (Click to view full-size image)

&nbsp;

After completing the Configure Data Source wizard, Visual Studio will automatically
create BoundColumns and a CheckBoxColumn for the product-related data fields. Like
we did in the previous tutorial, remove all but the ProductName, CategoryName, and
UnitPrice BoundFields, and change the HeaderText properties to Product , Category ,
and Price . Configure the UnitPrice BoundField so that its value is formatted as a
currency. Also configure the GridView to support paging by checking the Enable Paging
checkbox from the smart tag.

Let s also add the user interface for deleting the selected products. Add a Button Web
control beneath the GridView, setting its ID to DeleteSelectedProducts and its Text
property to Delete Selected Products . Rather than actually deleting products from the
database, for this example we ll just display a message stating the products that would
have been deleted. To accommodate this, add a Label Web control beneath the Button.
Set its ID to DeleteResults, clear out its Text property, and set its Visible and
EnableViewState properties to false.
After making these changes, the GridView, ObjectDataSource, Button, and Label s
declarative markup should similar to the following:

<p> <asp:GridView ID="Products" runat="server"


AutoGenerateColumns="False" DataKeyNames="ProductID"
DataSourceID="ProductsDataSource" AllowPaging="True"
EnableViewState="False"> <Columns> <asp:BoundField
DataField="ProductName" HeaderText="Product"
SortExpression="ProductName" /> <asp:BoundField DataField="CategoryName"
HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" />
<asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}"
HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
</Columns> </asp:GridView> <asp:ObjectDataSource ID="ProductsDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetProducts" TypeName="ProductsBLL">
</asp:ObjectDataSource> </p> <p> <asp:Button ID="DeleteSelectedProducts"
runat="server" Text="Delete Selected Products" /> </p> <p> <asp:Label
ID="DeleteResults" runat="server" EnableViewState="False"
Visible="False"></asp:Label> </p>

Take a moment to view the page in a browser (see Figure 5). At this point you should see
the name, category, and price of the first ten products.
Figure 5: The Name, Category, and Price of the First Ten Products are Listed (Click to
view full-size image)

&nbsp;

Step 2: Adding a Column of Checkboxes


Since ASP.NET 2.0 includes a CheckBoxField, one might think that it could be used to
add a column of checkboxes to a GridView. Unfortunately, that is not the case, as the
CheckBoxField is designed to work with a Boolean data field. That is, in order to use the
CheckBoxField we must specify the underlying data field whose value is consulted to
determine whether the rendered checkbox is checked. We cannot use the CheckBoxField
to just include a column of unchecked checkboxes.

Instead, we must add a TemplateField and add a CheckBox Web control to its
ItemTemplate. Go ahead and add a TemplateField to the Products GridView and make
it the first (far-left) field. From the GridView s smart tag, click on the Edit Templates link
and then drag a CheckBox Web control from the Toolbox into the ItemTemplate. Set
this CheckBox s ID property to ProductSelector.
Figure 6: Add a CheckBox Web Control Named ProductSelector to the TemplateField
s ItemTemplate (Click to view full-size image)

&nbsp;

With the TemplateField and CheckBox Web control added, each row now includes a
checkbox. Figure 7 shows this page, when viewed through a browser, after the
TemplateField and CheckBox have been added.

Figure 7: Each Product Row Now Includes a Checkbox (Click to view full-size image)

&nbsp;

Step 3: Determining What Checkboxes Were Checked


On Postback
At this point we have a column of checkboxes but no way to determine what checkboxes
were checked on postback. When the Delete Selected Products button is clicked, though,
we need to know what checkboxes were checked in order to delete those products.

The GridView s Rows property provides access to the data rows in the GridView. We can
iterate through these rows, programmatically access the CheckBox control, and then
consult its Checked property to determine whether the CheckBox has been selected.

Create an event handler for the DeleteSelectedProducts Button Web control s Click
event and add the following code:

protected void DeleteSelectedProducts_Click(object sender, EventArgs e)


{ bool atLeastOneRowDeleted = false; // Iterate through the
Products.Rows property foreach (GridViewRow row in Products.Rows) { //
Access the CheckBox CheckBox cb =
(CheckBox)row.FindControl("ProductSelector"); if (cb != null &&
cb.Checked) { // Delete row! (Well, not really...) atLeastOneRowDeleted
= true; // First, get the ProductID for the selected row int productID =
Convert.ToInt32(Products.DataKeys[row.RowIndex].Value); // "Delete" the
row DeleteResults.Text += string.Format( "This would have deleted
ProductID {0}<br />", productID); } } // Show the Label if at least one
row was deleted... DeleteResults.Visible = atLeastOneRowDeleted; }

The Rows property returns a collection of GridViewRow instances that makeup the
GridView s data rows. The foreach loop here enumerates this collection. For each
GridViewRow object, the row s CheckBox is programmatically accessed using
row.FindControl("controlID"). If the CheckBox is checked, the row s corresponding
ProductID value is retrieved from the DataKeys collection. In this exercise, we simply
display an informative message in the DeleteResults Label, although in a working
application we d instead make a call to the ProductsBLL class s
DeleteProduct(productID) method.

With the addition of this event handler, clicking the Delete Selected Products button now
displays the ProductIDs of the selected products.
Figure 8: When the Delete Selected Products Button is Clicked the Selected Products
ProductIDs are Listed (Click to view full-size image)

&nbsp;

Step 4: Adding Check All and Uncheck All Buttons


If a user wants to delete all products on the current page, they must check each of the ten
checkboxes. We can help expedite this process by adding a Check All button that, when
clicked, selects all of the checkboxes in the grid. An Uncheck All button would be
equally helpful.

Add two Button Web controls to the page, placing them above the GridView. Set the first
one s ID to CheckAll and its Text property to Check All ; set the second one s ID to
UncheckAll and its Text property to Uncheck All .
<asp:Button ID="CheckAll" runat="server" Text="Check All" /> <asp:Button
ID="UncheckAll" runat="server" Text="Uncheck All" />

Next, create a method in the code-behind class named ToggleCheckState(checkState)


that, when invoked, enumerates the Products GridView s Rows collection and sets each
CheckBox s Checked property to the value of the passed in checkState parameter.

private void ToggleCheckState(bool checkState) { // Iterate through the


Products.Rows property foreach (GridViewRow row in Products.Rows) { //
Access the CheckBox CheckBox cb =
(CheckBox)row.FindControl("ProductSelector"); if (cb != null) cb.Checked
= checkState; } }

Next, create Click event handlers for the CheckAll and UncheckAll buttons. In
CheckAll s event handler, simply call ToggleCheckState(true); in UncheckAll, call
ToggleCheckState(false).

protected void CheckAll_Click(object sender, EventArgs e)


{ ToggleCheckState(true); } protected void UncheckAll_Click(object
sender, EventArgs e) { ToggleCheckState(false); }

With this code, clicking the Check All button causes a postback and checks all of the
checkboxes in the GridView. Likewise, clicking Uncheck All unselects all checkboxes.
Figure 9 shows the screen after the Check All button has been checked.
Figure 9: Clicking the Check All Button Selects All Checkboxes (Click to view full-size
image)

&nbsp;

Note: When displaying a column of checkboxes, one approach for selecting or


unselecting all of the checkboxes is through a checkbox in the header row. Moreover, the
current Check All / Uncheck All implementation requires a postback. The checkboxes
can be checked or unchecked, however, entirely through client-side script, thereby
providing a snappier user experience. To explore using a header row checkbox for Check
All and Uncheck All in detail, along with a discussion on using client-side techniques,
check out Checking All CheckBoxes in a GridView Using Client-Side Script and a
Check All CheckBox.
Introduction
As web developers, our lives revolve around working with data. We create databases to
store the data, code to retrieve and modify it, and web pages to collect and summarize it.
This is the first tutorial in a lengthy series that will explore techniques for implementing
these common patterns in ASP.NET 2.0. We'll start with creating a software architecture
composed of a Data Access Layer (DAL) using Typed DataSets, a Business Logic Layer
(BLL) that enforces custom business rules, and a presentation layer composed of
ASP.NET pages that share a common page layout. Once this backend groundwork has
been laid, we'll move into reporting, showing how to display, summarize, collect, and
validate data from a web application. These tutorials are geared to be concise and provide
step-by-step instructions with plenty of screen shots to walk you through the process
visually. Each tutorial is available in C# and Visual Basic versions and includes a
download of the complete code used. (This first tutorial is quite lengthy, but the rest are
presented in much more digestible chunks.)

For these tutorials we'll be using a Microsoft SQL Server 2005 Express Edition version
of the Northwind database placed in the App_Data directory. In addition to the database
file, the App_Data folder also contains the SQL scripts for creating the database, in case
you want to use a different database version. These scripts can be also be downloaded
directly from Microsoft, if you'd prefer. If you use a different SQL Server version of the
Northwind database, you will need to update the NORTHWNDConnectionString
setting in the application's Web.config file. The web application was built using Visual
Studio 2005 Professional Edition as a file system-based Web site project. However, all of
the tutorials will work equally well with the free version of Visual Studio 2005, Visual
Web Developer.

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 (Click to view full-size image)

&nbsp;

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 by downloading the SQL Server 2000 version of Northwind and installation
script directly from Microsoft's web site.

Once you have the database installed, go to the Server Explorer in Visual Studio, right-
click on the Data Connections node, and choose Add Connection. If you don't see the
Server Explorer go to the View / Server Explorer, or hit Ctrl+Alt+S. This will bring up
the Add Connection dialog box, where you can specify the server to connect to, the
authentication information, and the database name. Once you have successfully
configured the database connection information and clicked the OK button, the database
will be added as a node underneath the Data Connections node. You can expand the
database node to explore its tables, views, stored procedures, and so on.
Figure 2: Add a Connection to Your Database Server's Northwind Database

&nbsp;

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 (Click to view full-size image)

&nbsp;

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 (Click to view full-size image)

&nbsp;

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 the Products table.

Keep in mind that strongly-typed DataTables do not include any information on how to
access data from their underlying database table. In order to retrieve the data to populate
the DataTable, we use a TableAdapter class, which functions as our Data Access Layer.
For our Products DataTable, the TableAdapter will contain the methods GetProducts(),
GetProductByCategoryID(categoryID), and so on that we'll invoke from the
presentation layer. The DataTable's role is to serve as the strongly-typed objects used to
pass data between the layers.

The TableAdapter Configuration Wizard begins by prompting you to select which


database to work with. The drop-down list shows those databases in the Server Explorer.
If you did not add the Northwind database to the Server Explorer, you can click the New
Connection button at this time to do so.

Figure 5: Choose the Northwind Database from the Drop-Down List (Click to view full-
size image)

&nbsp;
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 (Click to view full-size image)

&nbsp;

Next, we need to define the schema for the first strongly-typed DataTable and provide the
first method for our TableAdapter to use when populating the strongly-typed DataSet.
These two steps are accomplished simultaneously by creating a query that returns the
columns from the table that we want reflected in our DataTable. At the end of the wizard
we'll give a method name to this query. Once that's been accomplished, this method can
be invoked from our presentation layer. The method will execute the defined query and
populate a strongly-typed DataTable.

To get started defining the SQL query we must first indicate how we want the
TableAdapter to issue the query. We can use an ad-hoc SQL statement, create a new
stored procedure, or use an existing stored procedure. For these tutorials we'll use ad-hoc
SQL statements. Refer to Brian Noyes's article, Build a Data Access Layer with the
Visual Studio 2005 DataSet Designer for an example of using stored procedures.
Figure 7: Query the Data Using an Ad-Hoc SQL Statement (Click to view full-size
image)

&nbsp;

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 (Click to view full-size image)

&nbsp;

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 (Click to view full-size
image)

&nbsp;

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 (Click
to view full-size image)

&nbsp;

After verifying the advanced options, click Next to proceed to the final screen. Here we
are asked to select which methods to add to the TableAdapter. There are two patterns for
populating data:

 Fill a DataTable with this approach a method is created that takes in a DataTable
as a parameter and populates it based on the results of the query. The ADO.NET
DataAdapter class, for example, implements this pattern with its Fill() method.
 Return a DataTable with this approach the method creates and fills the
DataTable for you and returns it as the methods return value.

You can have the TableAdapter implement one or both of these patterns. You can also
rename the methods provided here. Let's leave both checkboxes checked, even though
we'll only be using the latter pattern throughout these tutorials. Also, let's rename the
rather generic GetData method to GetProducts.

If checked, the final checkbox, "GenerateDBDirectMethods," creates Insert(), Update(),


and Delete() methods for the TableAdapter. If you leave this option unchecked, all
updates will need to be done through the TableAdapter's sole Update() method, which
takes in the Typed DataSet, a DataTable, a single DataRow, or an array of DataRows. (If
you've unchecked the "Generate Insert, Update, and Delete statements" option from the
advanced properties in Figure 9 this checkbox's setting will have no effect.) Let's leave
this checkbox selected.
Figure 11: Change the Method Name from GetData to GetProducts (Click to view full-
size image)

&nbsp;

Complete the wizard by clicking Finish. After the wizard closes we are returned to the
DataSet Designer which shows the DataTable we just created. You can see the list of
columns in the Products DataTable (ProductID, ProductName, and so on), as well as
the methods of the ProductsTableAdapter (Fill() and GetProducts()).
Figure 12: The Products DataTable and ProductsTableAdapter have been Added to
the Typed DataSet (Click to view full-size image)

&nbsp;

At this point we have a Typed DataSet with a single DataTable (Northwind.Products)


and a strongly-typed DataAdapter class
(NorthwindTableAdapters.ProductsTableAdapter) with a GetProducts() method.
These objects can be used to access a list of all products from code like:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter =
new NorthwindTableAdapters.ProductsTableAdapter();
Northwind.ProductsDataTable products;
products = productsAdapter.GetProducts();
foreach (Northwind.ProductsRow productRow in products)
Response.Write("Product: " + productRow.ProductName + "<br />");

This code did not require us to write one bit of data access-specific code. We did not have
to instantiate any ADO.NET classes, we didn't have to refer to any connection strings,
SQL queries, or stored procedures. Instead, the TableAdapter provides the low-level data
access code for us.

Each object used in this example is also strongly-typed, allowing Visual Studio to
provide IntelliSense and compile-time type checking. And best of all the DataTables
returned by the TableAdapter can be bound to 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>
<h2>
All Products</h2>
<p>
<asp:GridView ID="GridView1" runat="server"
CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
</asp:GridView>
</p>
</div>
</form>
</body>
</html>

AllProducts.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
public partial class AllProducts : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ProductsTableAdapter productsAdapter = new
ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
}
}

Figure 13: The List of Products is Displayed in a GridView (Click to view full-size
image)
&nbsp;

While this example required that we write three lines of code in our ASP.NET page's
Page_Load event handler, in future tutorials we'll examine how to use the
ObjectDataSource to declaratively retrieve the data from the DAL. With the
ObjectDataSource we'll not have to write any code and will get paging and sorting
support as well!

Step 3: Adding Parameterized Methods to the Data


Access Layer
At this point our ProductsTableAdapter class has but one method, GetProducts(),
which returns all of the products in the database. While being able to work with all
products is definitely useful, there are times when we'll want to retrieve information
about a specific product, or all products that belong to a particular category. To add such
functionality to our Data Access Layer we can add parameterized methods to the
TableAdapter.

Let's add the GetProductsByCategoryID(categoryID) method. To add a new method to


the DAL, return to the DataSet Designer, right-click in the ProductsTableAdapter
section, and choose Add Query.
Figure 14: Right-Click on the TableAdapter and Choose Add Query

&nbsp;

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 (Click to view
full-size image)

&nbsp;

The next step is to define the SQL query used to access the data. Since we want to return
only those products that belong to a particular category, I use the same SELECT
statement from GetProducts(), but add the following WHERE clause: WHERE
CategoryID = @CategoryID. The @CategoryID parameter indicates to the
TableAdapter wizard that the method we're creating will require an input parameter of the
corresponding type (namely, a nullable integer).
Figure 16: Enter a Query to Only Return Products in a Specified Category (Click to view
full-size image)

&nbsp;

In the final step we can choose which data access patterns to use, as well as customize the
names of the methods generated. For the Fill pattern, let's change the name to
FillByCategoryID and for the return a DataTable return pattern (the GetX methods), let's
use GetProductsByCategoryID.
Figure 17: Choose the Names for the TableAdapter Methods (Click to view full-size
image)

&nbsp;

After completing the wizard, the DataSet Designer includes the new TableAdapter
methods.
Figure 18: The Products Can Now be Queried by Category

&nbsp;

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 (Click to
view full-size image)

&nbsp;

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

<%@ 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>
<h2>Beverages</h2>
<p>
<asp:GridView ID="GridView1" runat="server"
CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
</asp:GridView>
</p>
</div>
</form>
</body>
</html>

Beverages.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
public partial class Beverages : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ProductsTableAdapter productsAdapter = new
ProductsTableAdapter();
GridView1.DataSource =
productsAdapter.GetProductsByCategoryID(1);
GridView1.DataBind();
}
}
Figure 20: Those Products in the Beverages Category are Displayed (Click to view full-
size image)

&nbsp;

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 the UnitsOnStock, and so on.
Figure 21: Each Insert, Update, and Delete Request is Sent to the Database Immediately
(Click to view full-size image)

&nbsp;

The other pattern, which I'll refer to as the batch update pattern, is to update an entire
DataSet, DataTable, or collection of DataRows in one method call. With this pattern a
developer deletes, inserts, and modifies the DataRows in a DataTable and then passes
those DataRows or DataTable into an update method. This method then enumerates the
DataRows passed in, determines whether or not they've been modified, added, or deleted
(via the DataRow's RowState property value), and issues the appropriate database request
for each record.

Figure 22: All Changes are Synchronized with the Database When the Update Method is
Invoked (Click to view full-size image)

&nbsp;

The TableAdapter uses the batch update pattern by default, but also supports the DB
direct pattern. Since we selected the "Generate Insert, Update, and Delete statements"
option from the Advanced Properties when creating our TableAdapter, the
ProductsTableAdapter contains an Update() method, which implements the batch
update pattern. Specifically, the TableAdapter contains an Update() method that can be
passed the Typed DataSet, a strongly-typed DataTable, or one or more DataRows. If you
left the "GenerateDBDirectMethods" checkbox checked when first creating the
TableAdapter the DB direct pattern will also be implemented via Insert(), Update(), and
Delete() methods.

Both data modification patterns use the TableAdapter's InsertCommand,


UpdateCommand, and DeleteCommand properties to issue their INSERT, UPDATE,
and DELETE commands to the database. You can inspect and modify the
InsertCommand, UpdateCommand, and DeleteCommand properties by clicking on
the TableAdapter in the DataSet Designer and then going to the Properties window.
(Make sure you have selected the TableAdapter, and that the ProductsTableAdapter
object is the one selected in the drop-down list in the Properties window.)

Figure 23: The TableAdapter has InsertCommand, UpdateCommand, and


DeleteCommand Properties (Click to view full-size image)

&nbsp;

To examine or modify any of these database command properties, click on the


CommandText subproperty, which will bring up the Query Builder.
Figure 24: Configure the INSERT, UPDATE, and DELETE Statements in the Query
Builder (Click to view full-size image)

&nbsp;

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 (Click to view
full-size image)

&nbsp;
On the next screen the InsertCommand's CommandText appears. Augment this query
by adding SELECT SCOPE_IDENTITY() at the end of the query, which will return the
last identity value inserted into an IDENTITY column in the same scope. (See the
technical documentation for more information about SCOPE_IDENTITY() and why
you probably want to use SCOPE_IDENTITY() in lieu of @@IDENTITY.) Make sure
that you end the INSERT statement with a semi-colon before adding the SELECT
statement.

Figure 26: Augment the Query to Return the SCOPE_IDENTITY() Value (Click to
view full-size image)

&nbsp;

Finally, name the new method InsertProduct.


Figure 27: Set the New Method Name to InsertProduct (Click to view full-size image)

&nbsp;

When you return to the DataSet Designer you'll see that the ProductsTableAdapter
contains a new method, InsertProduct. If this new method doesn't have a parameter for
each column in the Products table, chances are you forgot to terminate the INSERT
statement with a semi-colon. Configure the InsertProduct method and ensure you have a
semi-colon delimiting the INSERT and SELECT statements.

By default, insert methods issue non-query methods, meaning that they return the number
of affected rows. However, we want the InsertProduct method to return the value
returned by the query, not the number of rows affected. To accomplish this, adjust the
InsertProduct method's ExecuteMode property to Scalar.
Figure 28: Change the ExecuteMode Property to Scalar (Click to view full-size image)

&nbsp;

The following code shows this new InsertProduct method in action:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter =
new NorthwindTableAdapters.ProductsTableAdapter();
// Add a new product
int new_productID = Convert.ToInt32(productsAdapter.InsertProduct
("New Product", 1, 1, "12 tins per carton", 14.95m, 10, 0, 10,
false));
// On second thought, delete the product
productsAdapter.Delete(new_productID);

Step 5: Completing the Data Access Layer


Note that the ProductsTableAdapters class returns the CategoryID and SupplierID
values from the Products table, but doesn't include the CategoryName column from the
Categories table or the CompanyName column from the Suppliers table, although these
are likely the columns we want to display when showing product information. We can
augment the TableAdapter's initial method, GetProducts(), to include both the
CategoryName and CompanyName column values, which will update the strongly-
typed DataTable to include these new columns as well.

This can present a problem, however, as the TableAdapter's methods for inserting,
updating, and deleting data are based off of this initial method. Fortunately, the auto-
generated methods for inserting, updating, and deleting are not affected by subqueries in
the SELECT clause. By taking care to add our queries to Categories and Suppliers as
subqueries, rather than JOINs, we'll avoid having to rework those methods for modifying
data. Right-click on the GetProducts() method in the ProductsTableAdapter and
choose Configure. Then, adjust the SELECT clause so that it looks like:

SELECT ProductID, ProductName, SupplierID, CategoryID,


QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel,
Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.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 (Click to view
full-size image)

&nbsp;

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

&nbsp;

Take a moment to update the SELECT clause in the


GetProductsByCategoryID(categoryID) method as well.

If you update the GetProducts() SELECT using JOIN syntax the DataSet Designer
won't be able to auto-generate the methods for inserting, updating, and deleting database
data using the DB direct pattern. Instead, you'll have to manually create them much like
we did with the InsertProduct method earlier in this tutorial. Furthermore, you'll
manually have to provide the InsertCommand, UpdateCommand, and
DeleteCommand property values if you want to use the batch updating pattern.

Adding the Remaining TableAdapters


Up until now, we've only looked at working with a single TableAdapter for a single
database table. However, the Northwind database contains several related tables that we'll
need to work with in our web application. A Typed DataSet can contain multiple, related
DataTables. Therefore, to complete our DAL we need to add DataTables for the other
tables we'll be using in these tutorials. To add a new TableAdapter to a Typed DataSet,
open the DataSet Designer, right-click in the Designer, and choose Add / TableAdapter.
This will create a new DataTable and TableAdapter and walk you through the wizard we
examined earlier in this tutorial.
Take a few minutes to create the following TableAdapters and methods using the
following queries. Note that the queries in the ProductsTableAdapter include the
subqueries to grab each product's category and supplier names. Additionally, if you've
been following along, you've already added the ProductsTableAdapter class's
GetProducts() and GetProductsByCategoryID(categoryID) methods.

 ProductsTableAdapter
o GetProducts:
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
o GetProductsByCategoryID:
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
WHERE CategoryID = @CategoryID
o GetProductsBySupplierID:
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
WHERE SupplierID = @SupplierID
o GetProductByProductID:
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
WHERE ProductID = @ProductID
 CategoriesTableAdapter
o GetCategories:
SELECT CategoryID, CategoryName, Description
FROM Categories
o GetCategoryByCategoryID:
SELECT CategoryID, CategoryName, Description
FROM Categories
WHERE CategoryID = @CategoryID
 SuppliersTableAdapter
o GetSuppliers:
SELECT SupplierID, CompanyName, Address,
City, Country, Phone
FROM Suppliers
o GetSuppliersByCountry:
SELECT SupplierID, CompanyName, Address,
City, Country, Phone
FROM Suppliers
WHERE Country = @Country
o GetSupplierBySupplierID:
SELECT SupplierID, CompanyName, Address,
City, Country, Phone
FROM Suppliers
WHERE SupplierID = @SupplierID
 EmployeesTableAdapter
o GetEmployees:
SELECT EmployeeID, LastName, FirstName, Title,
HireDate, ReportsTo, Country
FROM Employees
o GetEmployeesByManager:
SELECT EmployeeID, LastName, FirstName, Title,
HireDate, ReportsTo, Country
FROM Employees
WHERE ReportsTo = @ManagerID
o 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 (Click
to view full-size image)

&nbsp;

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
(Click to view full-size image)

&nbsp;

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

&nbsp;

While auto-generated code can be a great time saver, the code is often very generic and
needs to be customized to meet the unique needs of an application. The risk of extending
auto-generated code, though, is that the tool that generated the code might decide it's time
to "regenerate" and overwrite your customizations. With .NET 2.0's new partial class
concept, it's easy to split a class across multiple files. This enables us to add our own
methods, properties, and events to the auto-generated classes without having to worry
about Visual Studio overwriting our customizations.

To demonstrate how to customize the DAL, let's add a GetProducts() method to the
SuppliersRow class. The SuppliersRow class represents a single record in the Suppliers
table; each supplier can provider zero to many products, so GetProducts() will return
those products of the specified supplier. To accomplish this create a new class file in the
App_Code folder named SuppliersRow.cs and add the following code:

using System;
using System.Data;
using NorthwindTableAdapters;
public partial class Northwind
{
public partial class SuppliersRow
{
public Northwind.ProductsDataTable GetProducts()
{
ProductsTableAdapter productsAdapter =
new ProductsTableAdapter();
return
productsAdapter.GetProductsBySupplierID(this.SupplierID);
}
}
}

This partial class instructs the compiler that when building the
Northwind.SuppliersRow class to include the GetProducts() method we just defined. If
you build your project and then return to the Class View you'll see GetProducts() now
listed as a method of Northwind.SuppliersRow.
Figure 34: The GetProducts() Method is Now Part of the Northwind.SuppliersRow
Class

&nbsp;

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> </p>");
}

This data can also be displayed in any of ASP.NET's data Web controls. The following
page uses a GridView control with two fields:
 A BoundField that displays the name of each supplier, and
 A TemplateField that contains a BulletedList control that is bound to the results
returned by the GetProducts() method for each supplier.

We'll examine how to display such master-detail reports in future tutorials. For now, this
example is designed to illustrate using the custom method added to the
Northwind.SuppliersRow class.

SuppliersAndProducts.aspx

<%@ Page Language="C#" CodeFile="SuppliersAndProducts.aspx.cs"


AutoEventWireup="true" 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>
<h2>
Suppliers and Their Products</h2>
<p>
<asp:GridView ID="GridView1" runat="server"
AutoGenerateColumns="False"
CssClass="DataWebControlStyle">
<HeaderStyle CssClass="HeaderStyle" />
<AlternatingRowStyle CssClass="AlternatingRowStyle" />
<Columns>
<asp:BoundField DataField="CompanyName"
HeaderText="Supplier" />
<asp:TemplateField HeaderText="Products">
<ItemTemplate>
<asp:BulletedList ID="BulletedList1"
runat="server" DataSource="<%#
((Northwind.SuppliersRow) ((System.Data.DataRowView)
Container.DataItem).Row).GetProducts() %>"
DataTextField="ProductName">
</asp:BulletedList>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</p>
</div>
</form>
</body>
</html>

SuppliersAndProducts.aspx.cs

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;
public partial class SuppliersAndProducts : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
SuppliersTableAdapter suppliersAdapter = new
SuppliersTableAdapter();
GridView1.DataSource = suppliersAdapter.GetSuppliers();
GridView1.DataBind();
}
}

Figure 35: The Supplier's Company Name is Listed in the Left Column, Their Products
in the Right (Click to view full-size image)
Bussiness Logic layer:

Introduction
The Data Access Layer (DAL) created in the first tutorial cleanly separates the data
access logic from the presentation logic. However, while the DAL cleanly separates the
data access details from the presentation layer, it does not enforce any business rules that
may apply. For example, for our application we may want to disallow the CategoryID or
SupplierID fields of the Products table to be modified when the Discontinued field is
set to 1, or we might want to enforce seniority rules, prohibiting situations in which an
employee is managed by someone who was hired after them. Another common scenario
is authorization perhaps only users in a particular role can delete products or can change
the UnitPrice value.

In this tutorial we'll see how to centralize these business rules into a Business Logic
Layer (BLL) that serves as an intermediary for data exchange between the presentation
layer and the DAL. In a real-world application, the BLL should be implemented as a
separate Class Library project; however, for these tutorials we'll implement the BLL as a
series of classes in our App_Code folder in order to simplify the project structure. Figure 1
illustrates the architectural relationships among the presentation layer, BLL, and DAL.
Figure 1: The BLL Separates the Presentation Layer from the Data Access Layer and
Imposes Business Rules

&nbsp;

Step 1: Creating the BLL Classes


Our BLL will be composed of four classes, one for each TableAdapter in the DAL; each
of these BLL classes will have methods for retrieving, inserting, updating, and deleting
from the respective TableAdapter in the DAL, applying the appropriate business rules.

To more cleanly separate the DAL- and BLL-related classes, let's create two subfolders in
the App_Code folder, DAL and BLL. Simply right-click on the App_Code folder in the
Solution Explorer and choose New Folder. After creating these two folders, move the
Typed DataSet created in the first tutorial into the DAL subfolder.
Next, create the four BLL class files in the BLL subfolder. To accomplish this, right-click
on the BLL subfolder, choose Add a New Item, and choose the Class template. Name the
four classes ProductsBLL, CategoriesBLL, SuppliersBLL, and EmployeesBLL.

Figure 2: Add Four New Classes to the App_Code Folder

&nbsp;

Next, let's add methods to each of the classes to simply wrap the methods defined for the
TableAdapters from the first tutorial. For now, these methods will just call directly into
the DAL; we'll return later to add any needed business logic.

Note: If you are using Visual Studio Standard Edition or above (that is, you're not using
Visual Web Developer), you can optionally design your classes visually using the Class
Designer. Refer to the Class Designer Blog for more information on this new feature in
Visual Studio.

For the ProductsBLL class we need to add a total of seven methods:

 GetProducts() returns all products


 GetProductByProductID(productID) returns the product with the specified
product ID
 GetProductsByCategoryID(categoryID) returns all products from the specified
category
 GetProductsBySupplier(supplierID) returns all products from the specified
supplier
 AddProduct(productName, supplierID, categoryID, quantityPerUnit,
unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued)
inserts a new product into the database using the values passed-in; returns the
ProductID value of the newly inserted record
 UpdateProduct(productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder,
reorderLevel, discontinued, productID) updates an existing product in the
database using the passed-in values; returns true if precisely one row was
updated, false otherwise
 DeleteProduct(productID) deletes the specified product from the database

ProductsBLL.cs

using System; using System.Data; using System.Configuration; using


System.Web; using System.Web.Security; using System.Web.UI; using
System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls; using NorthwindTableAdapters;
[System.ComponentModel.DataObject] public class ProductsBLL { private
ProductsTableAdapter _productsAdapter = null; protected
ProductsTableAdapter Adapter { get { if (_productsAdapter == null)
_productsAdapter = new ProductsTableAdapter(); return
_productsAdapter; } } [System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)] public
Northwind.ProductsDataTable GetProducts() { return
Adapter.GetProducts(); }
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)] public
Northwind.ProductsDataTable GetProductByProductID(int productID)
{ return Adapter.GetProductByProductID(productID); }
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)] public
Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
{ return Adapter.GetProductsByCategoryID(categoryID); }
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)] public
Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
{ return Adapter.GetProductsBySupplierID(supplierID); }
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Insert, true)] public bool
AddProduct(string productName, int? supplierID, int? categoryID, string
quantityPerUnit, decimal? unitPrice, short? unitsInStock, short?
unitsOnOrder, short? reorderLevel, bool discontinued) { // Create a new
ProductRow instance Northwind.ProductsDataTable products = new
Northwind.ProductsDataTable(); Northwind.ProductsRow product =
products.NewProductsRow(); product.ProductName = productName; if
(supplierID == null) product.SetSupplierIDNull(); else
product.SupplierID = supplierID.Value; if (categoryID == null)
product.SetCategoryIDNull(); else product.CategoryID = categoryID.Value;
if (quantityPerUnit == null) product.SetQuantityPerUnitNull(); else
product.QuantityPerUnit = quantityPerUnit; if (unitPrice == null)
product.SetUnitPriceNull(); else product.UnitPrice = unitPrice.Value; if
(unitsInStock == null) product.SetUnitsInStockNull(); else
product.UnitsInStock = unitsInStock.Value; if (unitsOnOrder == null)
product.SetUnitsOnOrderNull(); else product.UnitsOnOrder =
unitsOnOrder.Value; if (reorderLevel == null)
product.SetReorderLevelNull(); else product.ReorderLevel =
reorderLevel.Value; product.Discontinued = discontinued; // Add the new
product products.AddProductsRow(product); int rowsAffected =
Adapter.Update(products); // Return true if precisely one row was
inserted, // otherwise false return rowsAffected == 1; }
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)] 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.DataObjectMethodType.Delete, true)] public bool
DeleteProduct(int productID) { int rowsAffected =
Adapter.Delete(productID); // Return true if precisely one row was
deleted, // otherwise false return rowsAffected == 1; } }

The methods that simply return data GetProducts, GetProductByProductID,


GetProductsByCategoryID, and GetProductBySuppliersID are fairly straightforward
as they simply call down into the DAL. While in some scenarios there may be business
rules that need to be implemented at this level (such as authorization rules based on the
currently logged on user or the role to which the user belongs), we'll simply leave these
methods as-is. For these methods, then, the BLL serves merely as a proxy through which
the presentation layer accesses the underlying data from the Data Access Layer.

The AddProduct and UpdateProduct methods both take in as parameters the values for
the various product fields and add a new product or update an existing one, respectively.
Since many of the Product table's columns can accept NULL values (CategoryID,
SupplierID, and UnitPrice, to name a few), those input parameters for AddProduct
and UpdateProduct that map to such columns use nullable types. Nullable types are new
to .NET 2.0 and provide a technique for indicating whether a value type should, instead,
be null. In C# you can flag a value type as a nullable type by adding ? after the type
(like int? x;). Refer to the Nullable Types section in the C# Programming Guide for
more information.

All three methods return a Boolean value indicating whether a row was inserted, updated,
or deleted since the operation may not result in an affected row. For example, if the page
developer calls DeleteProduct passing in a ProductID for a non-existent product, the
DELETE statement issued to the database will have no affect and therefore the
DeleteProduct method will return false.

Note that when adding a new product or updating an existing one we take in the new or
modified product's field values as a list of scalars as opposed to accepting a ProductsRow
instance. This approach was chosen because the ProductsRow class derives from the
ADO.NET DataRow class, which doesn't have a default parameterless constructor. In
order to create a new ProductsRow instance, we must first create a ProductsDataTable
instance and then invoke its NewProductRow() method (which we do in AddProduct).
This shortcoming rears its head when we turn to inserting and updating products using
the ObjectDataSource. In short, the ObjectDataSource will try to create an instance of the
input parameters. If the BLL method expects a ProductsRow instance, the
ObjectDataSource will try to create one, but fail due to the lack of a default parameterless
constructor. For more information on this problem, refer to the following two ASP.NET
Forums posts: Updating ObjectDataSources with Strongly-Typed DataSets, and Problem
With ObjectDataSource and Strongly-Typed DataSet.

Next, in both AddProduct and UpdateProduct, the code creates a ProductsRow instance
and populates it with the values just passed in. When assigning values to DataColumns of
a DataRow various field-level validation checks can occur. Therefore, manually putting
the passed in values back into a DataRow helps ensure the validity of the data being
passed to the BLL method. Unfortunately the strongly-typed DataRow classes generated
by Visual Studio do not use nullable types. Rather, to indicate that a particular
DataColumn in a DataRow should correspond to a NULL database value we must use the
SetColumnNameNull() method.

In UpdateProduct we first load in the product to update using


GetProductByProductID(productID). While this may seem like an unnecessary trip to
the database, this extra trip will prove worthwhile in future tutorials that explore
optimistic concurrency. Optimistic concurrency is a technique to ensure that two users
who are simultaneously working on the same data don't accidentally overwrite one
another's changes. Grabbing the entire record also makes it easier to create update
methods in the BLL that only modify a subset of the DataRow's columns. When we
explore the SuppliersBLL class we'll see such an example.

Finally, note that the ProductsBLL class has the DataObject attribute applied to it (the
[System.ComponentModel.DataObject] syntax right before the class statement near the
top of the file) and the methods have DataObjectMethodAttribute attributes. The
DataObject attribute marks the class as being an object suitable for binding to an
ObjectDataSource control, whereas the DataObjectMethodAttribute indicates the
purpose of the method. As we'll see in future tutorials, ASP.NET 2.0's ObjectDataSource
makes it easy to declaratively access data from a class. To help filter the list of possible
classes to bind to in the ObjectDataSource's wizard, by default only those classes marked
as DataObjects are shown in the wizard's drop-down list. The ProductsBLL class will
work just as well without these attributes, but adding them makes it easier to work with in
the ObjectDataSource's wizard.
Adding the Other Classes
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
o GetCategories()
o GetCategoryByCategoryID(categoryID)
 SuppliersBLL.cs
o GetSuppliers()
o GetSupplierBySupplierID(supplierID)
o GetSuppliersByCountry(country)
o UpdateSupplierAddress(supplierID, address, city, country)
 EmployeesBLL.cs
o GetEmployees()
o GetEmployeeByEmployeeID(employeeID)
o GetEmployeesByManager(managerID)

The one method worth noting is the SuppliersBLL class's UpdateSupplierAddress


method. This method provides an interface for updating just the supplier's address
information. Internally, this method reads in the SupplierDataRow object for the
specified supplierID (using GetSupplierBySupplierID), sets its address-related
properties, and then calls down into the SupplierDataTable's Update method. The
UpdateSupplierAddress method follows:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)] public bool
UpdateSupplierAddress (int supplierID, string address, string city,
string country) { Northwind.SuppliersDataTable suppliers =
Adapter.GetSupplierBySupplierID(supplierID); if (suppliers.Count ==
0) // no matching record found, return false return false; else
{ Northwind.SuppliersRow supplier = suppliers[0]; if (address == null)
supplier.SetAddressNull(); else supplier.Address = address; if (city ==
null) supplier.SetCityNull(); else supplier.City = city; if (country ==
null) supplier.SetCountryNull(); else supplier.Country = country; //
Update the supplier Address-related information int rowsAffected =
Adapter.Update(supplier); // Return true if precisely one row was
updated, // otherwise false return rowsAffected == 1; } }

Refer to this article's download for my complete implementation of the BLL classes.

Step 2: Accessing the Typed DataSets Through the BLL


Classes
In the first tutorial we saw examples of working directly with the Typed DataSet
programmatically, but with the addition of our BLL classes, the presentation tier should
work against the BLL instead. In the AllProducts.aspx example from the first tutorial,
the ProductsTableAdapter was used to bind the list of products to a GridView, as
shown in the following code:

ProductsTableAdapter productsAdapter = new ProductsTableAdapter();


GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();

To use the new BLL classes, all that needs to be changed is the first line of code simply
replace the ProductsTableAdapter object with a ProductBLL object:

ProductsBLL productLogic = new ProductsBLL(); GridView1.DataSource =


productLogic.GetProducts(); GridView1.DataBind();

The BLL classes can also be accessed declaratively (as can the Typed DataSet) by using
the ObjectDataSource. We'll be discussing the ObjectDataSource in greater detail in the
following tutorials.

Figure 3: The List of Products is Displayed in a GridView (Click to view full-size


image)

&nbsp;

Step 3: Adding Field-Level Validation to the DataRow


Classes
Field-level validation are checks that pertains to the property values of the business
objects when inserting or updating. Some field-level validation rules for products include:
 The ProductName field must be 40 characters or less in length
 The QuantityPerUnit field must be 20 characters or less in length
 The ProductID, ProductName, and Discontinued fields are required, but all
other fields are optional
 The UnitPrice, UnitsInStock, UnitsOnOrder, and ReorderLevel fields must
be greater than or equal to zero

These rules can and should be expressed at the database level. The character limit on the
ProductName and QuantityPerUnit fields are captured by the data types of those
columns in the Products table (nvarchar(40) and nvarchar(20), respectively).
Whether fields are required and optional are expressed by if the database table column
allows NULLs. Four check constraints exist that ensure that only values greater than or
equal to zero can make it into the UnitPrice, UnitsInStock, UnitsOnOrder, or
ReorderLevel columns.

In addition to enforcing these rules at the database they should also be enforced at the
DataSet level. In fact, the field length and whether a value is required or optional are
already captured for each DataTable's set of DataColumns. To see the existing field-level
validation automatically provided, go to the DataSet Designer, select a field from one of
the DataTables and then go to the Properties window. As Figure 4 shows, the
QuantityPerUnit DataColumn in the ProductsDataTable has a maximum length of 20
characters and does allow NULL values. If we attempt to set the ProductsDataRow's
QuantityPerUnit property to a string value longer than 20 characters an
ArgumentException will be thrown.
Figure 4: The DataColumn Provides Basic Field-Level Validation (Click to view full-
size image)

&nbsp;

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 (Click to view full-size image)

&nbsp;

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 Y supplied us with three products, A, B, and C, then we could mark any and all of
these as discontinued. An odd business rule, but business rules and common sense aren't
always aligned!

To enforce this business rule in the UpdateProducts method we'd start by checking if
Discontinued was set to true and, if so, we'd call GetProductsBySupplierID to
determine how many products we purchased from this product's supplier. If only one
product is purchased from this supplier, we throw an ApplicationException.

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


categoryID, string quantityPerUnit, decimal? unitPrice, short?
unitsInStock, short? unitsOnOrder, short? reorderLevel, bool
discontinued, int productID) { Northwind.ProductsDataTable products =
Adapter.GetProductByProductID(productID); if (products.Count == 0) // no
matching record found, return false return false; Northwind.ProductsRow
product = products[0]; // Business rule check - cannot discontinue // a
product that is 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 it is the only product purchased from a supplier"); }
product.ProductName = productName; if (supplierID == null)
product.SetSupplierIDNull(); else product.SupplierID = supplierID.Value;
if (categoryID == null) product.SetCategoryIDNull(); else
product.CategoryID = categoryID.Value; if (quantityPerUnit == null)
product.SetQuantityPerUnitNull(); else product.QuantityPerUnit =
quantityPerUnit; if (unitPrice == null) product.SetUnitPriceNull(); else
product.UnitPrice = unitPrice.Value; if (unitsInStock == null)
product.SetUnitsInStockNull(); else product.UnitsInStock =
unitsInStock.Value; if (unitsOnOrder == null)
product.SetUnitsOnOrderNull(); else product.UnitsOnOrder =
unitsOnOrder.Value; if (reorderLevel == null)
product.SetReorderLevelNull(); else product.ReorderLevel =
reorderLevel.Value; product.Discontinued = discontinued; // Update the
product record int rowsAffected = Adapter.Update(product); // Return
true if precisely one row was updated, // otherwise false return
rowsAffected == 1; }
Responding to Validation Errors in the Presentation
Tier
When calling the BLL from the presentation tier we can decide whether to attempt to
handle any exceptions that might be raised or let them bubble up to ASP.NET (which will
raise the HttpApplication's Error event). To handle an exception when working with
the BLL programmatically, we can use a try...catch block, as the following example
shows:

ProductsBLL productLogic = new ProductsBLL(); // Update information for


ProductID 1 try { // This will fail since we are 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.

Вам также может понравиться