asp tutorials, asp.net tutorials, sample code, and Microsoft news from 15Seconds
Data Access  |   Troubleshooting  |   Security  |   Performance  |   ADSI  |   Upload  |   Email  |   Control Building  |   Component Building  |   Forms  |   XML  |   Web Services  |   ASP.NET  |   .NET Features  |   .NET 2.0  |   App Development  |   App Architecture  |   IIS  |   Wireless
 
Pioneering Active Server
 Power Search





Active News
15 Seconds Weekly Newsletter
• Complete Coverage
• Site Updates
• Upcoming Features

More Free Newsletters
Reference
News
Articles
Archive
Writers
Code Samples
Components
Tools
FAQ
Feedback
Books
Links
DL Archives
Community
Messageboard
List Servers
Mailing List
WebHosts
Consultants
Tech Jobs
15 Seconds
Home
Site Map
Press
Legal
Privacy Policy
internet.commerce














internet.com
IT
Developer
Internet News
Small Business
Personal Technology

Search internet.com
Advertise
Corporate Info
Newsletters
Tech Jobs
E-mail Offers

HardwareCentral
Compare products, prices, and stores at Hardware Central!

Creating a Data Access Layer in .NET - Part 2
By Wayne Plourde
Rating: 4.4 out of 5
Rate this article


  • email this article to a colleague
  • suggest an article

    Introduction


    This is the second part of our series on creating a Data Access Layer (DAL) in .NET. In the first part, we discussed some of the basic principles associated with building a DAL and laid out the groundwork for the DAL - covering the setup of the database, login accounts, and Visual Studio projects. In this article, we will set up the code necessary for retrieving and manipulating data. So, let's not waste any time and jump right into building Data Handler classes!

    Creating Data Handlers with the Component Class

    The Data Handler classes in our Data Access Layer (DAL) will provide the interface to the data through public methods for retrieving and updating DataSets. These classes will contain private instances of Connection, Command and DataAdapter objects. These objects will do the work behind the scenes with our DataSets and DataReaders.

    So, how many Data Handlers do we need? This is where the art comes into the design of your DAL. This issue may be primarily influenced by the context in which the DAL is being used. One approach is to have one large Handler class for all the data access. This may be fine for scenarios where the lifetime of an instance of the class will be extended, however for Web apps, there will probably be a lot of unnecessary overhead since each instance of the Handler object will probably utilize only a few of the DataAdapters at any one time. For Web apps, my preference is to keep the Handler classes small and specific to a limited set of entities. This means less overhead in instantiating the objects and fewer constituent components in the tray. If the Handler class is too small though, you may have difficulty building a transaction scope around all the entities that need to work together.

    When we created the DAL project in VS .NET in the last article, it created a default class file called Class1.vb. You can delete this since we won't be using standard classes for our Data Handlers, just yet. Instead, we will use a special class that comes in the .NET arsenal called a Component, which extends from the System.ComponentModel.Component class. A Component Class provides a designer tray similar to the one found in Web forms that allow you to drag non-UI components from the Toolbox or Server Explorer on to the class (like DataSets, DataAdapters, etc). These constituent components can then be manipulated through the Properties panel or in some cases, special wizards.

    NOTE: .NET Components should not be confused with COM components.

    Let's create our first Handler class. To add a component class to the DAL project, right-click the project node in the Solution Explorer (Fig #1) then select Add, then Add Component...


    Fig #1 - Add a Component Class

    When the Add New Item dialog (Fig #2) appears, give it the name WidgetHandler.
    Fig #2 - Adding a Component class with 'Add a New Item' Dialog

    Click the Open button and the WidgetHandler component class will appear with the designer ready to add DataAdapters.

    Adding DataAdapters to the Handler Components

    DataAdapters are the primary means available for filling or updating a DataSet. As we mentioned in the last article, DataAdapters are built of up to 4 separate Command objects - one for each SELECT, INSERT, UPDATE and DELETE action. You could code these Command objects manually, or you can let VS .NET do it for you.

    Building by dragging from tables

    If you haven't done this yet, you won't believe how easy it is. Simply open the Tables node from the local database connection we set up in the last article, WidgetStore_Local. Click and drag one of the tables onto the WidgetHandler component's Designer Tray. For the first DataAdapter, I have selected the Widgets table. VS .NET automatically creates a configured DataAdapter with a corresponding Connection for us and displays an icon for each in the tray (Fig #3).


    Fig #3 - Viewing the new DataAdapter in the WidgetHandler designer

    In addition, you may have noticed that VS .NET is smart enough to know that the data source for the tables is SQL Server, so it has kindly provided the SqlClient versions of the Connection and DataAdapter objects.

    I recommend renaming the object references in the designer right after they are created. To do this, select one of the icons and change the (Name) property in the Properties panel. If there is only one connection, I like to rename it simply to "conn". If there is more than one, I would probably name it "widgetStoreConnection". Also, VS .NET kindly renames any references to the connection that DataAdapters are using. For DataAdapter names, I use the entity name followed by "DataAdapter", so for the Widgets DataAdapter, we will have "widgetsDataAdapter". I use camel-case formatting for these variable names since they are private in scope (actually they are Friend by default - close enough :). In fact, you could also prefix the names with and underscore or "m_" to indicate that they are private members of the entire class.


    Fig #4 - Renaming the components in the WidgetHandler designer

    Here are some guidelines from Microsoft for naming fields or members of a .NET class:

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconfieldusageguidelines.asp

    You may have noticed that I am purposefully avoiding the use of Hungarian Notation. The growing consensus is that Hungarian Notation has little place in .NET (even Microsoft states this in the above link). This is particularly due to the fact that with property driven tools like the component designer we just saw and the powerful Intellisense in the code window, it is almost difficult to not know what type of object you are dealing with. Here are a few more opinions on the use of Hungarian Notation.

    http://www.msdnaa.NET/Resources/Display.aspx?ResID=1805
    http://www.aspalliance.com/Ambrose/Articles/Hungarian.aspx

    When you have a chance, experiment with the component designer; you might find some interesting things. For example, if you drag a second table onto the component, VS .NET recognizes that the table comes from a connection that you have already defined, so it will continue to use the same object reference when creating the command objects. You will find that you can also drag more than one table onto the designer at a time. In this case, a separate DataAdapter will be created for each table. In addition, you can expand the table node in the Server Explorer and select a subset of fields from the table to drag over to create the DataAdapter. If you need to create joins, you will need to follow the techniques described in the next section.

    As constituent components are added to the tray, corresponding code is auto-generated for them in the code portion of the component. (This is almost like the code-behind of ASP.NET Web forms.) If you right-click on the component designer, and select View Code, you can see what VS .NET auto-generated for the DataAdapter. Expand the "Component Designer generated code" region in the code window and scroll down to the InitializeComponent method. Notice that the INSERT, UPDATE and DELETE command objects are pre-configured with a parameter collection. Very nice!

    Table Mappings

    Table mappings in the DataAdapter provide useful information for filling DataSets. Remember, DataSets can contain more than one table. Table mappings make it easier for the DataAdapter to ensure that the right table is being filled.

    Building the Stored Procedures

    Now, if you inspected the code generated above, you may have noticed that the four command objects in the previous example were created by embedding the SQL directly in the code. As we mentioned earlier, we want to avoid using SQL DML statements directly in our code for security reasons. Instead, we want to use stored procedures that perform these actions. In the olden days (before VS .NET) we would have to get cracking to write SQL scripts to build 4 separate stored procedures to handle the SELECT, INSERT, UPDATE, and DELETE statements for every entity in our system. Today, VS .NET can automate this script generation for us. There are two ways to do this. First, we can reconfigure a DataAdapter that was previously built by using the drag table method. Second, we can create a new DataAdapter by dragging a new SqlDataAdapter icon from the Data section of the Toolbox into our Handler component. Both of these techniques will launch the DataAdapter Configuration Wizard. However, I recommend the second method, since the first can disrupt the table mappings.

    Configuring the DataAdapter

    The DataAdapter Configuration Wizard (Fig #5) starts with a welcome screen explaining what will happen. All you have to do is click Next to start.


    Fig #5 - The DataAdapter Configuration Wizard

    Choosing the Data Connection

    The next step is to choose the data connection to build the DataAdapter from. The dropdown list (Fig #6) will provide a list of all the connections predefined in the Server Explorer's Data Connections. Select our current connection, WidgetStore_Local.


    Fig #6 - Choosing a Data Connection for the DataAdapter

    Choosing the Query Type

    Next, choose the query type you want to use for the DataAdapter. For our purposes, we will select the Create new stored procedures option (Fig #7).


    Fig #7 - Select 'Create new stored procedures' as the Query Type for the DataAdapter

    Generate Stored Procedures

    Next, we are presented with a screen to supply the SELECT statement that will be used to create our stored procedures (Fig #8).


    Fig #8 - Type in the SELECT statement for the DataAdapter

    If you are reconfiguring a previous generated DataAdapter, the SQL statement will appear in the box. If you are starting from scratch, you can just type in the SELECT statement. Don't be afraid to use SELECT * in your SELECT statement. The wizard will find all the current fields from the referenced table and insert them in place of the star. A nice time saver!

    From this SELECT statement, the wizard will automatically create the corresponding SELECT, INSERT, UPDATE, and DELETE stored procedures using the same fields in the SELECT statement.

    Query Builder

    You can provide a very complex SELECT statement with joins if you need to - practically anything goes. If you need some help, you can click the Query Builder... button to bring up a graphical design tool for building queries, similar to the one in Enterprise Manager.


    Fig #9 - Create the SELECT statement using the visual Query Builder tool

    Advanced Options

    On the SELECT statement wizard page, there is also a button for Advanced Options.... Click this and the Advanced SQL Generation dialog (Fig #10) appears with three advanced options for your consideration:


    Fig #10 - Set Advanced Options for the DataAdapter

    Generate INSERT, UPDATE and DELETE statements

    If you only need the DataAdapter for retrieving data, then be sure to unselect this option to avoid creating unnecessary command objects for INSERT, UPDATE and DELETE. If you unselect this option, the next two options become unselected as well.

    Use optimistic concurrency

    If selected, VS .NET will add conditions to the SQL WHERE clause of both the UPDATE and DELETE statements to check if the original record has changed on the server since the record was retrieved in our disconnected DataSet. If the original record has changed, then the action will fail, and the DataAdapter will throw a DbConcurrencyException.

    Refresh the DataSet

    When this option is selected, VS .NET will add a SELECT statement to both the UPDATE and INSERT stored procedures to reselect the record that was just updated or inserted. When the DataAdapter encounters a returned resultset after either of these actions, it will insert the values back into the corresponding rows in the DataSet. This will allow the DataSet to reflect values that are set by the server, such as auto-increment IDs.

    Create the Stored Procedures

    Once your SELECT statement has been defined, VS .NET is ready to create the stored procedures in the database for you (Fig #11). All you need to do is define what name to use for each. PLEASE, DO NOT USE THE DEFAULT NAMES THAT ARE PROVIDED. I recommend the following pattern: first start with a prefix of "dalsp_" followed by the entity name, another underscore and the name of the command. For example, the SELECT procedure for Widgets would be "dalsp_Widgets_Select".

    If you use a prefix, DO NOT USE "sp_". This prefix is used by the SQL Server to identify system procedures (that's why many of the utility stored procedures that come with SQL Server have this prefix). If you give your procedures this prefix, SQL Server will look for them in the master database first, then the context database, thus unnecessarily wasting resources.


    Fig #11 - Name the Stored Procedures for the DataAdapter

    Finally, the wizard confirms that we are complete (Fig #12).
    Fig #12 - View the Wizard Results

    Click Finish to End.

    Viewing the Auto-Generated Stored Procedures

    To view the stored procedures we created, expand the Stored Procedure node under our connection in the Server Explorer (Fig#13):


    Fig #13 - Viewing the newly created Stored Procedures in Server Explorer

    Double-click any of the stored procedures to view the contents. They will be presented as ALTER PROCEDURE statements so that you can make any editing changes you want, right inside of VS .NET!

    Here is what you will find:

    The SELECT Procedure

    The SELECT procedure is straightforward - no surprises.

    ALTER PROCEDURE dbo.dalsp_Widgets_Select

    AS

    SET NOCOUNT ON;

    SELECT WidgetID, Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT

    FROM Widgets

    The INSERT Procedure

    In the INSERT stored procedure, there are two statements - an INSERT statement followed by a SELECT statement.VS .NET provides the SELECT statement to refresh the DataSet. This will be very important if your DataSet has multiple tables with relationships since we will be using placeholder values for keys until we receive the actual values. Once the new values are in place, the DataSet will perform a cascade update to all the related child records.

    ALTER PROCEDURE dbo.dalsp_Widgets_Insert

    (

    @Name varchar(50),

    @Description varchar(200),

    @WidgetTypeID int,

    @Qty int,

    @Price money,

    @CreateDT datetime,

    @UpdatedDT datetime

    )

    AS

    SET NOCOUNT OFF;

    INSERT INTO Widgets(Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT) VALUES (@Name, @Description, @WidgetTypeID, @Qty, @Price, @CreateDT, @UpdatedDT);

    SELECT WidgetID, Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT FROM Widgets WHERE (WidgetID = @@IDENTITY)

    The UPDATE Procedure

    In the UPDATE procedure, we see a lengthy WHERE clause with many conditions testing the original values of the DataSet with the values currently in the database. This is how VS .NET tests for concurrency conflicts. If there is no match, then that means someone has changed (or deleted) the record before we had a chance to perform the update. As a result, the updated record count returned will be zero (note the SET NOCOUNT OFF). The DataAdapter will interpret this condition as a concurrency conflict and throw a DBConcurrencyException.

    ALTER PROCEDURE dbo.dalsp_Widgets_Update

    (

    @Name varchar(50),

    @Description varchar(200),

    @WidgetTypeID int,

    @Qty int,

    @Price money,

    @CreateDT datetime,

    @UpdatedDT datetime,

    @Original_WidgetID int,

    @Original_CreateDT datetime,

    @Original_Description varchar(200),

    @Original_Name varchar(50),

    @Original_Price money,

    @Original_Qty int,

    @Original_UpdatedDT datetime,

    @Original_WidgetTypeID int,

    @WidgetID int

    )

    AS

    SET NOCOUNT OFF;

    UPDATE Widgets SET

    Name = @Name,

    Description = @Description,

    WidgetTypeID = @WidgetTypeID,

    Qty = @Qty,

    Price = @Price,

    CreateDT = @CreateDT,

    UpdatedDT = @UpdatedDT

    WHERE (WidgetID = @Original_WidgetID)

    AND (CreateDT = @Original_CreateDT OR @Original_CreateDT IS NULL AND CreateDT IS NULL)

    AND (Description = @Original_Description OR @Original_Description IS NULL

    AND Description IS NULL)

    AND (Name = @Original_Name)

    AND (Price = @Original_Price)

    AND (Qty = @Original_Qty)

    AND (UpdatedDT = @Original_UpdatedDT OR @Original_UpdatedDT IS NULL

    AND UpdatedDT IS NULL)

    AND (WidgetTypeID = @Original_WidgetTypeID);

     

    SELECT WidgetID, Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT

    FROM Widgets WHERE (WidgetID = @WidgetID)

    The DELETE Procedure

    In the final stored procedure, the DELETE statement performs the same kind of concurrency check that was used by the UPDATE statement.

    ALTER PROCEDURE dbo.dalsp_Widgets_Delete

    (

    @Original_WidgetID int,

    @Original_CreateDT datetime,

    @Original_Description varchar(200),

    @Original_Name varchar(50),

    @Original_Price money,

    @Original_Qty int,

    @Original_UpdatedDT datetime,

    @Original_WidgetTypeID int

    )

    AS

    SET NOCOUNT OFF;

    DELETE FROM Widgets

    WHERE (WidgetID = @Original_WidgetID)

    AND (CreateDT = @Original_CreateDT OR @Original_CreateDT IS NULL AND CreateDT IS NULL) AND (Description = @Original_Description OR @Original_Description IS NULL

    AND Description IS NULL)

    AND (Name = @Original_Name)

    AND (Price = @Original_Price)

    AND (Qty = @Original_Qty)

    AND (UpdatedDT = @Original_UpdatedDT OR @Original_UpdatedDT IS NULL

    AND UpdatedDT IS NULL)

    AND (WidgetTypeID = @Original_WidgetTypeID)

    GO

    Variations on SELECT Statements for the Same Entities

    So far, we have only discussed building SELECT statements that will retrieve all the records. Of course, this is not always practical. Say, for instance, we want to be able to get all orders for a specific customer.

    SELECT statements usually fall into one of three patterns:

    • List - these SELECT statements usually provide the entire contents of a table and will denormalize lookup values.
    • Single Entity - usually for selecting a single record from the database for editing.
    • Filtered Subset - useful for getting related content such as Orders related to a customer

    To create these alternate statements, we would create "read-only" DataAdapters, meaning DataAdapters that only have a SELECT command. When you are building a read-only DataAdapter, remember to go into the Advanced Options (Fig #10) dialog and unselect the Generate Insert, Update and Delete statements option.

    Here is an example of a list statement we may use for retrieving widgets. We will use this in a DataAdapter named WidgetsListDataAdapter.

    
    SELECT Widgets.WidgetID, Widgets.Name, Widgets.Description, 
        Widgets.Qty, Widgets.Price, Widgets.CreateDT, Widgets.UpdatedDT,
        WidgetTypes.Name AS Type 
    FROM Widgets 
        INNER JOIN WidgetTypes 
        ON Widgets.WidgetTypeID = WidgetTypes.WidgetTypeID
    
    
    When specifying SQL statements with joins in the DataAdapter Wizard, you may experience some problems when the stored procedure is created. In this case, the wizard tries to be a little too smart and adds the WidgetTypeID of the WidgetType table as the primary key. If this happens, re-edit the stored procedure before creating a DataSet (which we will explain shortly). Here is an example of the SELECT statement you might use for filtering Orders by Customer:
    
    SELECT
        Orders.*
    FROM
        Orders
    WHERE
        (CustomerID = @CustomerID)
    
    
    When the above SQL statement is run through the DataAdapter wizard, The SELECT command object is built with a parameter for @CustomerID. You can add any parameter you want to your SQL statements, simply by prefixing the value with the @ symbol. You could also create a separate SELECT command for this parameterized query and swap it into the DataAdapter. Just be sure to swap the original back after the process.

    Another adjustment you may want to make to the SELECT statement is filtering out deleted items. In transactional systems like our Widget Store, it is rare that you would actually delete records from the database, particularly, when other historical records are dependent on them. Instead, you would change their status to "deleted" (or something else to indicate that they are inactive). In our Orders table, we use a bit field to represent the deleted status. Simply add another WHERE clause of Deleted = 0. If you need to list all the records, whether they are deleted or not, just build an additional "show all" DataAdapter.

    Altering the INSERT, UPDATE, and DELETE Stored Procedures

    The default INSERT, UPDATE, and DELETE stored procedures are good for most operations; however, you may require special conditions to be handled to maintain your data requirements and auditing rules. For instance, we want to be sure that the Create and Update datetime values in our records are set by SQL Server, not by the business logic of the application. This will prevent any inconsistencies in time that may occur across multiple machines that are using the DAL. In addition, it ensures that the values will be set every time there is an INSERT or UPDATE statement. The following INSERT procedure demonstrates this.

    ALTER PROCEDURE dbo.dalsp_Widgets_Insert

    (

    @Name varchar(50),

    @Description varchar(200),

    @WidgetTypeID int,

    @Qty int,

    @Price money,

    @CreateDT datetime,

    @UpdatedDT datetime

    )

    AS

    SET NOCOUNT OFF;

    INSERT INTO Widgets(Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT) VALUES (@Name, @Description, @WidgetTypeID, @Qty, @Price, getDate(), getDate());

     

    SELECT WidgetID, Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT FROM Widgets WHERE (WidgetID = @@IDENTITY)

    Notice that I have left the @CreateDT and @UpdatedDT parameters in place so that we do not have to make manual changes to the DataAdapter. I just simply ignore the value coming in. I also use the getDate() function to set the current datetime for the two fields.

    In the UPDATE statement, I have commented out the update for the CreateDT field, since this value should not change. I have again used the getDate() method for setting the UpdateDT field.

    CREATE PROCEDURE dbo.dalsp_Widgets_Update

    (

    @Name varchar(50),

    @Description varchar(200),

    @WidgetTypeID int,

    @Qty int,

    @Price money,

    @CreateDT datetime,

    @UpdatedDT datetime,

    @Original_WidgetID int,

    @Original_CreateDT datetime,

    @Original_Description varchar(200),

    @Original_Name varchar(50),

    @Original_Price money,

    @Original_Qty int,

    @Original_UpdatedDT datetime,

    @Original_WidgetTypeID int,

    @WidgetID int

    )

    AS

    SET NOCOUNT OFF;

    UPDATE Widgets SET

    Name = @Name,

    Description = @Description,

    WidgetTypeID = @WidgetTypeID,

    Qty = @Qty,

    Price = @Price,

    /* CreateDT = @CreateDT, */

    UpdatedDT = getDate()

    WHERE (WidgetID = @Original_WidgetID)

    AND (CreateDT = @Original_CreateDT OR @Original_CreateDT IS NULL AND CreateDT IS NULL)

    AND (Description = @Original_Description OR @Original_Description IS NULL

    AND Description IS NULL)

    AND (Name = @Original_Name)

    AND (Price = @Original_Price)

    AND (Qty = @Original_Qty)

    AND (UpdatedDT = @Original_UpdatedDT OR @Original_UpdatedDT IS NULL

    AND UpdatedDT IS NULL)

    AND (WidgetTypeID = @Original_WidgetTypeID);

     

    SELECT WidgetID, Name, Description, WidgetTypeID, Qty, Price, CreateDT, UpdatedDT

    FROM Widgets WHERE (WidgetID = @WidgetID)

    As we mentioned earlier, we may want to prevent the business logic of an application from being able to actually delete a record. We can alter our DELETE stored procedure so that the DataAdapter thinks it is actually deleting, but all we are doing is setting the Deleted field of the record equal to 1.

    ALTER PROCEDURE dbo.dalsp_Orders_Delete

    (

    @Original_OrderID int,

    @Original_CreateDT datetime,

    @Original_CustomerID int,

    @Original_Deleted bit,

    @Original_OrderDT datetime,

    @Original_OrderStatusID int,

    @Original_ShippingAddress varchar(200),

    @Original_ShippingCity varchar(50),

    @Original_ShippingState varchar(50),

    @Original_ShippingZip varchar(50),

    @Original_SubTotal money,

    @Original_Tax money,

    @Original_UpdateDT datetime

    )

    AS

    SET NOCOUNT OFF;

    UPDATE Orders SET Deleted = 1,

    UpdatedDT = getDate()

    WHERE (OrderID = @Original_OrderID)

    AND (CreateDT = @Original_CreateDT OR @Original_CreateDT IS NULL AND CreateDT IS NULL) AND (CustomerID = @Original_CustomerID)

    AND (Deleted = @Original_Deleted)

    AND (OrderDT = @Original_OrderDT)

    AND (OrderStatusID = @Original_OrderStatusID OR @Original_OrderStatusID IS NULL

    AND OrderStatusID IS NULL)

    AND (ShippingAddress = @Original_ShippingAddress OR @Original_ShippingAddress IS NULL

    AND ShippingAddress IS NULL)

    AND (ShippingCity = @Original_ShippingCity OR @Original_ShippingCity IS NULL

    AND ShippingCity IS NULL)

    AND (ShippingState = @Original_ShippingState OR @Original_ShippingState IS NULL

    AND ShippingState IS NULL)

    AND (ShippingZip = @Original_ShippingZip OR @Original_ShippingZip IS NULL

    AND ShippingZip IS NULL)

    AND (SubTotal = @Original_SubTotal OR @Original_SubTotal IS NULL AND SubTotal IS NULL) AND (Tax = @Original_Tax OR @Original_Tax IS NULL AND Tax IS NULL)

    AND (UpdateDT = @Original_UpdateDT OR @Original_UpdateDT IS NULL AND UpdateDT IS NULL);

     

    Completing the DataAdapters

    Here is a list of the DataAdapters to be added to the Widget Handler:

    Widgets

    Provides read and update for single widgets

    WidgetsList

    Provides denormalized readonly list of widgets

    WidgetType

    Provides readonly list of WidgetTypes

    Orders

    Provides read and update for all Orders

    OrderDetails

    Provides read and update for all Order Details

    Customers

    Provides read and update for all Customers

    OrdersByCustomers

    Provides readonly for all Orders for all customers

    Settings Permissions for the New Stored Procedures

    Once you have created all the stored procedures, you will need to grant the WidgetStoreDAL login account permission to use them. If you remember, our account has been restricted from using tables directly, so the stored procedures will be the only means of access to the data.

    When you create a new SQL login account, by default, it does not have permissions to access any tables. However, you should probably check just to make sure. To do this, open the Users folder under our database in Enterprise Manager, then double-click the WidgetStoreDAL user entry. This will bring up the Database User Properties dialog (Fig #14).


    Fig #14 - Changing Permissions for the WidgetStoreDAL

    Select the Permissions... button and ensure that all the tables are unselected. Then select the EXEC checkboxes for each of the DAL stored procedures we have just created (Fig #15).


    Fig #15 - Setting Execute permissions for the Stored Procedures

    If you don't have Enterprise Manager, you can grant the login account execute permissions to our stored procedures:

    GRANT EXECUTE ON [dbo].[dalsp_Orders_Select] TO [WidgetStoreDAL]

    GO

    Update Your SQL Create Scripts

    Once all your stored procedures are created for your DataAdapters, don't forget to update the Create scripts in our database project. To do this, right-click the database connection node in Server Explorer and select Generate Scripts... from the popup menu.

    Creating Typed DataSets

    Well, now it's time to get back on track with our DAL, and we will do that by creating some Typed DataSets.

    The easiest way to create a Typed DataSet is to start with a defined DataAdapter. Just right-click any of the DataAdapter icons in the component designer and select Generate DataSet - or if the icon is selected, you can also go to the Data menu item on the top menu bar and select Generate DataSet. When you have done that, the Generate DataSet dialog (Fig #16) will popup:


    Fig #16 - The Generate DataSet dialog

    Select New option, then type in a name for your DataSet. I recommend using the EntityDataSet pattern, for instance, CustomersDataSet.

    Note that a checklist of all the DataAdapters is presented in the dialog along with the name of the "table" they represent. This means that you can specify more than one table in a DataSet. (You would use each DataAdapter to fill the DataSet consecutively, as we will demonstrate shortly.) When you click OK, this wizard will create an XML Schema Definition (.XSD) file (Fig #17) with the name you provided. The generated schema documents the names and data types for each of the fields referenced in each selected DataAdapter table.


    Fig #17 - Viewing the DataSet Schema in design mode

    At the bottom of the XSD design panel, you will see two buttons for toggling the view of the Schema Definition - DataSet and XML. Click XML to see the code behind the design view.

    <?xml version="1.0" standalone="yes"?>

    <xs:schema id="CustomersDataSet" targetNamespace="http://www.tempuri.org/CustomersDataSet.xsd" xmlns:mstns="http://www.tempuri.org/CustomersDataSet.xsd" xmlns="http://www.tempuri.org/CustomersDataSet.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">

        <xs:element name="CustomersDataSet" msdata:IsDataSet="true">

            <xs:complexType>

                <xs:choice maxOccurs="unbounded">

                    <xs:element name="Customers">

                        <xs:complexType>

                            <xs:sequence>

                                <xs:element name="CustomerID" msdata:ReadOnly="true"

    msdata:AutoIncrement="true" type="xs:int" />

                                <xs:element name="Firstname" type="xs:string"

    minOccurs="0" />

                                <xs:element name="Lastname" type="xs:string" minOccurs="0" />

                                <xs:element name="Company" type="xs:string" minOccurs="0" />

                                <xs:element name="Address" type="xs:string" minOccurs="0" />

                                <xs:element name="City" type="xs:string" minOccurs="0" />

                                <xs:element name="State" type="xs:string" minOccurs="0" />

                                <xs:element name="Zip" type="xs:string" minOccurs="0" />

                                <xs:element name="CreditAvailable" type="xs:boolean"

    minOccurs="0" />

                                <xs:element name="CreateDT" type="xs:dateTime" />

                                <xs:element name="UpdateDT" type="xs:dateTime"

    minOccurs="0" />

                                <xs:element name="Deleted" type="xs:boolean" />

                            </xs:sequence>

                        </xs:complexType>

                    </xs:element>

                </xs:choice>

            </xs:complexType>

            <xs:unique name="Constraint1" msdata:PrimaryKey="true">

                <xs:selector xpath=".//mstns:Customers" />

                <xs:field xpath="mstns:CustomerID" />

            </xs:unique>

        </xs:element>

    </xs:schema>

    On the Generate DataSet dialog(Fig #16), you may have noticed that you can select an option to Add this DataSet to the designer. When selected, this option will create a member variable in the component class for the DataSet. Because we will be using a stateless model for our DAL component, it will be more efficient to create only the instances required for gathering the specific data we need. There will be less overhead if we don't instantiate all the DataSets every time.

    The DataSet Class

    The XSD file is just the beginning. The real magic behind the XSD is the Typed DataSet class that is auto-generated behind the scenes. This class will be the basis of the Object to Relational mapping we discussed in the last article. If you remember, Typed DataSets provide a collection of table and row objects that are specific to the entities they represent. Here is a diagram of the structure of an Untyped DataSet.

    
    DataSet
    	DataTables -  collection, numeric or name index
    		DataTable
    			DataColumns
    			DataRows - collection, numeric index
    				DataRow
    					Items - field collection - numeric or name index
    
    
    Names can be used for indexes with Untyped DataSets as long as the schema is loaded.

    For Typed DataSets the structure is a bit more concise. For instance, a Typed DataSet class provides named properties for each of the DataTables contained within. In addition, the class contains another object for DataRows.

    
    CustomersDataSet
    	Customers (CustomersDataTable) - direct instance
    		Column Properties
    		CustomersRows
    			CustomersRow
    			 	Properties (Fields)
    
    
    Our CustomersDataSet contains one public read-only property, Customers, which returns a CustomersDataTable. The CustomersDataTable is an inner class which inherits from the DataTable class. It carries column definitions and a collection of CustomersRows. Each CustomersRow contains numerous properties that each match fields in the Customers entity.

    Changing the XSD

    Now that we have a schema, let's look at how we can make a few adjustments to change the way the DataSet operates.

    Setting fields to Read Only

    Remember earlier we configured our INSERT and UPDATE stored procedures to not allow the CreateDT and UpdatedDT fields to be changed by the business logic of the app - only through the stored procedure.

    We can let our DAL user know this by setting these elements in the XSD to ReadOnly. To do this, select the appropriate element in the designer. In the Properties panel, change the ReadOnly property to True. The primary key for the Widgets schema is automatically set to ReadOnly. (Fig #18)


    Fig #18 - Setting schema properties

    Setting the Auto Increment Seed and Step values

    If you reviewed our database schema from the last article, you would have noticed that all the primary keys for our tables were set to be Identity fields. When our schema was created, the primary keys were discovered; however, the auto-increment settings were not. If you don't change this you will be able to add your first record to the DataSet but not the second -- the unique constraint will prevent it.

    Fortunately, the DataSet provides a mechanism for simulating an auto-increment. I say, "simulate" because it is important to remember that DataSets are disconnected as we are using them. This means the Primary Key will not contain the actual auto-increment from the database until the DataSet is updated. Therefore, the auto-increment value is relative to the DataSet only. For this reason, I have found it wise to set the AutoIncrementSeed to 0 and the AutoIncrementStep to -1. These settings can also be set programmatically.

    Setting a Caption

    Captions can also be applied to the elements in your entities, This is a great way to provide meta-data to the presentation layer so that consistent labels can be used for specific fields. You can access the Caption values programmatically through the corresponding column property as such:

    
    
    Changing the entity names when singular or plural

    You may have noticed that when VS .NET created the parts of our DataSet, it simply used the entity name as a prefix to the parts. Since our entity name, Customers, is plural, it can make singular items awkward sounding, such as "CustomersRow". The XSD file provides the ability to specify different prefixes for the singular and plural parts of the DataSet. You can learn more about this from the following MSDN article.

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconusingannotationswithtypeddataset.asp

    Creating Relationships

    As we mentioned before, DataSets not only can contain more than one table, they can also provide relationships between those tables. These relationships can enforce referential integrity, just like a database. You will find this feature very useful for validating values from lookup tables, like the WidgetsType table. Also, it is useful for controlling relations to child records - like OrderDetails of the Orders table.

    To get started, we will need to create a DataSet with two tables. We currently have a DataSet for the Widgets table so lets add the WidgetsType table to the same DataSet. You can do this by right-clicking the WidgetsType DataAdapter icon on the Handler class designer. From the popup menu, select Generate DataSet.... Now, rather than creating a new DataSet, you will select WidgetStore.DAL.WidgetsDataSet from the Existing dropdown list. Click OK and the WidgetTypes table will be added to the Widgets DataSet. The next step is to build the relationship. Open the WidgetsDataSet.XSD file and select one of the entity boxes. Then from the Schema menu, select Add, then Relation. The Edit Relation dialog (Fig #19) will then appear.


    Fig #19 - Editing parameters for a DataSet Relation

    Next you must identify which is the Parent element and which is the Child element from the dropdowns provided. For our example, select WidgetTypes as the parent and Widgets as the child. You may have noticed that the dialog will try to figure out which elements in the DataSet belong where. Sometimes this works; however, you should carefully review the choices it makes. If the parent table has a primary key, the dialog will automatically select this key for the Key option. In the Fields section, you should identify the primary Key Fields from the parent element and the Foreign Key Fields in the child element.

    Relationship Options and Rules

    There are additional DataSet Properties on the Edit Relation dialog you can use to configure your relationship.

    Create foreign key constraint only

    When you create a relationship in a DataSet, the DataSet code will be auto-generated to handle constraining values in the foreign key fields in addition to providing properties and methods for handling child records from a parent. If you select the Create foreign key constraints only option, the DataSet will exclude the child handling methods, making the DataSet a bit more lightweight. This may be a good scenario for situations were you only want to include a parent table for lookup values, and you don't plan to manipulate thee records in the parent table.


    Fig #20 - Viewing the DataSet Relation in design mode

    Action Rules

    Another consideration for DataSet relationships is what to do to child records when certain actions are performed on the data in parent records, such as updates and deletes. To support this, the Edit Relation dialog lets you set behaviors for these actions.

    Update rule

    The Update Rule handles situations where an existing primary key of a parent row that has related children rows is changed. The following options can be set:

    • None - no change will be made to foreign key values in the child table.
    • Cascade - the new parent key value will be applied to all related foreign key values.
    • SetNull - the related foreign key values will be set to null.
    • SetDefault - return the related foreign key to the default value defined in the DataSet.

    All the options but Cascade have the potential of creating orphan records in the child table, so these setting should be used with care. The default setting is Cascade.

    It is important to leave this set to default (or Cascade) if you want primary keys based on Identity values to be updated to children once they are returned by the database. Remember the auto-increment values we have when we are disconnected are just temporary placeholders. The database will report back the actual values after the DataAdapter performs the update.

    Delete rule

    The Delete Rule handles situations where a parent row that has related children records is deleted from the DataSet. The following options can be set:

    • None - no change will be made to the related child rows.
    • Cascade - delete all the child records that are related to the deleted parent record.
    • SetNull - the foreign key values will be set to null.
    • SetDefault - return the foreign key to the default value defined in the DataSet.

    Again, all the options but Cascade, have the potential of creating orphan records in the child table. The default setting is Cascade.

    Accept/Reject rule

    The Accept/Reject Rule handles situations where a parent row that has related children records has the AcceptChange or RejectChange methods called on it. There are only two options that can be set for this:

    • None - no change will be made to the related child rows.
    • Cascade - all the child records that are related to the parent record will be set to the same Accept/Reject status.

    Adding Methods to the Handler Class

    Once you have set up some Typed DataSets, you are ready to start creating methods for retrieving and updating data.

    At the beginning of this article, I discussed how the context for your DAL can influence how you partition your Handler classes. Context can also affect how there are two approaches to designing the methods for the Handler class - 'stateless' and 'stateful'. In the stateless approach, the class will be designed so that each method can operate autonomously. Member variables will not be required to persist across method calls. This is generally the model best suited for use with Web Applications. In stateful designs, classes maintain data in member variables across method calls.

    Although our class is designed with member variables (for convenience), we will design the methods as stateless.

    Methods for Retrieving DataSets

    The primary purpose of these methods is to use the Fill command of DataAdapters to load data into DataSets. Let's take a look at two examples.

    Show GetCustomersDataSet - later GetCustomersWithOrders

    Getting a List

    In our first example, we get a DataSet containing all the widget rows.

        Public Function GetWidgetsListDataSet() As WidgetsListDataSet

            Dim ds As New WidgetsListDataSet()

            Try

                widgetsListDataAdapter.Fill(ds)

     

                Return ds

            Catch exc As Exception

                Dim msg As String

                msg = "Error reading Widget List from Database."

                Throw New DALException(msg, exc, ds)

            End Try

        End Function

    At this point, with all the preparation we have done, it only requires a few lines of code to fill the DataSet.

    Notice, there was no need to open the connection before using the DataAdapter when filling a single DataSet. The DataAdapter will test the connection that is referenced by the DataAdapter's command objects. If the connection is not open, the DataAdapter will automatically open it at the start of the process and close it when the process is complete.

    Getting a Single Entity

    Retrieving a single entity is very similar to the previous example, however, our method now has a parameter for the widgetID which it passes into the corresponding parameter of the SelectCommand.

        Public Function GetWidgetsDataSet(ByVal widgetID As Integer) As WidgetsDataSet

            Dim ds As New WidgetsDataSet()

            Try

                'fill types lookup before widget row

                widgetTypesDataAdapter.Fill(ds)

     

                'set the ID parameter for the widget

                widgetsDataAdapter.SelectCommand.Parameters("@WidgetID").Value = widgetID

                widgetsDataAdapter.Fill(ds)

     

                Return ds

     

            Catch exc As Exception

                Dim msg As String

                msg = "Error reading Widgets from Database."

                Throw New DALException(msg, exc, ds)

            End Try

        End Function

    Exception Handling

    A key objective of our DAL is to prevent unfriendly errors from being propagated to the consumer of the DAL.

    In each of the examples above, we've used a Try Catch block around the core working code. In the Catch statement, we catch a single generic exception and pass it into the constructor of the DALException class, a custom class we have created to analyze errors that have occurred and present a consistent and more acceptable exception to the DAL consumer. The DALException class inherits from the ApplicationException class, a .NET Framework class especially designed for creating custom exceptions in applications.

    The best part of this technique is that the exception processing code is centralized and encapsulated, meaning that the data retrieval code above can be very streamlined and easy to maintain. We could even move this DALException class into another project and deploy it as a global assembly that can be shared amongst other projects, making it even easier to maintain.

    Imports System.Data.SqlClient

    Imports System.Diagnostics

    Imports System.Configuration

    Imports System.Text

     

     

    Public Class DALException

        Inherits ApplicationException

     

        Const PERMIT_LOGGING As Boolean = False

     

        Public Enum DALExceptionCode

            Concurrency

            ConnectionFailure

            DatabaseFailure

            LoginFailure

            Constraint

            Transaction

            Locks

            General

        End Enum

     

        Private m_ExceptionCode As DALExceptionCode = DALExceptionCode.General

     

        Private m_DataSet As DataSet

        Private m_ErrorRows As DataRow()

     

        Private m_Log As New StringBuilder()

     

     

        Public Sub New(ByVal msg As String, ByVal exc As Exception, ByVal ds As DataSet)

            'Set the default message and inner exception

            MyBase.New(msg, exc)

     

            m_DataSet = ds

     

            'determine what type of exception we have

            If TypeOf exc Is ConstraintException Then 'thrown by the DataSet

                m_Log.Append("ConstraintException occured" & vbCrLf)

                m_ExceptionCode = DALExceptionCode.Constraint

     

            ElseIf TypeOf exc Is DBConcurrencyException Then

                m_Log.Append("DBConcurrencyException occured" & vbCrLf)

                m_ExceptionCode = DALExceptionCode.Concurrency

     

            ElseIf TypeOf exc Is SqlClient.SqlException Then

                m_Log.Append("SqlClient.SqlException occured" & vbCrLf)

                processSQLException(exc)

     

            ElseIf TypeOf exc Is DataException Then

                'this should handle all other providers - OLEDB, ODBC

                m_Log.Append("DataException occured" & vbCrLf)

                m_ExceptionCode = DALExceptionCode.Constraint

     

            ElseIf TypeOf exc Is Exception Then

                'anything left

                m_Log.Append("Exception occured" & vbCrLf)

                m_ExceptionCode = DALExceptionCode.General

     

            End If

     

            detectRowErrors(ds)

     

            m_Log.Append("Original Exception: " & vbCrLf)

            m_Log.Append(exc.ToString)

            logException(m_Log.ToString)

     

        End Sub

     

     

        Private Sub processSQLException(ByVal exc As SqlClient.SqlException)

     

            '4060-4 - could not open database

            '18450 - 18461 - login failed

            '18482,3,5 - could not connect to server

            '547 - foriegn key violation

            '2627 - Unique Index/Constraint violation

            '2601 - Unique Index/Constraint violation

            '1201 - 1223 locks

            '2502 - could not start transaction

            '2520-5 - could not find database

            If exc.Number >= 4060 And exc.Number <= 4064 Then

                m_ExceptionCode = DALExceptionCode.DatabaseFailure

                m_Log.Append("Database Failure - Could not open" & vbCrLf)

     

            ElseIf exc.Number >= 18450 And exc.Number <= 18461 Then

                m_ExceptionCode = DALExceptionCode.LoginFailure

                m_Log.Append("Login Failure" & vbCrLf)

     

            ElseIf exc.Number = 18482 Or exc.Number = 18483 Or exc.Number = 18485 Then

                m_ExceptionCode = DALExceptionCode.ConnectionFailure

                m_Log.Append("Connection Failure" & vbCrLf)

     

            ElseIf exc.Number = 2627 Or exc.Number = 2601 Or exc.Number = 547 Then

                m_ExceptionCode = DALExceptionCode.Constraint

                m_Log.Append("Constraint Violation" & vbCrLf)

     

            ElseIf exc.Number >= 1201 And exc.Number <= 1223 Then

                m_ExceptionCode = DALExceptionCode.Locks

                m_Log.Append("Lock Error" & vbCrLf)

     

            ElseIf exc.Number = 2502 Then

                m_ExceptionCode = DALExceptionCode.Transaction

                m_Log.Append("Transaction Error" & vbCrLf)

     

            ElseIf exc.Number >= 2520 And exc.Number <= 2525 Then

                m_ExceptionCode = DALExceptionCode.DatabaseFailure

                m_Log.Append("Database Failure - Could not find database" & vbCrLf)

     

            End If

     

            Dim err As SqlError

            m_Log.Append(vbCrLf)

            m_Log.Append("*******************" & vbCrLf)

            m_Log.Append("SQL ERRORS:" & vbCrLf)

            For Each err In exc.Errors

                'default

                m_Log.Append(err.ToString)

                m_Log.Append(vbCrLf)

            Next

     

        End Sub

     

        Private Sub logException(ByVal msg As String)

            Debug.WriteLine(msg)

            If PERMIT_LOGGING Then

     

                'try to get the app name from the config settings

                Dim appName As String

                Try

                    appName = ConfigurationSettings.AppSettings("Application Name")

                Catch exc As Exception

                    'don't do anything

                    Debug.Write(exc.ToString)

                End Try

     

                Try

                    If appName <> "" Then

                        If (Not EventLog.SourceExists(appName)) Then

                            EventLog.CreateEventSource(appName, appName)

                        End If

                        EventLog.WriteEntry(appName, msg, EventLogEntryType.Error)

                    Else

                        EventLog.WriteEntry("DALException", msg, EventLogEntryType.Error)

                    End If

     

                    Dim LogName As String = "Application"

                    If (Not EventLog.SourceExists(LogName)) Then

                        EventLog.CreateEventSource(LogName, LogName)

                    End If

     

                    ' Insert into event log

                    Dim Log As New EventLog()

                    Log.Source = LogName

                    Log.WriteEntry(msg, EventLogEntryType.Error)

     

                Catch exc As Exception

                    'don't do anything

                    Debug.Write(exc.ToString)

                End Try

     

            End If

     

        End Sub

     

        'cycle thru all tables and rows in the DataSet to find errors

        Private Sub detectRowErrors(ByVal ds As DataSet)

            Dim row As DataRow

            Dim table As DataTable

            Dim column As DataColumn

     

            If IsNothing(ds) Then Exit Sub

            If ds.HasErrors Then

                m_Log.Append("****************************" & vbCrLf)

                m_Log.Append("Errors Detected in DataSet: " & vbCrLf)

                m_Log.Append(vbCrLf)

                For Each table In ds.Tables

                    If table.HasErrors Then

                        'set error rows property

                        m_ErrorRows = table.GetErrors

                        m_Log.Append("Entity: " & table.TableName() & vbCrLf)

                        For Each row In table.Rows

                            If row.HasErrors Then

                                m_Log.Append(vbTab & "Key: " & row(table.PrimaryKey(0)) & vbCrLf)

                                m_Log.Append(vbTab & row.RowError & vbCrLf)

                                m_Log.Append(vbCrLf)

                                For Each column In row.GetColumnsInError

                                    m_Log.Append(vbTab & vbTab & "ColumnName: " & column.ColumnName & vbCrLf)

                                    m_Log.Append(vbTab & vbTab & "Error: " & row.GetColumnError(column) & vbCrLf)

                                    m_Log.Append(vbCrLf)

                                Next

                            End If

                        Next

                    End If

                Next

            End If

        End Sub

     

        '********************************************

        Public ReadOnly Property ErrorCode() As DALExceptionCode

            Get

                Return m_ExceptionCode

            End Get

        End Property

     

        Public ReadOnly Property OriginalDataSet() As DataSet

            Get

                Return m_DataSet

            End Get

        End Property

     

        Public ReadOnly Property RowsWithErrors() As DataRow()

            Get

                'this is only from one table

                Return m_ErrorRows

            End Get

        End Property

     

     

    End Class

    The New constructor provides a filter to determine the exact type of exception that is being processed. Once the type is determined, we call a subordinate method to do the rest of the work. The processSQLException method is where the real analysis takes place. We actually examine the resulting SQL error codes and translate them into our own collection found in the DALExceptionCode Enum.

    Logging errors

    Unfortunately, logging is one of the things that has become difficult in ASP.NET. Basically you have three choices: writing to the event log, the file system, or a database. Writing database errors to the database is unwise - especially if errors are resulting from an inability to get to the database. For the event log or the file system, your ASP.NET app must be running under an account that has sufficient permissions to access these resources. If the Web application is permitting anonymous users, then the app is impersonating the ASPNET account. This account does not have sufficient permissions to write to the file system or make the necessary registry changes to setup the source for event logs.

    The following knowledge base articles explains how to set these permissions or provide alternate impersonations on the Web site.

    http://support.microsoft.com/default.aspx?scid=KB;en-us;q317012
    http://support.microsoft.com/default.aspx?scid=KB;en-us;q306158

    The downside of these work-arounds is they require that you have administrative access to the server. If your production site is hosted by a third party, you may be out of luck. Another approach may be to send an e-mail reporting the error. I explain how to do this in my recent article, Tracking Visitors in ASP.NET.

    Methods for Updating

    To handle all INSERT, UPDATE, and DELETE actions on a DataSet, we only need a single update method for each DataAdapter. That is because the DataAdapter's Update method can handle all these functions.

        Public Sub UpdateWidgetsDataSet(ByVal ds As WidgetsDataSet)

            Try

                widgetsDataAdapter.Update(ds)

                ds.AcceptChanges()

     

            Catch exc As Exception

                Dim msg As String

                msg = "Error update Widgets."

                Throw New DALException(msg, exc, ds)

            End Try

        End Sub

    In the case of the previous example, we are probably only updating a single record at a time since the DataSet was retrieved as a single entity. There may be situations where you have a large number of records in a DataSet and only a few have changed. The following example demonstrates how to extract the specific types of changed records and update them separately through the GetChanges method of the DataSet.

        Public Sub UpdateCustomersDataSet(ByVal ds As CustomersDataSet)

            Try

                conn.Open()

                Dim insertedRows As System.Data.DataSet

                InsertedRows = ds.GetChanges(System.Data.DataRowState.Added)

                If Not IsNothing(InsertedRows) Then

                    customersDataAdapter.Update(insertedRows)

     

                    ds.Merge(insertedRows)

                End If

     

                Dim UpdatedRows As System.Data.DataSet

                UpdatedRows = ds.GetChanges(System.Data.DataRowState.Modified)

     

                If Not IsNothing(UpdatedRows) Then

                    customersDataAdapter.Update(UpdatedRows)

                    ds.Merge(UpdatedRows)

                End If

     

                Dim DeletedRows As System.Data.DataSet

                DeletedRows = ds.GetChanges(System.Data.DataRowState.Deleted)

     

                If Not IsNothing(DeletedRows) Then

                    customersDataAdapter.Update(DeletedRows)

                    ds.Merge(DeletedRows)

                End If

     

                ds.AcceptChanges()

            Catch exc As Exception

                Dim msg As String

                msg = "Error while updating Customers."

                Throw New DALException(msg, exc, ds)

            Finally

                conn.Close()

            End Try

     

            'TODO: save changes only

        End Sub

    Another option to consider for Update methods is to set the ContinueUpdateOnError property of the DataAdapter to True before calling the DataAdapter's Update method. When this is set, the DataAdapter will continue to process data rows and simply record the error with the row. It will not throw an exception. The offending rows can then be retrieved using the same process we use in the detectRowErrors method of the DALException class.

    Methods for Opening DataReaders

    If you already have SELECT commands built for DataAdapters, then you can just use those for executing a DataReader.

        Public Function GetWidgetsListDataReader() As IDataReader

            Dim dr As IDataReader

            Try

                widgetsListDataAdapter.SelectCommand.Connection.Open()

                dr = widgetsListDataAdapter.SelectCommand.ExecuteReader( _

                                    CommandBehavior.CloseConnection)

     

                Return dr

            Catch exc As Exception

                Dim msg As String

                msg = "Error reading Widget List from Database."

                Throw New DALException(msg, exc, Nothing)

            End Try

    Note that we have used the CloseConnection CommandBehavior on the ExecuteReader method. Also, keep in mind that you can't use a DataReader through a Web service.

    Note that I have used the generic interface IdataReader as the return type for this method. This way you can switch to or from SQLClient, OLEDB, or ODBC and your DAL consumer won't know the difference.

    Handling Transactions

    In most DALs, you are bound to run into situations where multiple records in multiple related tables need to be added or updated together. In these situations, the entire transaction should be completed together or not at all. To accommodate this, ADO.NET provides easy access to SQL Server transaction context through the Connection Object.

    In the example below, we take a look at updating both the Orders and OrderDetails tables from the OrdersDataSet.

    The process is started by acquiring a reference to a Transaction object after the Connection object is explicitly opened. That Transaction reference is then added to the INSERT, UPDATE, and DELETE commands of each of the DataAdapters.

        Public Sub UpdateOrdersDataSet(ByVal ds As OrdersDataSet)

            Dim trans As SqlTransaction

            Try

                conn.Open()

                trans = conn.BeginTransaction

                ordersDataAdapter.UpdateCommand.Transaction = trans

                ordersDataAdapter.InsertCommand.Transaction = trans

                ordersDataAdapter.DeleteCommand.Transaction = trans

     

                orderDetailsDataAdapter.UpdateCommand.Transaction = trans

                orderDetailsDataAdapter.InsertCommand.Transaction = trans

                orderDetailsDataAdapter.DeleteCommand.Transaction = trans

     

                ordersDataAdapter.Update(ds)

                orderDetailsDataAdapter.Update(ds)

                trans.Commit()

                ds.AcceptChanges()

     

            Catch exc As Exception

                trans.Rollback()

                Dim msg As String

                msg = "Error while updating Orders."

                Throw New DALException(msg, exc, ds)

            Finally

                conn.Close()

            End Try

        End Sub

    If the code manages to get to the end of the Try block without error, we call the Commit method on the transaction; otherwise, in the Catch block we call Rollback. Also, notice that we close the Connection in the final clause when done.

    If your DAL requires transactions to span across multiple databases, you may need to consider developing your DAL with Serviced Components that can be deployed under COM+ using Component Services.

    Reinitializing the Connection

    Keep in mind that the connection object we created in the designer is pointing to our development database. We don't want the app to have a hard coded connection, so we will reset the connection string to be whatever is set by our application. In this case, we will set the connection string value in the Web.config file. Using the System.Configuration.ConfigurationSettings.AppSettings method, we can retrieve this value at runtime.

        Public Sub New()

            MyBase.New()

     

            'This call is required by the Component Designer.

            InitializeComponent()

     

            'Add any initialization after the InitializeComponent() call

            Dim connStr As String = _

    System.Configuration.ConfigurationSettings.AppSettings("connectionString")

            If connStr <> "" Then

                conn.ConnectionString = connStr

            End If

     

        End Sub

    Web.config file

    In the appSettings block of the Web.config, create the following entry if you are using the WidgetSoreDAL account:

           <appSettings>

    <add key="connectionString" value="data source=(local)\NetSDK;

    initial catalog=WidgetStore;persist security info=True;

    user id=WidgetStoreDAL;password=password;"/>

     

           </appSettings>

    or, if you are using integrated security, create this entry:

           <appSettings>

                  <add key="connectionString" value="data source=(local)\NetSDK;

    initial catalog=WidgetStore;persist security info=False;

    Integrated Security=SSPI;"/>

     

           </appSettings>

    Calling from Web Page

    Now, we can start utilizing our DAL in the Widget Store Web application. We will just look at a few examples for managing widgets. (Fig #21)


    Fig #21 - The Widget Store Web App

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)

    Handles MyBase.Load

            'Put user code to initialize the page here

            Dim wh As New WidgetHandler()

            Try

                WidgetsListDataSet1 = wh.GetWidgetsListDataSet

                DataGrid1.DataBind()

     

            Catch exc As DALException

                'hide grid

                DataGrid1.Visible = False

                'show error

                ErrorLabel.Visible = True

                ErrorLabel.Text = exc.Message

     

                ErrorLabel.Text &= "<br>Contact system administrator."

     

            Catch exc As Exception

                'show error

                ErrorLabel.Visible = True

                ErrorLabel.Text = "An error occured while retrieving Widget data."

     

                ErrorLabel.Text &= "<br>Contact system administrator."

            Finally

     

            End Try

     

            Session("SelectedWidgetID") = Nothing

        End Sub

    On the first line of the Page_Load event we instantiate

    When a user selects a widget from the grid, we put the Primary Key for that widget in the session then redirect to EditWidget.aspx Web form

        Private Sub DataGrid1_SelectedIndexChanged(ByVal sender As System.Object, _

    ByVal e As System.EventArgs) Handles DataGrid1.SelectedIndexChanged

            Session("SelectedWidgetID") = DataGrid1.DataKeys(DataGrid1.SelectedIndex)

            Response.Redirect("EditWidget.aspx")

        End Sub

    The EditWidget.aspx appears as follows (Fig #22):


    Fig #22 - The Widget Store Web App

    The Page_Load event of our detail page has a similar structure to

        Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

            Dim widgetID As Integer

            If Not (Session("SelectedWidgetID")) Then

                widgetID = Session("SelectedWidgetID")

            Else

                Response.Redirect("WebForm1.aspx")

            End If

            If Not IsPostBack Then

     

                Try

                    WidgetsDataSet1 = handler.GetWidgetsDataSet(widgetID)

                Catch exc As DALException

                    'show error

                    ErrorLabel.Visible = True

                    ErrorLabel.Text = exc.Message

     

                    ErrorLabel.Text &= "<br>Contact system administrator."

                Catch exc As Exception

                    'show error

                    ErrorLabel.Visible = True

                    ErrorLabel.Text = "An error occured while retrieving Widget data."

     

                    ErrorLabel.Text &= "<br>Contact system administrator."

                Finally

     

                End Try

     

                'check row count

                bind()

     

                'get current row

                Dim row As WidgetsDataSet.WidgetsRow

                row = WidgetsDataSet1.Widgets.Rows(0)

     

                'select dropdown entry

                ddType.Items.FindByValue(row.WidgetTypeID).Selected = True

     

                'put the DataSet into the session

                Session("widgetsDataSet") = WidgetsDataSet1

            Else

                WidgetsDataSet1 = Session("widgetsDataSet")

            End If

        End Sub

     

        Private Sub bind()

            DataBind()

            Dim row As WidgetsDataSet.WidgetsRow

            row = WidgetsDataSet1.Widgets.Rows(0)

        End Sub

    Forcing an Error

    If you want to see how errors are propogated to the presentation layer, try changing the connection string to something that will fail.

    Summary

    Once again, we have covered a lot of territory. Hope I have been able to provide you with the basic framework for building your own Data Access Layer with ADO.NET and Visual Studio .NET. Please send me your comments and suggestions. I will be happy to assemble the ideas you are willing to share for a followup. I am definitely interested in different approaches to this problem. In our next article, we will take a more in-depth look at utilizing the DAL in Web apps, Web Services and Client-Server apps with .NET Remoting. We will also explore integration with a business rules layer and what to do when concurrency errors occur.

    Until then ...

    About the Author

    Wayne Plourde is a consulting Software Architect who began his career as a building architect twenty years ago. In 1995, he succumbed to the call of the World Wide Web, and since then has been designing sophisticated Web-based and client-server applications for corporations around the country. Wayne holds both MCSD and SCJP certifications and has just completed his .NET MCAD certification. You can contact Wayne at wayne@plourdenet.com or visit his Web site at http://www.plourdenet.com.

  • Rate This Article
    Not HelpfulMost Helpful
    1 2 3 4 5
    Supporting Products/Tools
    Proposion N2N
    Proposion N2N connects Microsoft .NET applications to Lotus Notes and Lotus Domino databases. This ADO.NET managed data provider allows you to perform blindingly fast queries and updates of Notes data from ASP.NET pages, .NET web services, Windows, or Mobile applications. An innovative SQL-like query language leverages the unique features of Notes and makes collaborative software accessible to relational database programmers.
    [Top]
    Other Articles
    Sep 15, 2005 - Building an Image Keyword System
    Unlike text-based file formats image files aren't made up of words, which makes searching for an image file by keyword difficult. Instead of being able to simply open the file to see what it contains, we're stuck looking at the text around it and other metadata to determine the image's meaning. In this article, Ziran Sun shows you how to build a simple database-based image keyword system that allows you to associate keywords with images and use these keywords to make finding images easier.
    [Read This Article]  [Top]
    Apr 7, 2005 - A Step-by-Step Guide To Using MySQL with ASP.NET - Part 2
    In the second part of of his article on using MySQL with ASP.NET, Ziran Sun covers how to add a new MySQL user to the database server, assign the user the appropriate permissions, connect to the database, and build a simple ASP.NET page to perform a query.
    [Read This Article]  [Top]
    Feb 10, 2005 - A Step-by-Step Guide To Using MySQL with ASP.NET - Part 1
    Back in the days of classic ASP, if you were building a database-driven web site, your choice was either to invest a lot of money to get a copy of Microsoft SQL Server (or some other enterprise-ready database) or invest a lot of time finding a way to deal with the performance and scalability limitations of Microsoft Access. Luckily these days there's another viable alternative: MySQL.
    [Read This Article]  [Top]
    Jan 27, 2005 - Moving a Database from SQL Server 7.0 to SQL Server 2000
    Moving or copying a SQL Server database from one machine to another requires a lot of preparation in order to ensure a smooth transfer. In this article, Dina Fleet Berry examines the different methods and highlights the different issues associated with each of them.
    [Read This Article]  [Top]
    Jan 6, 2005 - Debugging a SQL Stored Procedure from inside SQL Server 2000 Query Analyzer
    There are many times when using SQL Server 2000 Query Analyzer to debug SQL statements is a better choice than debugging in Visual Studio .NET. In this article, Dina Fleet Berry explains why and walks you through the debugging process step-by step.
    [Read This Article]  [Top]
    Nov 24, 2004 - Persisting .NET Objects to SQL Server Using SQLXML and Serialization
    As a follow up to his article on retrieving objects from SQL Server using SQLXML and serialization, Gianluca Nuzzo discusses saving objects back to SQL Server using a schema definition file and updategrams.
    [Read This Article]  [Top]
    Sep 14, 2004 - Transaction Processing in ADO.NET 2.0
    One area that stands out when comparing ADO.NET 1.x to ADO.NET 2.0 is transaction processing. Bill Ryan shows just how easy transaction processing has become with the TransactionScope object in ADO.NET 2.0.
    [Read This Article]  [Top]
    Sep 8, 2004 - Custom Object Data Binding with .NET
    Developers often use brute force coding to marshal data between the GUI and application objects. In this article, Luther Stanton explains how to use .NET's out-of-the box data-binding functionality to make this job much easier.
    [Read This Article]  [Top]
    Sep 2, 2004 - Queue MSMQ Messages from SQL Server
    Learn how to create a console application to queue a message in Microsoft Message Queuing (MSMQ) and then use an extended stored procedure to call the console application from a SQL Server trigger.
    [Read This Article]  [Top]
    Aug 30, 2004 - Tuning Up ADO.NET Connection Pooling in ASP.NET Applications
    Connection pooling increases the performance of Web applications by reusing active database connections instead of creating a new connection with every request. This article shows how to monitor the connection pool, diagnose a potential problem, and apply the appropriate fix.
    [Read This Article]  [Top]
    Mailing List
    Want to receive email when the next article is published? Just Click Here to sign up.

    Support the Active Server Industry

    internet.commediabistro.comJusttechjobs.comGraphics.com

    Search:

    WebMediaBrands Corporate Info

    Legal Notices, Licensing, Reprints, Permissions, Privacy Policy.
    Advertise | Newsletters | Shopping | E-mail Offers | Freelance Jobs