Vous êtes sur la page 1sur 135

ASP.

NET 4 D ATA ACCESS OVERVIEW


CAPTAIN
Tutorials for working with ASP.NET 4 and Visual Studio 2010

Author Godwin Oluyemi Awojobi. B.Sc Information Technology (Salem ( University Lokoja.)

Captain Global Technologies Bethel HouseTanke, Ilorin, Kwara State, Nigeria. 2347057987704 10/13/2012

An Overview of Inserting, Updating, and Deleting Data(VB)


In this tutorial we'll see how to map an ObjectDataSource's Insert(), Update(), and Delete() methods to the methods of BLL classes, as well as how to configure the GridView, DetailsView, and FormView controls to provide data modification capabilities.

Introduction
Over the past several tutorials we've examined how to display data in an ASP.NET page using the GridView, DetailsView, and FormView controls. These controls simply work with data supplied to them. Commonly, these controls access data through the use of a data source control, such as the ObjectDataSource. We've seen how the ObjectDataSource acts as a proxy between the ASP.NET page and the underlying data. When a GridView needs to display data, it invokes its ObjectDataSource's Select() method, which in turn invokes a method from our Business Logic Layer (BLL), which calls a method in the appropriate Data Access Layer's (DAL) TableAdapter, which in turn sends a SELECT query to the Northwind database.

Recall that when we created the TableAdapters in the DAL in our first tutorial, Visual Studio automatically added methods for inserting, updating, and deleting data from the underlying database table. Moreover, in Creating a Business Logic Layer we designed methods in the BLL that called down into these data modification DAL methods. In addition to its Select() method, the ObjectDataSource also has Insert(), Update(), and Delete() methods. Like the Select() method, these three methods can be mapped to methods in an underlying object. When configured to insert, update, or delete data, the GridView, DetailsView, and FormView controls provide a user interface for modifying the underlying data. This user interface calls the Insert(), Update(), and Delete()methods of the ObjectDataSource, which then invoke the underlying object's associated methods (see Figure 1).

Figure 1: The ObjectDataSource's Insert(), Update(), and Delete() Methods Serve as a Proxy into the BLL (Click to view full-size image)

In this tutorial we'll see how to map the ObjectDataSource's Insert(), Update(), and Delete() methods to methods of classes in the BLL, as well as how to configure the GridView, DetailsView, and FormView controls to provide data modification capabilities.

Step 1: Creating the Insert, Update, and Delete Tutorials Web Pages
Before we start exploring how to insert, update, and delete data, let's first take a moment to create the ASP.NET pages in our website project that we'll need for this tutorial and the next several ones. Start by adding a new folder named EditInsertDelete. Next, add the following ASP.NET pages to that folder, making sure to associate each page with the Site.master master page: Default.aspx Basics.aspx DataModificationEvents.aspx ErrorHandling.aspx UIValidation.aspx CustomizedUI.aspx OptimisticConcurrency.aspx ConfirmationOnDelete.aspx UserLevelAccess.aspx

Figure 2: Add the ASP.NET Pages for the Data Modification-Related Tutorials Like in the other folders, Default.aspx in the EditInsertDelete folder will list the tutorials in its section. Recall that the SectionLevelTutorialListing.ascx User Control provides this functionality. Therefore, add this User Control to Default.aspx by dragging it from the Solution Explorer onto the page's Design view.

Figure 3: Add the SectionLevelTutorialListing.ascx User Control to Default.aspx (Click to view full-size image) Lastly, add the pages as entries to the Web.sitemap file. Specifically, add the following markup after the Customized Formatting <siteMapNode>: <siteMapNode title="Editing, Inserting, and Deleting" url="~/EditInsertDelete/Default.aspx" description="Samples of Reports that Provide Editing, Inserting, and Deleting Capabilities"> <siteMapNode url="~/EditInsertDelete/Basics.aspx" title="Basics" description="Examines the basics of data modification with the GridView, DetailsView, and FormView controls." /> <siteMapNode url="~/EditInsertDelete/DataModificationEvents.aspx" title="Data Modification Events" description="Explores the events raised by the ObjectDataSource pertinent to data modification." /> <siteMapNode url="~/EditInsertDelete/ErrorHandling.aspx" title="Error Handling" description="Learn how to gracefully handle exceptions raised during the data modification workflow." /> <siteMapNode url="~/EditInsertDelete/UIValidation.aspx" title="Adding Data Entry Validation" description="Help prevent data entry errors by providing validation." /> <siteMapNode url="~/EditInsertDelete/CustomizedUI.aspx" title="Customize the User Interface" description="Customize the editing and inserting user interfaces." /> <siteMapNode url="~/EditInsertDelete/OptimisticConcurrency.aspx" title="Optimistic Concurrency" description="Learn how to help prevent simultaneous users from overwritting one another s changes." /> <siteMapNode url="~/EditInsertDelete/ConfirmationOnDelete.aspx" title="Confirm On Delete" description="Prompt a user for confirmation when deleting a record." /> <siteMapNode url="~/EditInsertDelete/UserLevelAccess.aspx" title="Limit Capabilities Based on User" description="Learn how to limit the data modification functionality based on the user role or permissions." /> </siteMapNode> After updating Web.sitemap, take a moment to view the tutorials website through a browser. The menu on the left now includes items for the editing, inserting, and deleting tutorials.

Figure 4: The Site Map Now Includes Entries for the Editing, Inserting, and Deleting Tutorials

Step 2: Adding and Configuring the ObjectDataSource Control


Since the GridView, DetailsView, and FormView each differ in their data modification capabilities and layout, let's examine each one individually. Rather than have each control using its own ObjectDataSource, however, let's just create a single ObjectDataSource that all three control examples can share. Open the Basics.aspx page, drag an ObjectDataSource from the Toolbox onto the Designer, and click the Configure Data Source link from its smart tag. Since the ProductsBLL is the only BLL class that provides editing, inserting, and deleting methods, configure the ObjectDataSource to use this class.

Figure 5: Configure the ObjectDataSource to Use the ProductsBLL Class (Click to view full-size image) In the next screen we can specify what methods of the ProductsBLL class are mapped to the ObjectDataSource'sSelect(), Insert(), Update(), and Delete() by selecting the appropriate tab and choosing the method from the drop-down list. Figure 6, which should look familiar by now, maps the ObjectDataSource's Select() method to theProductsBLL class's GetProducts() method. The Insert(), Update(), and Delete() methods can be configured by selecting the appropriate tab from the list along the top.

Figure 6: Have the ObjectDataSource Return All of the Products (Click to view full-size image)

Figures 7, 8, and 9 show the ObjectDataSource's UPDATE, INSERT, and DELETE tabs. Configure these tabs so that the Insert(), Update(), and Delete() methods invoke the ProductsBLL class's UpdateProduct, AddProduct, andDeleteProduct methods, respectively.

Figure 7: Map the ObjectDataSource's Update() Method to the ProductBLL Class's UpdateProduct Method (Click to view full-size image)

Figure 8: Map the ObjectDataSource's Insert() Method to the ProductBLL Class's AddProduct Method (Click to view full-size image)

Figure 9: Map the ObjectDataSource's Delete() Method to the ProductBLL Class's DeleteProduct Method (Click to view full-size image) You may have noticed that the drop-down lists in the UPDATE, INSERT, and DELETE tabs already had these methods selected. This is thanks to our use of the DataObjectMethodAttribute that decorates the methods ofProducstBLL. For example, the DeleteProduct method has the following signature: <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Delete, True)> _ Public Function DeleteProduct(ByVal productID As Integer) As Boolean End Function The DataObjectMethodAttribute attribute indicates the purpose of each method whether it is for selecting, inserting, updating, or deleting and whether or not it's the default value. If you omitted these attributes when creating your BLL classes, you'll need to manually select the methods from the UPDATE, INSERT, and DELETE tabs. After ensuring that the appropriate ProductsBLL methods are mapped to the ObjectDataSource's Insert(),Update(), and Delete() methods, click Finish to complete the wizard.

Examining the ObjectDataSource's Markup


After configuring the ObjectDataSource through its wizard, go to the Source view to examine the generated declarative markup. The <asp:ObjectDataSource> tag specifies the underlying object and the methods to invoke. Additionally, there are DeleteParameters, UpdateParameters, and InsertParameters that map to the input parameters for the ProductsBLL class's AddProduct, UpdateProduct, and DeleteProduct methods: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="DeleteProduct" InsertMethod="AddProduct" OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct"> <DeleteParameters> <asp:Parameter Name="productID" Type="Int32" /> </DeleteParameters> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" />

<asp:Parameter Name="discontinued" Type="Boolean" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> <InsertParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" /> <asp:Parameter Name="discontinued" Type="Boolean" /> </InsertParameters> </asp:ObjectDataSource> The ObjectDataSource includes a parameter for each of the input parameters for its associated methods, just like a list of SelectParameters is present when the ObjectDataSource is configured to call a select method that expects an input parameter (such as GetProductsByCategoryID(categoryID)). As we'll see shortly, values for theseDeleteParameters, UpdateParameters, and InsertParameters are set automatically by the GridView, DetailsView, and FormView prior to invoking the ObjectDataSource's Insert(), Update(), or Delete() method. These values can also be set programmatically as needed, as we'll discuss in a future tutorial. One side effect of using the wizard to configure to ObjectDataSource is that Visual Studio sets theOldValuesParameterFormatString property to original_{0}. This property value is used to include the original values of the data being edited and is useful in two scenarios: If, when editing a record, users are able to change the primary key value. In this case, both the new primary key value and the original primary key value must be provided so that the record with the original primary key value can be found and have its value updated accordingly. When using optimistic concurrency. Optimistic concurrency is a technique to ensure that two simultaneous users don't overwrite one another's changes, and is the topic for a future tutorial.

The OldValuesParameterFormatString property indicates the name of the input parameters in the underlying object's update and delete methods for the original values. We'll discuss this property and its purpose in greater detail when we explore optimistic concurrency. I bring it up now, however, because our BLL's methods do not expect the original values and therefore it's important that we remove this property. Leaving theOldValuesParameterFormatString property set to anything other than the default ({0}) will cause an error when a data Web control attempts to invoke the ObjectDataSource's Update() or Delete() methods because the ObjectDataSource will attempt to pass in both the UpdateParameters or DeleteParameters specified as well as original value parameters. If this isn't terribly clear at this juncture, don't worry, we'll examine this property and its utility in a future tutorial. For now, just be certain to either remove this property declaration entirely from the declarative syntax or set the value to the default value ({0}). Note: If you simply clear out the OldValuesParameterFormatString property value from the Properties window in the Design view, the property will still exist in the declarative syntax, but be set to an empty string. This, unfortunately, will still result in the same problem discussed above. Therefore, either remove the property altogether from the declarative syntax or, from the Properties window, set the value to the default, {0}.

Step 3: Adding a Data Web Control and Configuring It for Data Modification
Once the ObjectDataSource has been added to the page and configured, we're ready to add data Web controls to the page to both display the data and provide a means for the end user to modify it. We'll look at the GridView, DetailsView, and FormView separately, as these data Web controls differ in their data modification capabilities and configuration. As we'll see in the remainder of this article, adding very basic editing, inserting, and deleting support through the GridView, DetailsView, and FormView controls is really as simple as checking a couple of checkboxes. There are many subtleties and edge cases in the real-world that make providing such functionality more involved than just point and

click. This tutorial, however, focuses solely on proving simplistic data modification capabilities. Future tutorials will examine concerns that will undoubtedly arise in a real-world setting.

Deleting Data from the GridView


Start by dragging a GridView from the Toolbox onto the Designer. Next, bind the ObjectDataSource to the GridView by selecting it from the drop-down list in the GridView's smart tag. At this point the GridView's declarative markup will be: <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:BoundField DataField="ProductID" HeaderText="ProductID" InsertVisible="False" ReadOnly="True" SortExpression="ProductID" /> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="SupplierID" HeaderText="SupplierID" SortExpression="SupplierID" /> <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" SortExpression="CategoryID" /> <asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" SortExpression="QuantityPerUnit" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" SortExpression="UnitsInStock" /> <asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" /> <asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" SortExpression="ReorderLevel" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> <asp:BoundField DataField="CategoryName" HeaderText="CategoryName" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="SupplierName" ReadOnly="True" SortExpression="SupplierName" /> </Columns> </asp:GridView> Binding the GridView to the ObjectDataSource through its smart tag has two benefits: BoundFields and CheckBoxFields are automatically created for each of the fields returned by the ObjectDataSource. Moreover, the BoundField and CheckBoxField's properties are set based on the underlying field's metadata. For example, the ProductID, CategoryName, and SupplierName fields are marked as read-only in the ProductsDataTable and therefore shouldn't be updatable when editing. To accommodate this, these BoundFields' ReadOnly properties are set to True. The DataKeyNames property is assigned to the primary key field(s) of the underlying object. This is essential when using the GridView for editing or deleting data, as this property indicates the field (or set of fields) that unique identifies each record. For more information on the DataKeyNames property, refer back to theMaster/Detail Using a Selectable Master GridView with a Details DetailView tutorial. While the GridView can be bound to the ObjectDataSource through the Properties window or declarative syntax, doing so requires you to manually add the appropriate BoundField and DataKeyNames markup. The GridView control provides built-in support for row-level editing and deleting. Configuring a GridView to support deleting adds a column of Delete buttons. When the end user clicks the Delete button for a particular row, a postback ensues and the GridView performs the following steps: 1. The ObjectDataSource's DeleteParameters value(s) are assigned 2. The ObjectDataSource's Delete() method is invoked, deleting the specified record 3. The GridView rebinds itself to the ObjectDataSource by invoking its Select() method The values assigned to the DeleteParameters are the values of the DataKeyNames field(s) for the row whose Delete button was clicked. Therefore it's vital that a GridView's DataKeyNames property be correctly set. If it's missing,

the DeleteParameters will be assigned a value of Nothing in Step 1, which in turn will not result in any deleted records in Step 2. Note: The DataKeys collection is stored in the GridView s control state, meaning that the DataKeys values will be remembered across postback even if the GridView s view state has been disabled. However, it is very important that view state remains enabled for GridViews that support editing or deleting (the default behavior). If you set the GridView s EnableViewState property to false, the editing and deleting behavior will work fine for a single user, but if there are concurrent users deleting data, there exists the possibility that these concurrent users may accidentally delete or edit records that they didn t intend. See my blog entry, WARNING: Concurrency Issue with ASP.NET 2.0 GridViews/DetailsView/FormViews that Support Editing and/or Deleting and Whose View State is Disabled, for more information. This same warning also applies to DetailsViews and FormViews. To add deleting capabilities to a GridView, simply go to its smart tag and check the Enable Deleting checkbox.

Figure 10: Check the Enable Deleting Checkbox Checking the Enable Deleting checkbox from the smart tag adds a CommandField to the GridView. The CommandField renders a column in the GridView with buttons for performing one or more of the following tasks: selecting a record, editing a record, and deleting a record. We previously saw the CommandField in action with selecting records in the Master/Detail Using a Selectable Master GridView with a Details DetailView tutorial. The CommandField contains a number of ShowXButton properties that indicate what series of buttons are displayed in the CommandField. By checking the Enable Deleting checkbox a CommandField whoseShowDeleteButton property is True has been added to the GridView's Columns collection. <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:CommandField ShowDeleteButton="True" /> ... BoundFields removed for brevity ... </Columns> </asp:GridView> At this point, believe it or not, we're done with adding deleting support to the GridView! As Figure 11 shows, when visiting this page through a browser a column of Delete buttons is present.

Figure 11: The CommandField Adds a Column of Delete Buttons (Click to view full-size image) If you've been building this tutorial from the ground up on your own, when testing this page clicking the Delete button will raise an exception. Continue reading to learn as to why these exceptions were raised and how to fix them. Note: If you're following along using the download accompanying this tutorial, these problems have already been accounted for. However, I encourage you to read through the details listed below to help identify problems that may arise and suitable workarounds. If, when attempting to delete a product, you get an exception whose message is similar to "ObjectDataSource 'ObjectDataSource1' could not find a non-generic method 'DeleteProduct' that has parameters: productID, original_ProductID," you likely forgot to remove the OldValuesParameterFormatString property from the ObjectDataSource. With the OldValuesParameterFormatString property specified, the ObjectDataSource attempts to pass in both productID and original_ProductID input parameters to the DeleteProduct method.DeleteProduct, however, only accepts a single input parameter, hence the exception. Removing theOldValuesParameterFormatString property (or setting it to {0}) instructs the ObjectDataSource to not attempt to pass in the original input parameter.

Figure 12: Ensure That the OldValuesParameterFormatString Property Has Been Cleared Out (Click to view full-size image) Even if you had removed the OldValuesParameterFormatString property, you still will get an exception when trying to delete a product with the message: "The DELETE statement conflicted with the REFERENCE constraint 'FK_Order_Details_Products'." The Northwind database contains a foreign key constraint between the Order Detailsand Products table, meaning that a product cannot be deleted from the system if there are one or more records for it in the Order Details table. Since every product in the Northwind database has at least one record in Order Details, we cannot delete any products until we first delete the product's associated order details records.

Figure 13: A Foreign Key Constraint Prohibits the Deletion of Products (Click to view full-size image) For our tutorial, let's just delete all of the records from the Order Details table. In a real-world application we'd need to either: Have another screen to manage order details information Augment the DeleteProduct method to include logic to delete the specified product's order details Modify the SQL query used by the TableAdapter to include deletion of the specified product's order details

Let's just delete all of the records from the Order Details table to circumvent the foreign key constraint. Go to the Server Explorer in Visual Studio, right-click on the NORTHWND.MDF node, and choose New Query. Then, in the query window, run the following SQL statement: DELETE FROM [Order Details]

Figure 14: Delete All Records from the Order Details Table (Click to view full-size image) After clearing out the Order Details table clicking on the Delete button will delete the product without error. If clicking on the Delete button does not delete the product, check to ensure that the GridView's DataKeyNamesproperty is set to the primary key field (ProductID). Note: When clicking on the Delete button a postback ensues and the record is deleted. This can be dangerous since it is easy to accidentally click on the wrong row's Delete button. In a future tutorial we'll see how to add a client-side confirmation when deleting a record.

Editing Data with the GridView


Along with deleting, the GridView control also provides built-in row-level editing support. Configuring a GridView to support editing adds a column of Edit buttons. From the end user's perspective, clicking a row's Edit button causes that row to become editable, turning the cells into textboxes containing the existing values and replacing the Edit button with Update and Cancel buttons. After making their desired changes, the end user can click the Update button to commit the changes or the Cancel button to discard them. In either case, after clicking Update or Cancel the GridView returns to its pre-editing state. From our perspective as the page developer, when the end user clicks the Edit button for a particular row, a postback ensues and the GridView performs the following steps: 1. 2. The GridView's EditItemIndex property is assigned to the index of the row whose Edit button was clicked The GridView rebinds itself to the ObjectDataSource by invoking its Select() method

The row index that matches the EditItemIndex is rendered in "edit mode." In this mode, the Edit button is replaced by Update and Cancel buttons and BoundFields whose ReadOnly properties are False (the default) are rendered as TextBox Web controls whose Text properties are assigned to the data fields' values. At this point the markup is returned to the browser, allowing the end user to make any changes to the row's data. When the user clicks the Update button, a postback occurs and the GridView performs the following steps: The ObjectDataSource's UpdateParameters value(s) are assigned the values entered by the end user into the GridView's editing interface 2. The ObjectDataSource's Update() method is invoked, updating the specified record 3. The GridView rebinds itself to the ObjectDataSource by invoking its Select() method The primary key values assigned to the UpdateParameters in Step 1 come from the values specified in theDataKeyNames property, whereas the non-primary key values come from the text in the TextBox Web controls for the edited row. As with deleting, it is vital that a GridView's DataKeyNames property be correctly set. If it's missing, the UpdateParameters primary key value will be assigned a value of Nothing in Step 1, which in turn will not result in any updated records in Step 2. Editing functionality can be activated by simply checking the Enable Editing checkbox in the GridView's smart tag. 1.

3.

Figure 15: Check the Enable Editing Checkbox Checking the Enable Editing checkbox will add a CommandField (if needed) and set its ShowEditButton property toTrue. As we saw earlier, the CommandField contains a number of ShowXButton properties that indicate what series of buttons are displayed in the CommandField. Checking the Enable Editing checkbox adds the ShowEditButtonproperty to the existing CommandField: <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" /> ... BoundFields removed for brevity ... </Columns> </asp:GridView> That's all there is to adding rudimentary editing support. As Figure16 shows, the editing interface is rather crude each BoundField whose ReadOnly property is set to False (the default) is rendered as a TextBox. This includes fields like CategoryID and SupplierID, which are keys to other tables.

Figure 16: Clicking Chai s Edit Button Displays the Row in Edit Mode (Click to view full-size image) In addition to asking users to edit foreign key values directly, the editing interface's interface is lacking in the following ways: If the user enters a CategoryID or SupplierID that does not exist in the database, the UPDATE will violate a foreign key constraint, causing an exception to be raised. The editing interface doesn't include any validation. If you don't provide a required value (such asProductName), or enter a string value where a numeric value is expected (such as entering "Too much!" into the UnitPrice textbox), an exception will be thrown. A future tutorial will examine how to add validation controls to the editing user interface. Currently, all product fields that are not read-only must be included in the GridView. If we were to remove a field from the GridView, say UnitPrice, when updating the data the GridView would not set the UnitPriceUpdateParameters value, which would change the database record's UnitPrice to a NULL value. Similarly, if a required field, such as ProductName, is removed from the GridView, the update will fail with the same "Column 'ProductName' does not allow nulls" exception mentioned above. The editing interface formatting leaves a lot to be desired. The UnitPrice is shown with four decimal points. Ideally the CategoryID and SupplierID values would contain DropDownLists that list the categories and suppliers in the system. These are all shortcomings that we'll have to live with for now, but will be addressed in future tutorials.

Inserting, Editing, and Deleting Data with the DetailsView


As we've seen in earlier tutorials, the DetailsView control displays one record at a time and, like the GridView, allows for editing and deleting of the currently displayed record. Both the end user's experience with editing and deleting items from a DetailsView and the workflow from the ASP.NET side is identical to that of the GridView. Where the DetailsView differs from the GridView is that it also provides built-in inserting support. To demonstrate the data modification capabilities of the GridView, start by adding a DetailsView to theBasics.aspx page above the existing GridView and bind it to the existing ObjectDataSource through the DetailsView's smart tag. Next, clear out the DetailsView's Height and Width properties, and check the Enable Paging option from the smart tag. To enable editing, inserting, and deleting support, simply check the Enable Editing, Enable Inserting, and Enable Deleting checkboxes in the smart tag.

Figure 17: Configure the DetailsView to Support Editing, Inserting, and Deleting As with the GridView, adding editing, inserting, or deleting support adds a CommandField to the DetailsView, as the following declarative syntax shows: <asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True"> <Fields> <asp:BoundField DataField="ProductID" HeaderText="ProductID" InsertVisible="False" ReadOnly="True" SortExpression="ProductID" /> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="SupplierID" HeaderText="SupplierID" SortExpression="SupplierID" /> <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" SortExpression="CategoryID" /> <asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" SortExpression="QuantityPerUnit" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" SortExpression="UnitsInStock" /> <asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" /> <asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" SortExpression="ReorderLevel" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> <asp:BoundField DataField="CategoryName" HeaderText="CategoryName" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="SupplierName" ReadOnly="True" SortExpression="SupplierName" /> <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" ShowInsertButton="True" /> </Fields> </asp:DetailsView> Note that for the DetailsView the CommandField appears at the end of the Columns collection by default. Since the DetailsView's fields are rendered as rows, the CommandField appears as a row with Insert, Edit, and Delete buttons at the bottom of the DetailsView.

Figure 18: Configure the DetailsView to Support Editing, Inserting, and Deleting (Click to view full-size image) Clicking on the Delete button starts the same sequence of events as with the GridView: a postback; followed by the DetailsView populating its ObjectDataSource's DeleteParameters based on the DataKeyNames values; and completed with a call its ObjectDataSource's Delete() method, which actually removes the product from the database. Editing in the DetailsView also works in a fashion identical to that of the GridView. For inserting, the end user is presented with a New button that, when clicked, renders the DetailsView in "insert mode." With "insert mode" the New button is replaced by Insert and Cancel buttons and only those BoundFields whose InsertVisible property is set to True (the default) are displayed. Those data fields identified as auto-increment fields, such as ProductID, have their InsertVisible property set to False when binding the DetailsView to the data source through the smart tag. When binding a data source to a DetailsView through the smart tag, Visual Studio sets the InsertVisible property to False only for auto-increment fields. Read-only fields, like CategoryName and SupplierName, will be displayed in the "insert mode" user interface unless their InsertVisible property is explicitly set to False. Take a moment to set these two fields' InsertVisible properties to False, either through the DetailsView's declarative syntax or through the Edit Fields link in the smart tag. Figure 19 shows setting the InsertVisible properties to False by clicking on the Edit Fields link.

Figure 19: Northwind Traders Now Offers Acme Tea (Click to view full-size image) After setting the InsertVisible properties, view the Basics.aspx page in a browser and click the New button. Figure 20 shows the DetailsView when adding a new beverage, Acme Tea, to our product line.

Figure 20: Northwind Traders Now Offers Acme Tea (Click to view full-size image) After entering the details for Acme Tea and clicking the Insert button, a postback ensues and the new record is added to the Products database table. Since this DetailsView lists the products in order with which they exist in the database table, we must page to the last product in order to see the new product.

Figure 21: Details for Acme Tea (Click to view full-size image) Note: The DetailsView's CurrentMode property indicates the interface being displayed and can be one of the following values: Edit, Insert, or ReadOnly. The DefaultMode property indicates the mode the DetailsView returns to after an edit or insert has been completed and is useful for displaying a DetailsView that is permanently in edit or insert mode. The point and click inserting and editing capabilities of the DetailsView suffer from the same limitations as the GridView: the user must enter existing CategoryID and SupplierID values through a textbox; the interface lacks any validation logic; all product fields that do not allow NULL values or don't have a default value specified at the database level must be included in the inserting interface, and so on. The techniques we will examine for extending and enhancing the GridView's editing interface in future articles can be applied to the DetailsView control's editing and inserting interfaces as well.

Using the FormView for a More Flexible Data Modification User Interface
The FormView offers built-in support for inserting, editing, and deleting data, but because it uses templates instead of fields there's no place to add the BoundFields or the CommandField used by the GridView and DetailsView controls to provide the data modification interface. Instead, this interface the Web controls for collecting user input when adding a new item or editing an existing one along with the New, Edit, Delete, Insert, Update, and Cancel buttons must be added manually to the appropriate templates. Fortunately, Visual Studio will automatically create the needed interface when binding the FormView to a data source through the drop-down list in its smart tag. To illustrate these techniques, start by adding a FormView to the Basics.aspx page and, from the FormView's smart tag, bind it to the ObjectDataSource already created. This will generate an EditItemTemplate,InsertItemTemplate, and ItemTemplate for the FormView with TextBox Web controls for collecting the user's input and Button Web controls for the New, Edit, Delete, Insert, Update, and Cancel buttons. Additionally, the FormView's DataKeyNames property is set to the primary key field (ProductID) of the object returned by the ObjectDataSource. Lastly, check the Enable Paging option in the FormView's smart tag.

The following shows the declarative markup for the FormView's ItemTemplate after the FormView has been bound to the ObjectDataSource. By default, each non-Boolean value product field is bound to the Text property of a Label Web control while each Boolean value field (Discontinued) is bound to the Checked property of a disabled CheckBox Web control. In order for the New, Edit, and Delete buttons to trigger certain FormView behavior when clicked, it is imperative that their CommandName values be set to New, Edit, and Delete, respectively. <asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True"> <EditItemTemplate> ... </EditItemTemplate> <InsertItemTemplate> ... </InsertItemTemplate> <ItemTemplate> ProductID: <asp:Label ID="ProductIDLabel" runat="server" Text='<%# Eval("ProductID") %>'></asp:Label><br /> ProductName: <asp:Label ID="ProductNameLabel" runat="server" Text='<%# Bind("ProductName") %>'> </asp:Label><br /> SupplierID: <asp:Label ID="SupplierIDLabel" runat="server" Text='<%# Bind("SupplierID") %>'> </asp:Label><br /> CategoryID: <asp:Label ID="CategoryIDLabel" runat="server" Text='<%# Bind("CategoryID") %>'> </asp:Label><br /> QuantityPerUnit: <asp:Label ID="QuantityPerUnitLabel" runat="server" Text='<%# Bind("QuantityPerUnit") %>'> </asp:Label><br /> UnitPrice: <asp:Label ID="UnitPriceLabel" runat="server" Text='<%# Bind("UnitPrice") %>'></asp:Label><br /> UnitsInStock: <asp:Label ID="UnitsInStockLabel" runat="server" Text='<%# Bind("UnitsInStock") %>'> </asp:Label><br /> UnitsOnOrder: <asp:Label ID="UnitsOnOrderLabel" runat="server" Text='<%# Bind("UnitsOnOrder") %>'> </asp:Label><br /> ReorderLevel: <asp:Label ID="ReorderLevelLabel" runat="server" Text='<%# Bind("ReorderLevel") %>'> </asp:Label><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked='<%# Bind("Discontinued") %>' Enabled="false" /><br /> CategoryName: <asp:Label ID="CategoryNameLabel" runat="server" Text='<%# Bind("CategoryName") %>'> </asp:Label><br /> SupplierName: <asp:Label ID="SupplierNameLabel" runat="server" Text='<%# Bind("SupplierName") %>'> </asp:Label><br /> <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False" CommandName="Edit" Text="Edit"> </asp:LinkButton> <asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete"> </asp:LinkButton> <asp:LinkButton ID="NewButton" runat="server" CausesValidation="False" CommandName="New" Text="New"> </asp:LinkButton> </ItemTemplate> </asp:FormView> Figure 22 shows the FormView's ItemTemplate when viewed through a browser. Each product field is listed with the New, Edit, and Delete buttons at the bottom.

Figure 22: The Defaut FormView ItemTemplate Lists Each Product Field Along with New, Edit, and Delete Buttons(Click to view full-size image) Like with the GridView and DetailsView, clicking the Delete button or any Button, LinkButton, or ImageButton whoseCommandName property is set to Delete causes a postback, populates the ObjectDataSource's DeleteParametersbased on the FormView's DataKeyNames value, and invokes the ObjectDataSource's Delete() method. When the Edit button is clicked a postback ensues and the data is rebound to the EditItemTemplate, which is responsible for rendering the editing interface. This interface includes the Web controls for editing data along with the Update and Cancel buttons. The default EditItemTemplate generated by Visual Studio contains a Label for any autoincrement fields (ProductID), a TextBox for each non-Boolean value field, and a CheckBox for each Boolean value field. This behavior is very similar to the auto-generated BoundFields in the GridView and DetailsView controls. Note: One small issue with the FormView's auto-generation of the EditItemTemplate is that it renders TextBox Web controls for those fields that are read-only, such as CategoryName and SupplierName. We'll see how to account for this shortly. The TextBox controls in the EditItemTemplate have their Text property bound to the value of their corresponding data field using two-way databinding. Two-way databinding, denoted by <%# Bind("dataField") %>, performs databinding both when binding data to the template and when populating the ObjectDataSource's parameters for inserting or editing records. That is, when the user clicks the Edit button from the ItemTemplate, the Bind()method returns the specified data field value. After the user makes their changes and clicks Update, the values posted back that correspond to the data fields specified using Bind() are applied to the ObjectDataSource'sUpdateParameters. Alternatively, one-way databinding, denoted by <%# Eval("dataField") %>, only retrieves the data field values when binding data to the template and does not return the user-entered values to the data source's parameters on postback. The following declarative markup shows the FormView's EditItemTemplate. Note that the Bind() method is used in the databinding syntax here and that the Update and Cancel Button Web controls have their CommandNameproperties set accordingly.

<asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True"> <EditItemTemplate> ProductID: <asp:Label ID="ProductIDLabel1" runat="server" Text="<%# Eval("ProductID") %>"></asp:Label><br /> ProductName: <asp:TextBox ID="ProductNameTextBox" runat="server" Text="<%# Bind("ProductName") %>"> </asp:TextBox><br /> SupplierID: <asp:TextBox ID="SupplierIDTextBox" runat="server" Text="<%# Bind("SupplierID") %>"> </asp:TextBox><br /> CategoryID: <asp:TextBox ID="CategoryIDTextBox" runat="server" Text="<%# Bind("CategoryID") %>"> </asp:TextBox><br /> QuantityPerUnit: <asp:TextBox ID="QuantityPerUnitTextBox" runat="server" Text="<%# Bind("QuantityPerUnit") %>"> </asp:TextBox><br /> UnitPrice: <asp:TextBox ID="UnitPriceTextBox" runat="server" Text="<%# Bind("UnitPrice") %>"> </asp:TextBox><br /> UnitsInStock: <asp:TextBox ID="UnitsInStockTextBox" runat="server" Text="<%# Bind("UnitsInStock") %>"> </asp:TextBox><br /> UnitsOnOrder: <asp:TextBox ID="UnitsOnOrderTextBox" runat="server" Text="<%# Bind("UnitsOnOrder") %>"> </asp:TextBox><br /> ReorderLevel: <asp:TextBox ID="ReorderLevelTextBox" runat="server" Text="<%# Bind("ReorderLevel") %>"> </asp:TextBox><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked="<%# Bind("Discontinued") %>" /><br /> CategoryName: <asp:TextBox ID="CategoryNameTextBox" runat="server" Text="<%# Bind("CategoryName") %>"> </asp:TextBox><br /> SupplierName: <asp:TextBox ID="SupplierNameTextBox" runat="server" Text="<%# Bind("SupplierName") %>"> </asp:TextBox><br /> <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update" Text="Update"> </asp:LinkButton> <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel"> </asp:LinkButton> </EditItemTemplate> <InsertItemTemplate> ... </InsertItemTemplate> <ItemTemplate> ... </ItemTemplate> </asp:FormView> Our EditItemTemplate, at this point, will cause an exception to be thrown if we attempt to use it. The problem is that the CategoryName and SupplierName fields are rendered as TextBox Web controls in the EditItemTemplate. We either need to change these TextBoxes to Labels or remove them altogether. Let's simply delete them entirely from the EditItemTemplate. Figure 23 shows the FormView in a browser after the Edit button has been clicked for Chai. Note that theSupplierName and CategoryName fields shown in the ItemTemplate are no longer present, as we just removed them from the EditItemTemplate. When the Update button is clicked the FormView proceeds through the same sequence of steps as the GridView and DetailsView controls.

Figure 23: By Default the EditItemTemplate Shows Each Editable Product Field as a TextBox or CheckBox (Click to view full-size image) When the Insert button is clicked the FormView's ItemTemplate a postback ensues. However, no data is bound to the FormView because a new record is being added. The InsertItemTemplate interface includes the Web controls for adding a new record along with the Insert and Cancel buttons. The default InsertItemTemplate generated by Visual Studio contains a TextBox for each non-Boolean value field and a CheckBox for each Boolean value field, similar to the auto-generated EditItemTemplate's interface. The TextBox controls have their Text property bound to the value of their corresponding data field using two-way databinding. The following declarative markup shows the FormView's InsertItemTemplate. Note that the Bind() method is used in the databinding syntax here and that the Insert and Cancel Button Web controls have their CommandNameproperties set accordingly. <asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True"> <EditItemTemplate> ... </EditItemTemplate> <InsertItemTemplate> ProductName: <asp:TextBox ID="ProductNameTextBox" runat="server" Text="<%# Bind("ProductName") %>"> </asp:TextBox><br /> SupplierID: <asp:TextBox ID="SupplierIDTextBox" runat="server" Text="<%# Bind("SupplierID") %>"> </asp:TextBox><br /> CategoryID: <asp:TextBox ID="CategoryIDTextBox" runat="server" Text="<%# Bind("CategoryID") %>"> </asp:TextBox><br /> QuantityPerUnit: <asp:TextBox ID="QuantityPerUnitTextBox" runat="server" Text="<%# Bind("QuantityPerUnit") %>"> </asp:TextBox><br /> UnitPrice: <asp:TextBox ID="UnitPriceTextBox" runat="server" Text="<%# Bind("UnitPrice") %>"> </asp:TextBox><br /> UnitsInStock: <asp:TextBox ID="UnitsInStockTextBox" runat="server" Text="<%# Bind("UnitsInStock") %>"> </asp:TextBox><br /> UnitsOnOrder: <asp:TextBox ID="UnitsOnOrderTextBox" runat="server" Text="<%# Bind("UnitsOnOrder") %>"> </asp:TextBox><br /> ReorderLevel: <asp:TextBox ID="ReorderLevelTextBox" runat="server" Text="<%# Bind("ReorderLevel") %>"> </asp:TextBox><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked="<%# Bind("Discontinued") %>" /><br /> CategoryName: <asp:TextBox ID="CategoryNameTextBox" runat="server" Text="<%# Bind("CategoryName") %>"> </asp:TextBox><br /> SupplierName: <asp:TextBox ID="SupplierNameTextBox" runat="server" Text="<%# Bind("SupplierName") %>"> </asp:TextBox><br /> <asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True"

CommandName="Insert" Text="Insert"> </asp:LinkButton> <asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel"> </asp:LinkButton> </InsertItemTemplate> <ItemTemplate> ... </ItemTemplate> </asp:FormView> There's a subtlety with the FormView's auto-generation of the InsertItemTemplate. Specifically, the TextBox Web controls are created even for those fields that are read-only, such as CategoryName and SupplierName. Like with the EditItemTemplate, we need to remove these TextBoxes from the InsertItemTemplate. Figure 24 shows the FormView in a browser when adding a new product, Acme Coffee. Note that the SupplierNameand CategoryName fields shown in the ItemTemplate are no longer present, as we just removed them. When the Insert button is clicked the FormView proceeds through the same sequence of steps as the DetailsView control, adding a new record to the Products table. Figure 25 shows Acme Coffee product's details in the FormView after it has been inserted.

Figure 24: The InsertItemTemplate Dictates the FormView's Inserting Interface (Click to view full-size image)

Figure 25: The Details for New Product, Acme Coffee, are Displayed in the FormView (Click to view full-size image) By separating out the read-only, editing, and inserting interfaces into three separate templates, the FormView allows for a finer degree of control over these interfaces than the DetailsView and GridView. Note: Like the DetailsView, the FormView's CurrentMode property indicates the interface being displayed and itsDefaultMode property indicates the mode the FormView returns to after an edit or insert has been completed.

Summary
In this tutorial we examined the basics of inserting, editing, and deleting data using the GridView, DetailsView, and FormView. All three of these controls provide some level of built-in data modification capabilities that can be utilized without writing a single line of code in the ASP.NET page thanks to the data Web controls and the ObjectDataSource. However, the simple point and click techniques render a fairly frail and nave data modification user interface. To provide validation, inject programmatic values, gracefully handle exceptions, customize the user interface, and so on, we'll need to rely on a bevy of techniques that will be discussed over the next several tutorials. Happy Programming!

Examining the Events Associated with Inserting, Updating, and Deleting (VB)
Tutorials

By Scott Mitchell|July 17, 2006 DOWNLOAD ASSETS: Sample App or PDF In this tutorial we'll examine using the events that occur before, during, and after an insert, update, or delete operation of an ASP.NET data Web control. We'll also see how to customize the editing interface to only update a subset of the product fields.

Introduction
When using the built-in inserting, editing, or deleting features of the GridView, DetailsView, or FormView controls, a variety of steps transpire when the end user completes the process of adding a new record or updating or deleting an existing record. As we discussed in the previous tutorial, when a row is edited in the GridView the Edit button is replaced by Update and Cancel buttons and the BoundFields turn into TextBoxes. After the end user updates the data and clicks Update, the following steps are performed on postback: 1. The GridView populates its ObjectDataSource's UpdateParameters with the edited record's unique identifying field(s) (via the DataKeyNames property) along with the values entered by the user 2. The GridView invokes its ObjectDataSource's Update() method, which in turn invokes the appropriate method in the underlying object (ProductsDAL.UpdateProduct, in our previous tutorial) 3. The underlying data, which now includes the updated changes, is rebound to the GridView During this sequence of steps, a number of events fire, enabling us to create event handlers to add custom logic where needed. For example, prior to Step 1, the GridView's RowUpdating event fires. We can, at this point, cancel the update request if there is some validation error. When the Update() method is invoked, the ObjectDataSource'sUpdating event fires, providing an opportunity to add or customize the values of any of the UpdateParameters. After the ObjectDataSource's underlying object's method has completed executing, the ObjectDataSource'sUpdated event is raised. An event handler for the Updated event can inspect the details about the update operation, such as how many rows were affected and whether or not an exception occurred. Finally, after Step 2, the GridView's RowUpdated event fires; an event handler for this event can examine additional information about the update operation just performed. Figure 1 depicts this series of events and steps when updating a GridView. The event pattern in Figure 1 is not unique to updating with a GridView. Inserting, updating, or deleting data from the GridView, DetailsView, or FormView precipitates the same sequence of pre- and post-level events for both the data Web control and the ObjectDataSource.

Figure 1: A Series of Pre- and Post-Events Fire When Updating Data in a GridView (Click to view full-size image) In this tutorial we'll examine using these events to extend the built-in inserting, updating, and deleting capabilities of the ASP.NET data Web controls. We'll also see how to customize the editing interface to only update a subset of the product fields.

Step 1: Updating a Product's ProductName and UnitPrice Fields


In the editing interfaces from the previous tutorial all product fields that were not read-only had to be included. If we were to remove a field from the GridView - say QuantityPerUnit - when updating the data the data Web control would not set the ObjectDataSource's QuantityPerUnit UpdateParameters value. The ObjectDataSource would then pass in a value of Nothing into the UpdateProduct Business Logic Layer (BLL) method, which would change the edited database record's QuantityPerUnit column to a NULL value. Similarly, if a required field, such as ProductName, is removed from the editing interface, the update will fail with a "Column 'ProductName' does not allow nulls" exception. The reason for this behavior was because the ObjectDataSource was configured to call theProductsBLL class's UpdateProduct method, which expected an input parameter for each of the product fields. Therefore, the ObjectDataSource's UpdateParameters collection contained a parameter for each of the method's input parameters. If we want to provide a data Web control that allows the end user to only update a subset of fields, then we need to either programmatically set the missing UpdateParameters values in the ObjectDataSource's Updating event handler or create and call a BLL method that expects only a subset of the fields. Let's explore this latter approach. Specifically, let's create a page that displays just the ProductName and UnitPrice fields in an editable GridView. This GridView's editing interface will only allow the user to update the two displayed fields, ProductName andUnitPrice. Since this editing interface only provides a subset of a product's fields, we either need to create an ObjectDataSource that uses the existing BLL's UpdateProduct method and has the missing product field values set programmatically in its Updating event handler, or we need to create a new BLL method that expects only the subset of fields defined in the GridView. For this tutorial, let's use the latter option and create an overload of theUpdateProduct method, one that takes in just three input parameters: productName, unitPrice, and productID:

<System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Update, False)> _ Public Function UpdateProduct(productName As String, _ unitPrice As Nullable(Of Decimal), productID As Integer) _ As Boolean Dim products As Northwind.ProductsDataTable = _ Adapter.GetProductByProductID(productID) If products.Count = 0 Then Return False End If Dim product As Northwind.ProductsRow = products(0) product.ProductName = productName If Not unitPrice.HasValue Then product.SetUnitPriceNull() Else product.UnitPrice = unitPrice.Value End If Dim rowsAffected As Integer = Adapter.Update(product) Return rowsAffected = 1 End Function Like the original UpdateProduct method, this overload starts by checking to see if there is a product in the database with the specified ProductID. If not, it returns False, indicating that the request to update the product information failed. Otherwise it updates the existing product record's ProductName and UnitPrice fields accordingly and commits the update by calling the TableAdpater's Update() method, passing in the ProductsRowinstance. With this addition to our ProductsBLL class, we're ready to create the simplified GridView interface. Open theDataModificationEvents.aspx in the EditInsertDelete folder and add a GridView to the page. Create a new ObjectDataSource and configure it to use the ProductsBLL class with its Select() method mapping toGetProducts and its Update() method mapping to the UpdateProduct overload that takes in only theproductName, unitPrice, and productID input parameters. Figure 2 shows the Create Data Source wizard when mapping the ObjectDataSource's Update() method to the ProductsBLL class's new UpdateProduct method overload.

Figure 2: Map the ObjectDataSource's Update() Method to the New UpdateProduct Overload (Click to view full-size image) Since our example will initially just need the ability to edit data, but not to insert or delete records, take a moment to explicitly indicate that the ObjectDataSource's Insert() and Delete() methods shouldn't be mapped to any of the ProductsBLL class's methods by going to the INSERT and DELETE tabs and choosing (None) from the drop-down list.

Figure 3: Choose (None) From the Drop-Down List for the INSERT and DELETE Tabs (Click to view full-size image) After completing this wizard check the Enable Editing checkbox from the GridView's smart tag.

With the completion of the Create Data Source wizard and binding that to the GridView, Visual Studio has created the declarative syntax for both controls. Go to the Source view to inspect the ObjectDataSource's declarative markup, which is shown below: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource> Since there are no mappings for the ObjectDataSource's Insert() and Delete() methods, there are noInsertParameters or DeleteParameters sections. Furthermore, since the Update() method is mapped to theUpdateProduct method overload that only accepts three input parameters, the UpdateParameters section has just three Parameter instances. Note that the ObjectDataSource's OldValuesParameterFormatString property is set to original_{0}. This property is set automatically by Visual Studio when using the Configure Data Source wizard. However, since our BLL methods don't expect the original ProductID value to be passed in, remove this property assignment altogether from the ObjectDataSource's declarative syntax. Note: If you simply clear out the OldValuesParameterFormatString property value from the Properties window in the Design view, the property will still exist in the declarative syntax, but will be set to an empty string. Either remove the property altogether from the declarative syntax or, from the Properties window, set the value to the default, {0}. While the ObjectDataSource only has UpdateParameters for the product's name, price, and ID, Visual Studio has added a BoundField or CheckBoxField in the GridView for each of the product's fields.

Figure 4: The GridView Contains a BoundField or CheckBoxField for Each of the Product's Fields (Click to view full-size image) When the end user edits a product and clicks its Update button, the GridView enumerates those fields that were not read-only. It then sets the value of the corresponding parameter in the ObjectDataSource's UpdateParameterscollection to the value entered by the user. If there is not a corresponding parameter, the GridView adds one to the collection. Therefore, if our GridView contains BoundFields and CheckBoxFields for all of the product's fields, the ObjectDataSource will end up invoking the UpdateProduct overload that takes in all of these parameters, despite the fact that the ObjectDataSource's declarative markup specifies only three input parameters (see Figure 5). Similarly, if there is some combination of non-read-only product fields in the GridView that doesn't

correspond to the input parameters for a UpdateProduct overload, an exception will be raised when attempting to update.

Figure 5: The GridView Will Add Parameters to the ObjectDataSource's UpdateParameters Collection (Click to view fullsize image) To ensure that the ObjectDataSource invokes the UpdateProduct overload that takes in just the product's name, price, and ID, we need to restrict the GridView to having editable fields for just the ProductName and UnitPrice. This can be accomplished by removing the other BoundFields and CheckBoxFields, by setting those other fields'ReadOnly property to True, or by some combination of the two. For this tutorial let's simply remove all GridView fields except the ProductName and UnitPrice BoundFields, after which the GridView's declarative markup will look like: <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:CommandField ShowEditButton="True" /> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> </Columns> </asp:GridView> Even though the UpdateProduct overload expects three input parameters, we only have two BoundFields in our GridView. This is because the productID input parameter is a primary key value and passed in through the value of the DataKeyNames property for the edited row. Our GridView, along with the UpdateProduct overload, allows a user to edit just the name and price of a product without losing any of the other product fields.

Figure 6: The Interface Allows Editing Just the Product's Name and Price (Click to view full-size image) Note: As discussed in the previous tutorial, it is vitally important that the GridView s view state be enabled (the default behavior). If you set the GridView s EnableViewState property to false, you run the risk of having concurrent users unintentionally deleting or editing records. See WARNING: Concurrency Issue with ASP.NET 2.0 GridViews/DetailsView/FormViews that Support Editing and/or Deleting and Whose View State is Disabled for more information.

Improving the UnitPrice Formatting


While the GridView example shown in Figure 6 works, the UnitPrice field is not formatted at all, resulting in a price display that lacks any currency symbols and has four decimal places. To apply a currency formatting for the non-editable rows, simply set the UnitPrice BoundField's DataFormatString property to {0:c} and its HtmlEncodeproperty to False.

Figure 7: Set the UnitPrice's DataFormatString and HtmlEncode Properties Accordingly (Click to view full-size image) With this change, the non-editable rows format the price as a currency; the edited row, however, still displays the value without the currency symbol and with four decimal places.

Figure 8: The Non-Editable Rows are Now Formatted as Currency Values (Click to view full-size image) The formatting instructions specified in the DataFormatString property can be applied to the editing interface by setting the BoundField's ApplyFormatInEditMode property to True (the default is False). Take a moment to set this property to True.

Figure 9: Set the UnitPrice BoundField's ApplyFormatInEditMode property to True (Click to view full-size image) With this change, the value of the UnitPrice displayed in the edited row is also formatted as a currency.

Figure 10: The Edited Row's UnitPrice Value is Now Formatted as a Currency (Click to view full-size image) However, updating a product with the currency symbol in the textbox such as $19.00 throws a FormatException. When the GridView attempts to assign the user-supplied values to the ObjectDataSource's UpdateParameterscollection it is unable to convert the UnitPrice string "$19.00" into the Decimal required by the parameter (see Figure 11). To remedy this we can create an event handler for the GridView's RowUpdating event and have it parse the usersupplied UnitPrice as a currency-formatted Decimal. The GridView's RowUpdating event accepts as its second parameter an object of type GridViewUpdateEventArgs, which includes a NewValues dictionary as one of its properties that holds the user-supplied values ready to be assigned to the ObjectDataSource's UpdateParameters collection. We can overwrite the existing UnitPrice value in the NewValues collection with a decimal value parsed using the currency format with the following lines of code in the RowUpdating event handler: Protected Sub GridView1_RowUpdating(sender As Object, e As GridViewUpdateEventArgs) _ Handles GridView1.RowUpdating If e.NewValues("UnitPrice") IsNot Nothing Then e.NewValues("UnitPrice") = _ Decimal.Parse(e.NewValues("UnitPrice").ToString(), _ System.Globalization.NumberStyles.Currency) End If End Sub If the user has supplied a UnitPrice value (such as "$19.00"), this value is overwritten with the decimal value computed by Decimal.Parse, parsing the value as a currency. This will correctly parse the decimal in the event of any currency symbols, commas, decimal points, and so on, and uses the NumberStyles enumeration in theSystem.Globalization namespace. Figure 11 shows both the problem caused by currency symbols in the user-supplied UnitPrice, along with how the GridView's RowUpdating event handler can be utilized to correctly parse such input.

Figure 11: The Edited Row's UnitPrice Value is Now Formatted as a Currency (Click to view full-size image)

Step 2: Prohibiting NULL UnitPrices


While the database is configured to allow NULL values in the Products table's UnitPrice column, we may want to prevent users visiting this particular page from specifying a NULL UnitPrice value. That is, if a user fails to enter aUnitPrice value when editing a product row, rather than save the results to the database we want to display a message informing the user that, through this page, any edited products must have a price specified. The GridViewUpdateEventArgs object passed into the GridView's RowUpdating event handler contains a Cancelproperty that, if set to True, terminates the updating process. Let's extend the RowUpdating event handler to sete.Cancel to True and display a message explaining why if the UnitPrice value in the NewValues collection has a value of Nothing. Start by adding a Label Web control to the page named MustProvideUnitPriceMessage. This Label control will be displayed if the user fails to specify a UnitPrice value when updating a product. Set the Label's Text property to "You must provide a price for the product." I've also created a new CSS class in Styles.css named Warning with the following definition: .Warning { color: Red; font-style: italic; font-weight: bold; font-size: x-large; } Finally, set the Label's CssClass property to Warning. At this point the Designer should show the warning message in a red, bold, italic, extra large font size above the GridView, as shown in Figure 12.

Figure 12: A Label Has Been Added Above the GridView (Click to view full-size image) By default, this Label should be hidden, so set its Visible property to False in the Page_Load event handler: Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load MustProvideUnitPriceMessage.Visible = False End Sub If the user attempts to update a product without specifying the UnitPrice, we want to cancel the update and display the warning label. Augment the GridView's RowUpdating event handler as follows: Protected Sub GridView1_RowUpdating(sender As Object, e As GridViewUpdateEventArgs) _ Handles GridView1.RowUpdating If e.NewValues("UnitPrice") IsNot Nothing Then e.NewValues("UnitPrice") = _ Decimal.Parse(e.NewValues("UnitPrice").ToString(), _ System.Globalization.NumberStyles.Currency) Else MustProvideUnitPriceMessage.Visible = True e.Cancel = True End If End Sub If a user attempts to save a product without specifying a price, the update is cancelled and a helpful message is displayed. While the database (and business logic) allows for NULL UnitPrices, this particular ASP.NET page does not.

Figure 13: A User Cannot Leave UnitPrice Blank (Click to view full-size image) So far we have seen how to use the GridView's RowUpdating event to programmatically alter the parameter values assigned to the ObjectDataSource's UpdateParameters collection as well as how to cancel the updating process altogether. These concepts carry over to the DetailsView and FormView controls and also apply to inserting and deleting. These tasks can also be done at the ObjectDataSource level through event handlers for its Inserting, Updating, and Deleting events. These events fire before the associated method of the underlying object is invoked and provide a last-chance opportunity to modify the input parameters collection or cancel the operation outright. The event handlers for these three events are passed an object of type ObjectDataSourceMethodEventArgs that has two properties of interest: Cancel, which, if set to True, cancels the operation being performed InputParameters, which is the collection of InsertParameters, UpdateParameters, or DeleteParameters, depending on whether the event handler is for the Inserting, Updating, or Deleting event To illustrate working with the parameter values at the ObjectDataSource level, let's include a DetailsView in our page that allows the users to add a new product. This DetailsView will be used to provide an interface for quickly adding a new product to the database. To keep a consistent user interface when adding a new product let's allow the user to only enter values for the ProductName and UnitPrice fields. By default, those values that aren't supplied in the DetailsView's inserting interface will be set to a NULL database value. However, we can use the ObjectDataSource's Inserting event to inject different default values, as we'll see shortly.

Step 3: Providing an Interface to Add New Products


Drag a DetailsView from the Toolbox onto the Designer above the GridView, clear out its Height and Widthproperties, and bind it to the ObjectDataSource already present on the page. This will add a BoundField or CheckBoxField for each of the product's fields. Since we want to use this DetailsView to add new products, we need to check the Enable Inserting option from the smart tag; however, there's no such option because the ObjectDataSource's Insert() method is not mapped to a method in the ProductsBLL class (recall that we set this mapping to (None) when configuring the data source see Figure 3). To configure the ObjectDataSource, select the Configure Data Source link from its smart tag, launching the wizard. The first screen allows you to change the underlying object the ObjectDataSource is bound to; leave it set toProductsBLL. The next screen lists the mappings from the ObjectDataSource's methods to the underlying object's. Even though we explicitly indicated that the Insert() and Delete() methods should not be mapped to any methods, if you go to the

INSERT and DELETE tabs you'll see that a mapping is there. This is because theProductsBLL's AddProduct and DeleteProduct methods use the DataObjectMethodAttribute attribute to indicate that they are the default methods for Insert() and Delete(), respectively. Hence, the ObjectDataSource wizard selects these each time you run the wizard unless there's some other value explicitly specified. Leave the Insert() method pointing to the AddProduct method, but again set the DELETE tab's drop-down list to (None).

Figure 14: Set the INSERT Tab's Drop-Down List to the AddProduct Method (Click to view full-size image)

Figure 15: Set the DELETE Tab's Drop-Down List to (None) (Click to view full-size image)

After making these changes, the ObjectDataSource's declarative syntax will be expanded to include anInsertParameters collection, as shown below: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct" OnUpdating="ObjectDataSource1_Updating" InsertMethod="AddProduct" OldValuesParameterFormatString="original_{0}"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> <InsertParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" /> <asp:Parameter Name="discontinued" Type="Boolean" /> </InsertParameters> </asp:ObjectDataSource> Rerunning the wizard added back the OldValuesParameterFormatString property. Take a moment to clear this property by setting it to the default value ({0}) or removing it altogether from the declarative syntax. With the ObjectDataSource providing inserting capabilities, the DetailsView's smart tag will now include the Enable Inserting checkbox; return to the Designer and check this option. Next, pare down the DetailsView so that it only has two BoundFields - ProductName and UnitPrice - and the CommandField. At this point the DetailsView's declarative syntax should look like: <asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Fields> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> <asp:CommandField ShowInsertButton="True" /> </Fields> </asp:DetailsView> Figure 16 shows this page when viewed through a browser at this point. As you can see, the DetailsView lists the name and price of the first product (Chai). What we want, however, is an inserting interface that provides a means for the user to quickly add a new product to the database.

Figure 16: The DetailsView is Currently Rendered in Read-Only Mode (Click to view full-size image) In order to show the DetailsView in its inserting mode we need to set the DefaultMode property to Inserting. This renders the DetailsView in insert mode when first visited and keeps it there after inserting a new record. As Figure 17 shows, such a DetailsView provides a quick interface for adding a new record.

Figure 17: The DetailsView Provides an Interface for Quickly Adding a New Product (Click to view full-size image) When the user enters a product name and price (such as "Acme Water" and 1.99, as in Figure 17) and clicks Insert, a postback ensues and the inserting workflow commences, culminating in a new product record being added to the database. The DetailsView maintains its inserting interface and the GridView is automatically rebound to its data source in order to include the new product, as shown in Figure 18.

Figure 18: The Product "Acme Water" Has Been Added to the Database While the GridView in Figure 18 doesn't show it, the product fields lacking from the DetailsView interfaceCategoryID, SupplierID, QuantityPerUnit, and so on are assigned NULL database values. You can see this by performing the following steps: 1. Go to the Server Explorer in Visual Studio 2. 3. 4. Expanding the NORTHWND.MDF database node Right-click on the Products database table node Select Show Table Data

This will list all of the records in the Products table. As Figure 19 shows, all of our new product's columns other than ProductID, ProductName, and UnitPrice have NULL values.

Figure 19: The Product Fields Not Provided in the DetailsView are Assigned NULL Values (Click to view full-size image)

We may want to provide a default value other than NULL for one or more of these column values, either becauseNULL isn't the best default option or because the database column itself doesn't allow NULLs. To accomplish this we can programmatically set the values of the parameters of the DetailsView's InputParameters collection. This assignment can be done either in the event handler for the DetailsView's ItemInserting event or the ObjectDataSource's Inserting event. Since we've already looked at using the pre- and post-level events at the data Web control level, let's explore using the ObjectDataSource's events this time.

Step 4: Assigning Values to the CategoryID and SupplierID Parameters


For this tutorial let's imagine that for our application when adding a new product through this interface it should be assigned a CategoryID and SupplierID value of 1. As mentioned earlier, the ObjectDataSource has a pair of pre- and post-level events that fire during the data modification process. When its Insert() method is invoked, the ObjectDataSource first raises its Inserting event, then calls the method that its Insert() method has been mapped to, and finally raises the Inserted event. The Inserting event handler affords us one last opportunity to tweak the input parameters or cancel the operation outright. Note: In a real-world application you would likely want to either let the user specify the category and supplier or would pick this value for them based on some criteria or business logic (rather than blindly selecting an ID of 1). Regardless, the example illustrates how to programmatically set the value of an input parameter from the ObjectDataSource's prelevel event. Take a moment to create an event handler for the ObjectDataSource's Inserting event. Notice that the event handler's second input parameter is an object of type ObjectDataSourceMethodEventArgs, which has a property to access the parameters collection (InputParameters) and a property to cancel the operation (Cancel). Protected Sub ObjectDataSource1_Inserting _ (sender As Object, e As ObjectDataSourceMethodEventArgs) _ Handles ObjectDataSource1.Inserting End Sub At this point, the InputParameters property contains the ObjectDataSource's InsertParameters collection with the values assigned from the DetailsView. To change the value of one of these parameters, simply use:e.InputParameters("paramName") = value. Therefore, to set the CategoryID and SupplierID to values of 1, adjust the Inserting event handler to look like the following: Protected Sub ObjectDataSource1_Inserting _ (sender As Object, e As ObjectDataSourceMethodEventArgs) _ Handles ObjectDataSource1.Inserting e.InputParameters("CategoryID") = 1 e.InputParameters("SupplierID") = 1 End Sub This time when adding a new product (such as Acme Soda), the CategoryID and SupplierID columns of the new product are set to 1 (see Figure 20).

Figure 20: New Products Now Have Their CategoryID and SupplierID Values Set to 1 (Click to view full-size image)

Summary
During the editing, inserting, and deleting process, both the data Web control and the ObjectDataSource proceed through a number of pre- and post-level events. In this tutorial we examined the pre-level events and saw how to use these to customize the input parameters or cancel the data modification operation altogether both from the data Web control and ObjectDataSource's events. In the next tutorial we'll look at creating and using event handlers for the postlevel events. Happy Programming!

Handling BLL- and DAL-Level Exceptions in an ASP.NET Page (VB)


In this tutorial we will see how to display a friendly, informative error message should an exception occur during an insert, update, or delete operation of an ASP.NET data Web control.

Introduction
Working with data from an ASP.NET web application using a tiered application architecture involves the following three general steps: 1. 2. 3. Determine what method of the Business Logic Layer needs to be invoked and what parameter values to pass it. The parameter values can be hard coded, programmatically-assigned, or inputs entered by the user. Invoke the method. Process the results. When calling a BLL method that returns data, this may involve binding the data to a data Web control. For BLL methods that modify data, this may include performing some action based on a return value or gracefully handling any exception that arose in Step 2.

As we saw in the previous tutorial, both the ObjectDataSource and the data Web controls provide extensibility points for Steps 1 and 3. The GridView, for example, fires its RowUpdating event prior to assigning its field values to its

ObjectDataSource's UpdateParameters collection; its RowUpdated event is raised after the ObjectDataSource has completed the operation. We've already examined the events that fire during Step 1 and have seen how they can be used to customize the input parameters or cancel the operation. In this tutorial we'll turn our attention to the events that fire after the operation has completed. With these post-level event handlers we can, among other things, determine if an exception occurred during the operation and handle it gracefully, displaying a friendly, informative error message on the screen rather than defaulting to the standard ASP.NET exception page. To illustrate working with these post-level events, let's create a page that lists the products in an editable GridView. When updating a product, if an exception is raised our ASP.NET page will display a short message above the GridView explaining that a problem has occurred. Let's get started!

Step 1: Creating an Editable GridView of Products


In the previous tutorial we created an editable GridView with just two fields, ProductName and UnitPrice. This required creating an additional overload for the ProductsBLL class's UpdateProduct method, one that only accepted three input parameters (the product's name, unit price, and ID) as opposed a parameter for each product field. For this tutorial, let's practice this technique again, creating an editable GridView that displays the product's name, quantity per unit, unit price, and units in stock, but only allows the name, unit price, and units in stock to be edited. To accommodate this scenario we'll need another overload of the UpdateProduct method, one that accepts four parameters: the product's name, unit price, units in stock, and ID. Add the following method to the ProductsBLLclass: <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Update, True)> _ Public Function UpdateProduct _ (ByVal productName As String, ByVal unitPrice As Nullable(Of Decimal), _ ByVal unitsInStock As Nullable(Of Short), ByVal productID As Integer) As Boolean Dim products As Northwind.ProductsDataTable = _ Adapter.GetProductByProductID(productID) If products.Count = 0 Then Return False End If Dim product As Northwind.ProductsRow = products(0) product.ProductName = productName If Not unitPrice.HasValue Then product.SetUnitPriceNull() Else product.UnitPrice = unitPrice.Value End If If Not unitsInStock.HasValue Then product.SetUnitsInStockNull() Else product.UnitsInStock = unitsInStock.Value End If Dim rowsAffected As Integer = Adapter.Update(product) Return rowsAffected = 1 End Function With this method complete, we're ready to create the ASP.NET page that allows for editing these four particular product fields. Open the ErrorHandling.aspx page in the EditInsertDelete folder and add a GridView to the page through

the Designer. Bind the GridView to a new ObjectDataSource, mapping the Select() method to theProductsBLL class's GetProducts() method and the Update() method to the UpdateProduct overload just created.

Figure 1: Use the UpdateProduct Method Overload That Accepts Four Input Parameters (Click to view full-size image) This will create an ObjectDataSource with an UpdateParameters collection with four parameters and a GridView with a field for each of the product fields. The ObjectDataSource's declarative markup assigns theOldValuesParameterFormatString property the value original_{0}, which will cause an exception since our BLL class's don't expect an input parameter named original_productID to be passed in. Don't forget to remove this setting altogether from the declarative syntax (or set it to the default value, {0}). Next, pare down the GridView to include only the ProductName, QuantityPerUnit, UnitPrice, and UnitsInStockBoundFields. Also feel free to apply any field-level formatting you deem necessary (such as changing theHeaderText properties). In the previous tutorial we looked at how to format the UnitPrice BoundField as a currency both in read-only mode and edit mode. Let's do the same here. Recall that this required setting the BoundField's DataFormatStringproperty to {0:c}, its HtmlEncode property to false, and its ApplyFormatInEditMode to true, as shown in Figure 2.

Figure 2: Configure the UnitPrice BoundField to Display as a Currency (Click to view full-size image) Formatting the UnitPrice as a currency in the editing interface requires creating an event handler for the GridView's RowUpdating event that parses the currency-formatted string into a decimal value. Recall that theRowUpdating event handler from the last tutorial also checked to ensure that the user provided a UnitPrice value. However, for this tutorial let's allow the user to omit the price. Protected Sub GridView1_RowUpdating(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs) _ Handles GridView1.RowUpdating If e.NewValues("UnitPrice") IsNot Nothing Then e.NewValues("UnitPrice") = _ Decimal.Parse(e.NewValues("UnitPrice").ToString(), _ System.Globalization.NumberStyles.Currency) End If Our GridView includes a QuantityPerUnit BoundField, but this BoundField should be only for display purposes and should not be editable by the user. To arrange this, simply set the BoundFields' ReadOnly property to true.

Figure 3: Make the QuantityPerUnit BoundField Read-Only (Click to view full-size image) Finally, check the Enable Editing checkbox from the GridView's smart tag. After completing these steps theErrorHandling.aspx page's Designer should look similar to Figure 4.

Figure 4: Remove All But the Needed BoundFields and Check the Enable Editing Checkbox (Click to view full-size image) At this point we have a list of all of the products' ProductName, QuantityPerUnit, UnitPrice, and UnitsInStockfields; however, only the ProductName, UnitPrice, and UnitsInStock fields can be edited.

Figure 5: Users Can Now Easily Edit Products' Names, Prices, and Units In Stock Fields (Click to view full-size image)

Step 2: Gracefully Handling DAL-Level Exceptions


While our editable GridView works wonderfully when users enter legal values for the edited product's name, price, and units in stock, entering illegal values results in an exception. For example, omitting the ProductName value causes a NoNullAllowedException to be thrown since the ProductName property in the ProdcutsRow class has itsAllowDBNull property set to false; if the database is down, a SqlException will be thrown by the TableAdapter when attempting to connect to the database. Without taking any action, these exceptions bubble up from the Data Access Layer to the Business Logic Layer, then to the ASP.NET page, and finally to the ASP.NET runtime. Depending on how your web application is configured and whether or not you're visiting the application fromlocalhost, an unhandled exception can result in either a generic server-error page, a detailed error report, or a user-friendly web page. See Web Application Error Handling in ASP.NET and the customErrors Element for more information on how the ASP.NET runtime responds to an uncaught exception. Figure 6 shows the screen encountered when attempting to update a product without specifying the ProductNamevalue. This is the default detailed error report displayed when coming through localhost.

Figure 6: Omitting the Product's Name Will Display Exception Details (Click to view full-size image) While such exception details are helpful when testing an application, presenting an end user with such a screen in the face of an exception is less than ideal. An end user likely doesn't know what a NoNullAllowedException is or why it was caused. A better approach is to present the user with a more user-friendly message explaining that there were problems attempting to update the product. If an exception occurs when performing the operation, the post-level events in both the ObjectDataSource and the data Web control provide a means to detect it and cancel the exception from bubbling up to the ASP.NET runtime. For our example, let's create an event handler for the GridView's RowUpdated event that determines if an exception has fired and, if so, displays the exception details in a Label Web control. Start by adding a Label to the ASP.NET page, setting its ID property to ExceptionDetails and clearing out itsText property. In order to draw the user's eye to this message, set its CssClass property to Warning, which is a CSS class we added to the Styles.css file in the previous tutorial. Recall that this CSS class causes the Label's text to be displayed in a red, italic, bold, extra large font.

Figure 7: Add a Label Web Control to the Page (Click to view full-size image) Since we want this Label Web control to be visible only immediately after an exception has occurred, set itsVisible property to false in the Page_Load event handler: Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load ExceptionDetails.Visible = False End Sub With this code, on the first page visit and subsequent postbacks the ExceptionDetails control will have itsVisible property set to false. In the face of a DAL- or BLL-level exception, which we can detect in the GridView'sRowUpdated event handler, we will set the ExceptionDetails control's Visible property to true. Since Web control event handlers occur after the Page_Load event handler in the page lifecycle, the Label will be shown. However, on the next postback, the Page_Load event handler will revert the Visible property back to false, hiding it from view again.

Note: Alternatively, we could remove the necessity for setting the ExceptionDetails control's Visible property inPage_Load by assigning its Visible property false in the declarative syntax and disabling its view state (setting itsEnableViewState property to false). We'll use this alternative approach in a future tutorial. With the Label control added, our next step is to create the event handler for the GridView's RowUpdated event. Select the GridView in the Designer, go to the Properties window, and click the lightning bolt icon, listing the GridView's events. There should already be an entry there for the GridView's RowUpdating event, as we created an event handler for this event earlier in this tutorial. Create an event handler for the RowUpdated event as well.

Figure 8: Create an Event Handler for the GridView's RowUpdated Event Note: You can also create the event handler through the drop-down lists at the top of the code-behind class file. Select the GridView from the drop-down list on the left and the RowUpdated event from the one on the right. Creating this event handler will add the following code to the ASP.NET page's code-behind class: Protected Sub GridView1_RowUpdated(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _ Handles GridView1.RowUpdated End Sub

This event handler's second input parameter is an object of type GridViewUpdatedEventArgs, which has three properties of interest for handling exceptions: Exception a reference to the thrown exception; if no exception has been thrown, this property will have a value of null ExceptionHandled a Boolean value that indicates whether or not the exception was handled in theRowUpdated event handler; if false (the default), the exception is re-thrown, percolating up to the ASP.NET runtime KeepInEditMode if set to true the edited GridView row remains in edit mode; if false (the default), the GridView row reverts back to its read-only mode Our code, then, should check to see if Exception is not null, meaning that an exception was raised while performing the operation. If this is the case, we want to: Display a user-friendly message in the ExceptionDetails Label Indicate that the exception was handled Keep the GridView row in edit mode This following code accomplishes these objectives: Protected Sub GridView1_RowUpdated(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _ Handles GridView1.RowUpdated If e.Exception IsNot Nothing Then ExceptionDetails.Visible = True ExceptionDetails.Text = "There was a problem updating the product. " If e.Exception.InnerException IsNot Nothing Then Dim inner As Exception = e.Exception.InnerException If TypeOf inner Is System.Data.Common.DbException Then ExceptionDetails.Text &= _ "Our database is currently experiencing problems." & _ "Please try again later." ElseIf TypeOf inner _ Is System.Data.NoNullAllowedException Then ExceptionDetails.Text += _ "There are one or more required fields that are missing." ElseIf TypeOf inner Is ArgumentException Then Dim paramName As String = CType(inner, ArgumentException).ParamName ExceptionDetails.Text &= _ String.Concat("The ", paramName, " value is illegal.") ElseIf TypeOf inner Is ApplicationException Then ExceptionDetails.Text += inner.Message End If End If e.ExceptionHandled = True e.KeepInEditMode = True End If End Sub This event handler begins by checking to see if e.Exception is null. If it's not, the ExceptionDetails Label'sVisible property is set to true and its Text property to "There was a problem updating the product." The details of the actual exception that was thrown reside in the e.Exception object's InnerException property. This inner exception is examined and, if it is of a particular type,

an additional, helpful message is appended to theExceptionDetails Label's Text property. Lastly, the ExceptionHandled and KeepInEditMode properties are both set to true. Figure 9 shows a screen shot of this page when omitting the name of the product; Figure 10 shows the results when entering an illegal UnitPrice value (-50).

Figure 9: The ProductName BoundField Must Contain a Value (Click to view full-size image)

Figure 10: Negative UnitPrice Values are Not Allowed (Click to view full-size image) By setting the e.ExceptionHandled property to true, the RowUpdated event handler has indicated that it has handled the exception. Therefore, the exception won't propagate up to the ASP.NET runtime. Note: Figures 9 and 10 show a graceful way to handle exceptions raised due to invalid user input. Ideally, though, such invalid input will never reach the Business Logic Layer in the first place, as the ASP.NET page should ensure that the user's inputs are valid before invoking the ProductsBLL class's UpdateProduct method. In our next tutorial we'll see how to add validation controls to the editing and inserting interfaces to ensure that the data submitted to the Business Logic Layer conforms to the business rules. The validation controls not only prevent the invocation of

the UpdateProduct method until the user-supplied data is valid, but also provide a more informative user experience for identifying data entry problems.

Step 3: Gracefully Handling BLL-Level Exceptions


When inserting, updating, or deleting data, the Data Access Layer may throw an exception in the face of a data-related error. The database may be offline, a required database table column might not have had a value specified, or a tablelevel constraint may have been violated. In addition to strictly data-related exceptions, the Business Logic Layer can use exceptions to indicate when business rules have been violated. In the Creating a Business Logic Layertutorial, for example, we added a business rule check to the original UpdateProduct overload. Specifically, if the user was marking a product as discontinued, we required that the product not be the only one provided by its supplier. If this condition was violated, an ApplicationException was thrown. For the UpdateProduct overload created in this tutorial, let's add a business rule that prohibits the UnitPrice field from being set to a new value that's more than twice the original UnitPrice value. To accomplish this, adjust theUpdateProduct overload so that it performs this check and throws an ApplicationException if the rule is violated. The updated method follows: <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Update, True)> _ Public Function UpdateProduct(ByVal productName As String, _ ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _ ByVal productID As Integer) As Boolean Dim products As Northwind.ProductsDataTable = Adapter.GetProductByProductID(productID) If products.Count = 0 Then Return False End If Dim product As Northwind.ProductsRow = products(0) If unitPrice.HasValue AndAlso Not product.IsUnitPriceNull() Then If unitPrice > product.UnitPrice * 2 Then Throw New ApplicationException( _ "When updating a product price," & _ " the new price cannot exceed twice the original price.") End If End If product.ProductName = productName If Not unitPrice.HasValue Then product.SetUnitPriceNull() Else product.UnitPrice = unitPrice.Value End If If Not unitsInStock.HasValue Then product.SetUnitsInStockNull() Else product.UnitsInStock = unitsInStock.Value End If Dim rowsAffected As Integer = Adapter.Update(product) Return rowsAffected = 1 End Function With this change, any price update that is more than twice the existing price will cause an ApplicationExceptionto be thrown. Just like the exception raised from the DAL, this BLL-raised ApplicationException can be detected and

handled in the GridView's RowUpdated event handler. In fact, the RowUpdated event handler's code, as written, will correctly detect this exception and display the ApplicationException's Message property value. Figure 11 shows a screen shot when a user attempts to update the price of Chai to $50.00, which is more than double its current price of $19.95.

Figure 11: The Business Rules Disallow Price Increases That More Than Double a Product's Price (Click to view full-size image) Note: Ideally our business logic rules would be refactored out of the UpdateProduct method overloads and into a common method. This is left as an exercise for the reader.

Summary
During inserting, updating, and deleting operations, both the data Web control and the ObjectDataSource involved fire pre- and post-level events that bookend the actual operation. As we saw in this tutorial and the preceding one, when working with an editable GridView the GridView's RowUpdating event fires, followed by the ObjectDataSource's Updating event, at which point the update command is made to the ObjectDataSource's underlying object. After the operation has completed, the ObjectDataSource's Updated event fires, followed by the GridView's RowUpdated event. We can create event handlers for the pre-level events in order to customize the input parameters or for the post-level events in order to inspect and respond to the operation's results. Post-level event handlers are most commonly used to detect whether an exception occurred during the operation. In the face of an exception, these post-level event handlers can optionally handle the exception on their own. In this tutorial we saw how to handle such an exception by displaying a friendly error message. In the next tutorial we'll see how to lessen the likelihood of exceptions arising from data formatting issues (such as entering a negative UnitPrice). Specifically, we'll look at how to add validation controls to the editing and inserting interfaces. Happy Programming!

Adding Validation Controls to the Editing and Inserting Interfaces (VB)


In this tutorial we'll see how easy it is to add validation controls to the EditItemTemplate and InsertItemTemplate of a data Web control, to provide a more foolproof user interface.

Introduction
The GridView and DetailsView controls in the examples we've explored over the past three tutorials have all been composed of BoundFields and CheckBoxFields (the field types automatically added by Visual Studio when binding a GridView or DetailsView to a data source control through the smart tag). When editing a row in a GridView or DetailsView, those BoundFields that are not read-only are converted into textboxes, from which the end user can modify the existing data. Similarly, when inserting a new record into a DetailsView control, those BoundFields whoseInsertVisible property is set to True (the default) are rendered as empty textboxes, into which the user can provide the new record's field values. Likewise, CheckBoxFields, which are disabled in the standard, read-only interface, are converted into enabled checkboxes in the editing and inserting interfaces. While the default editing and inserting interfaces for the BoundField and CheckBoxField can be helpful, the interface lacks any sort of validation. If a user makes a data entry mistake - such as omitting the ProductName field or entering an invalid value for UnitsInStock (such as -50) an exception will be raised from within the depths of the application architecture. While this exception can be gracefully handled as demonstrated in the previous tutorial, ideally the editing or inserting user interface would include validation controls to prevent a user from entering such invalid data in the first place. In order to provide a customized editing or inserting interface, we need to replace the BoundField or CheckBoxField with a TemplateField. TemplateFields, which were the topic of discussion in the Using TemplateFields in the GridView Control and Using TemplateFields in the DetailsView Control tutorials, can consist of multiple templates defining separate interfaces for different row states. The TemplateField's ItemTemplate is used to when rendering read-only fields or rows in the DetailsView or GridView controls, whereas the EditItemTemplate and InsertItemTemplateindicate the interfaces to use for the editing and inserting modes, respectively. In this tutorial we'll see how easy it is to add validation controls to the TemplateField's EditItemTemplate andInsertItemTemplate to provide a more foolproof user interface. Specifically, this tutorial takes the example created in the Examining the Events Associated with Inserting, Updating, and Deleting tutorial and augments the editing and inserting interfaces to include appropriate validation.

Step 1: Replicating the Example from Examining the Events Associated with Inserting, Updating, and Deleting
In the Examining the Events Associated with Inserting, Updating, and Deleting tutorial we created a page that listed the names and prices of the products in an editable GridView. Additionally, the page included a DetailsView whoseDefaultMode property was set to Insert, thereby always rendering in insert mode. From this DetailsView, the user could enter the name and price for a new product, click Insert, and have it added to the system (see Figure 1).

Figure 1: The Previous Example Allows Users to Add New Products and Edit Existing Ones (Click to view full-size image) Our goal for this tutorial is to augment the DetailsView and GridView to provide validation controls. In particular, our validation logic will: Require that the name be provided when inserting or editing a product Require that the price be provided when inserting a record; when editing a record, we will still require a price, but will use the programmatic logic in the GridView's RowUpdating event handler already present from the earlier tutorial Ensure that the value entered for the price is a valid currency format

Before we can look at augmenting the previous example to include validation, we first need to replicate the example from the DataModificationEvents.aspx page to the page for this tutorial, UIValidation.aspx. To accomplish this we need to copy over both the DataModificationEvents.aspx page's declarative markup and its source code. First copy over the declarative markup by performing the following steps: 1. Open the DataModificationEvents.aspx page in Visual Studio 2. Go to the page's declarative markup (click on the Source button at the bottom of the page) 3. Copy the text within the <asp:Content> and </asp:Content> tags (lines 3 through 44), as shown in Figure 2.

Figure 2: Copy the Text Within the <asp:Content> Control (Click to view full-size image) 1. Open the UIValidation.aspx page 2. Go to the page's declarative markup 3. Paste the text within the <asp:Content> control. To copy over the source code, open the DataModificationEvents.aspx.vb page and copy just the text within theEditInsertDelete_DataModificationEvents class. Copy the three event handlers (Page_Load,GridView1_RowUpdating, and ObjectDataSource1_Inserting), but do not copy the class declaration. Paste the copied text within the EditInsertDelete_UIValidation class in UIValidation.aspx.vb. After moving over the content and code from DataModificationEvents.aspx to UIValidation.aspx, take a moment to test out your progress in a browser. You should see the same output and experience the same functionality in each of these two pages (refer back to Figure 1 for a screen shot ofDataModificationEvents.aspx in action).

Step 2: Converting the BoundFields Into TemplateFields


To add validation controls to the editing and inserting interfaces, the BoundFields used by the DetailsView and GridView controls need to be converted into TemplateFields. To achieve this, click on the Edit Columns and Edit Fields links in the GridView and DetailsView's smart tags, respectively. There, select each of the BoundFields and click the "Convert this field into a TemplateField" link.

Figure 3: Convert Each of the DetailsView's and GridView's BoundFields Into TemplateFields (Click to view full-size image) Converting a BoundField into a TemplateField through the Fields dialog box generates a TemplateField that exhibits the same read-only, editing, and inserting interfaces as the BoundField itself. The following markup shows the declarative syntax for the ProductName field in the DetailsView after it has been converted into a TemplateField: <asp:TemplateField HeaderText="ProductName" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> </EditItemTemplate> <InsertItemTemplate> <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> </InsertItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> Note that this TemplateField had three templates automatically created ItemTemplate, EditItemTemplate, andInsertItemTemplate. The ItemTemplate displays a single data field value (ProductName) using a Label Web control, while the EditItemTemplate and InsertItemTemplate present the data field value in a TextBox Web control that associates the data field with the TextBox's Text property using two-way databinding. Since we are only using the DetailsView in this page for inserting, you may remove the ItemTemplate and EditItemTemplate from the two TemplateFields, although there's no harm in leaving them. Since the GridView does not support the built-in inserting features of the DetailsView, converting the GridView'sProductName field into a TemplateField results in only an ItemTemplate and EditItemTemplate:

<asp:TemplateField HeaderText="ProductName" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> By clicking the "Convert this field into a TemplateField," Visual Studio has created a TemplateField whose templates mimic the user interface of the converted BoundField. You can verify this by visiting this page through a browser. You'll find that the appearance and behavior of the TemplateFields is identical to the experience when BoundFields were used instead. Note: Feel free to customize the editing interfaces in the templates as needed. For example, we may want to have the TextBox in the UnitPrice TemplateFields rendered as a smaller textbox than the ProductName textbox. To accomplish this you can set the TextBox's Columns property to an appropriate value or provide an absolute width via the Width property. In the next tutorial we'll see how to completely customize the editing interface by replacing the TextBox with an alternate data entry Web control.

Step 3: Adding the Validation Controls to the GridView'sEditItemTemplates


When constructing data entry forms, it is important that users enter any required fields and that all provided inputs are legal, properly-formatted values. To help ensure that a user's inputs are valid, ASP.NET provides five built-in validation controls that are designed to be used to validate the value of a single input control: RequiredFieldValidator ensures that a value has been provided CompareValidator validates a value against another Web control value or a constant value, or ensures that the value's format is legal for a specified data type RangeValidator ensures that a value is within a range of values RegularExpressionValidator validates a value against a regular expression CustomValidator validates a value against a custom, user-defined method For more information on these five controls, check out the Validation Controls section of the ASP.NET Quickstart Tutorials. For our tutorial we'll need to use a RequiredFieldValidator in both the DetailsView and GridView's ProductNameTemplateFields and a RequiredFieldValidator in the DetailsView's UnitPrice TemplateField. Furthermore, we'll need to add a CompareValidator to both controls' UnitPrice TemplateFields that ensures that the entered price has a value greater than or equal to 0 and is presented in a valid currency format. Note: While ASP.NET 1.x had these same five validation controls, ASP.NET 2.0 has added a number of improvements, the main two being client-side script support for browsers other than Internet Explorer and the ability to partition validation controls on a page into validation groups. For more information on the new validation control features in 2.0, refer to Dissecting the Validation Controls in ASP.NET 2.0. Let's start by adding the necessary validation controls to the EditItemTemplates in the GridView's TemplateFields. To accomplish this, click on the Edit Templates link from the GridView's smart tag to bring up the template editing interface. From here, you can select which template to edit from the drop-down list. Since we want to augment the editing interface, we need to add validation controls to the ProductName and UnitPrice's EditItemTemplates.

Figure 4: We Need to Extend the ProductName and UnitPrice's EditItemTemplates (Click to view full-size image) In the ProductName EditItemTemplate, add a RequiredFieldValidator by dragging it from the Toolbox into the template editing interface, placing after the TextBox.

Figure 5: Add a RequiredFieldValidator to the ProductName EditItemTemplate (Click to view full-size image) All validation controls work by validating the input of a single ASP.NET Web control. Therefore, we need to indicate that the RequiredFieldValidator we just added should validate against the TextBox in the EditItemTemplate; this is accomplished by setting the validation control's ControlToValidate property to the ID of the appropriate Web control. The TextBox currently has the rather nondescript ID of TextBox1, but let's change it to something more appropriate. Click on the TextBox in the template and then, from the Properties window, change the ID fromTextBox1 to EditProductName.

Figure 6: Change the TextBox's ID to EditProductName (Click to view full-size image) Next, set the RequiredFieldValidator's ControlToValidate property to EditProductName. Finally, set theErrorMessage property to "You must provide the product's name" and the Text property to "*". The Text property value, if provided, is the text that is displayed by the validation control if the validation fails. The ErrorMessageproperty value, which is required, is used by the ValidationSummary control; if the Text property value is omitted, the ErrorMessage property value is also the text displayed by the validation control on invalid input. After setting these three properties of the RequiredFieldValidator, your screen should look similar to Figure 7.

Figure 7: Set the RequiredFieldValidator's ControlToValidate, ErrorMessage, and Text Properties (Click to view fullsize image) With the RequiredFieldValidator added to the ProductName EditItemTemplate, all that remains is to add the necessary validation to the UnitPrice EditItemTemplate. Since we've decided that, for this page, the UnitPrice is optional when editing a record, we don't need to add a RequiredFieldValidator. We do, however, need to add a CompareValidator to ensure that the UnitPrice, if supplied, is properly formatted as a currency and is greater than or equal to 0.

Before we add the CompareValidator to the UnitPrice EditItemTemplate, let's first change the TextBox Web control's ID from TextBox2 to EditUnitPrice. After making this change, add the CompareValidator, setting itsControlToValidate property to EditUnitPrice, its ErrorMessage property to "The price must be greater than or equal to zero and cannot include the currency symbol", and its Text property to "*". To indicate that the UnitPrice value must be greater than or equal to 0, set the CompareValidator's Operator property to GreaterThanEqual, its ValueToCompare property to "0", and its Type property to Currency. The following declarative syntax shows the UnitPrice TemplateField's EditItemTemplate after these changes have been made: <EditItemTemplate> <asp:TextBox ID="EditUnitPrice" runat="server" Text='<%# Bind("UnitPrice", "{0:c}") %>' Columns="6"></asp:TextBox> <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="EditUnitPrice" ErrorMessage="The price must be greater than or equal to zero and cannot include the currency symbol" Operator="GreaterThanEqual" Type="Currency" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> After making these changes, open the page in a browser. If you attempt to omit the name or enter an invalid price value when editing a product, an asterisk appears next to the textbox. As Figure 8 shows, a price value that includes the currency symbol such as $19.95 is considered invalid. The CompareValidator's Currency Type allows for digit separators (such as commas or periods, depending on the culture settings) and a leading plus or minus sign, but does not permit a currency symbol. This behavior may perplex users as the editing interface currently renders theUnitPrice using the currency format. Note: Recall that in the Events Associated with Inserting, Updating, and Deleting tutorial we set the BoundField'sDataFormatString property to {0:c} in order to format it as a currency. Furthermore, we set theApplyFormatInEditMode property to true, causing the GridView's editing interface to format the UnitPrice as a currency. When converting the BoundField into a TemplateField, Visual Studio noted these settings and formatted the TextBox's Text property as a currency using the databinding syntax <%# Bind("UnitPrice", "{0:c}") %>.

Figure 8: An Asterisk Appears Next to the Textboxes with Invalid Input (Click to view full-size image) While the validation works as-is, the user has to manually remove the currency symbol when editing a record, which is not acceptable. To remedy this, we have three options: 1. 2. Configure the EditItemTemplate so that the UnitPrice value is not formatted as a currency. Allow the user to enter a currency symbol by removing the CompareValidator and replacing it with a RegularExpressionValidator that properly checks for a properly formatted currency value. The problem here is that the regular expression to validate a currency value is not pretty and would require writing code if we wanted to incorporate culture settings.

Remove the validation control altogether and rely on server-side validation logic in the GridView'sRowUpdating event handler. Let's go with option #1 for this exercise. Currently the UnitPrice is formatted as a currency due to the databinding expression for the TextBox in the EditItemTemplate: <%# Bind("UnitPrice", "{0:c}") %>. Change the Bind statement to Bind("UnitPrice", "{0:n2}"), which formats the result as a number with two digits of precision. This can be done directly through the declarative syntax or by clicking on the Edit DataBindings link from theEditUnitPrice TextBox in the UnitPrice TemplateField's EditItemTemplate (see Figures 9 and 10).

3.

Figure 9: Click on the TextBox's Edit DataBindings link (Click to view full-size image)

Figure 10: Specify the Format Specifier in the Bind Statement (Click to view full-size image) With this change, the formatted price in the editing interface includes commas as the group separator and a period as the decimal separator, but leaves off the currency symbol. Note: The UnitPrice EditItemTemplate doesn't include a RequiredFieldValidator, allowing the postback to ensue and the updating logic to commence. However, the RowUpdating event handler copied over from the Examining the Events Associated with Inserting, Updating, and Deleting tutorial includes a programmatic check that ensures that theUnitPrice is provided. Feel free to remove this logic, leave it in as-is, or add a RequiredFieldValidator to theUnitPrice EditItemTemplate.

Step 4: Summarizing Data Entry Problems


In addition to the five validation controls, ASP.NET includes the ValidationSummary control, which displays theErrorMessages of those validation controls that detected invalid data. This summary data can be displayed as text on the web page or through a modal, client-side messagebox. Let's enhance this tutorial to include a client-side messagebox summarizing any validation problems. To accomplish this, drag a ValidationSummary control from the Toolbox onto the Designer. The location of the Validation control doesn't really matter, since we're going to configure it to only display the summary as a messagebox. After adding the control, set its ShowSummary property to False and its ShowMessageBox propertyto True. With this addition, any validation errors are summarized in a client-side messagebox.

Figure 11: The Validation Errors are Summarized in a Client-Side Messagebox (Click to view full-size image)

Step 5: Adding the Validation Controls to the DetailsView'sInsertItemTemplate


All that remains for this tutorial is to add the validation controls to the DetailsView's inserting interface. The process of adding validation controls to the DetailsView's templates is identical to that examined in Step 3; therefore, we'll breeze through the task in this step. As we did with the GridView's EditItemTemplates, I encourage you to rename the IDs of the TextBoxes from the nondescript TextBox1 and TextBox2 to InsertProductName andInsertUnitPrice. Add a RequiredFieldValidator to the ProductName InsertItemTemplate. Set the ControlToValidate to the ID of the TextBox in the template, its Text property to "*" and its ErrorMessage property to "You must provide the product's name". Since the UnitPrice is required for this page when adding a new record, add a RequiredFieldValidator to theUnitPrice InsertItemTemplate, setting its ControlToValidate, Text, and ErrorMessage properties appropriately. Finally, add a CompareValidator to the UnitPrice InsertItemTemplate as well, configuring itsControlToValidate, Text, ErrorMessage, Type, Operator, and ValueToCompare properties just like we did with the UnitPrice's CompareValidator in the GridView's EditItemTemplate. After adding these validation controls, a new product cannot be added to the system if its name is not supplied or if its price is a negative number or illegally formatted.

Figure 12: Validation Logic has been Added to the DetailsView's Inserting Interface (Click to view full-size image)

Step 6: Partitioning the Validation Controls Into Validation Groups


Our page consists of two logically disparate sets of validation controls: those that correspond to the GridView's editing interface and those that correspond to the DetailsView's inserting interface. By default, when a postback occurs all validation controls on the page are checked. However, when editing a record we don't want the DetailsView's inserting interface's validation controls to validate. Figure 13 illustrates our current dilemma when a user is editing a product with perfectly legal values, clicking Update causes a validation error because the name and price values in the inserting interface are blank.

Figure 13: Updating a Product Causes the Inserting Interface's Validation Controls to Fire (Click to view full-size image)

The validation controls in ASP.NET 2.0 can be partitioned into validation groups through their ValidationGroupproperty. To associate a set of validation controls in a group, simply set their ValidationGroup property to the same value. For our tutorial, set the ValidationGroup properties of the validation controls in the GridView's TemplateFields to EditValidationControls and the ValidationGroup properties of the DetailsView's TemplateFields to InsertValidationControls. These changes can be done directly in the declarative markup or through the Properties window when using the Designer's edit template interface. In addition to the validation controls, the Button and Button-related controls in ASP.NET 2.0 also include aValidationGroup property. A validation group's validators are checked for validity only when a postback is induced by a Button that has the same ValidationGroup property setting. For example, in order for the DetailsView's Insert button to trigger the InsertValidationControls validation group we need to set the CommandField's ValidationGroup property to InsertValidationControls (see Figure 14). Additionally, set the GridView's CommandField's ValidationGroup property to EditValidationControls.

Figure 14: Set the DetailsView's CommandField's ValidationGroup property to InsertValidationControls (Click to view full-size image) After these changes, the DetailsView and GridView's TemplateFields and CommandFields should look similar to the following: The DetailsView's TemplateFields and CommandField <asp:TemplateField HeaderText="ProductName" SortExpression="ProductName"> <InsertItemTemplate> <asp:TextBox ID="InsertProductName" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ControlToValidate="InsertProductName" ErrorMessage="You must provide the product name"

ValidationGroup="InsertValidationControls">* </asp:RequiredFieldValidator> </InsertItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="UnitPrice" SortExpression="UnitPrice"> <InsertItemTemplate> <asp:TextBox ID="InsertUnitPrice" runat="server" Text='<%# Bind("UnitPrice") %>' Columns="6"> </asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator3" runat="server" ControlToValidate="InsertUnitPrice" ErrorMessage="You must provide the product price" ValidationGroup="InsertValidationControls">* </asp:RequiredFieldValidator> <asp:CompareValidator ID="CompareValidator2" runat="server" ControlToValidate="InsertUnitPrice" ErrorMessage="The price must be greater than or equal to zero and cannot include the currency symbol" Operator="GreaterThanEqual" Type="Currency" ValueToCompare="0" ValidationGroup="InsertValidationControls">* </asp:CompareValidator> </InsertItemTemplate> </asp:TemplateField> <asp:CommandField ShowInsertButton="True" ValidationGroup="InsertValidationControls" /> The GridView's CommandField and TemplateFields <asp:CommandField ShowEditButton="True" ValidationGroup="EditValidationControls" /> <asp:TemplateField HeaderText="ProductName" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="EditProductName" runat="server" Text='<%# Bind("ProductName") %>'> </asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="EditProductName" ErrorMessage="You must provide the product name" ValidationGroup="EditValidationControls">* </asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="UnitPrice" SortExpression="UnitPrice"> <EditItemTemplate> <asp:TextBox ID="EditUnitPrice" runat="server"

Text='<%# Bind("UnitPrice", "{0:n2}") %>' Columns="6"></asp:TextBox> <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="EditUnitPrice" ErrorMessage="The price must be greater than or equal to zero and cannot include the currency symbol" Operator="GreaterThanEqual" Type="Currency" ValueToCompare="0" ValidationGroup="EditValidationControls">* </asp:CompareValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server" Text='<%# Bind("UnitPrice", "{0:c}") %>'> </asp:Label> </ItemTemplate> </asp:TemplateField> At this point the edit-specific validation controls fire only when the GridView's Update button is clicked and the insertspecific validation controls fire only when the DetailsView's Insert button is clicked, resolving the problem highlighted by Figure 13. However, with this change our ValidationSummary control no longer displays when entering invalid data. The ValidationSummary control also contains a ValidationGroup property and only shows summary information for those validation controls in its validation group. Therefore, we need to have two validation controls in this page, one for the InsertValidationControls validation group and one forEditValidationControls. <asp:ValidationSummary ID="ValidationSummary1" runat="server" ShowMessageBox="True" ShowSummary="False" ValidationGroup="EditValidationControls" /> <asp:ValidationSummary ID="ValidationSummary2" runat="server" ShowMessageBox="True" ShowSummary="False" ValidationGroup="InsertValidationControls" /> With this addition our tutorial is complete!

Summary
While BoundFields can provide both an inserting and editing interface, the interface is not customizable. Commonly, we want to add validation controls to the editing and inserting interface to ensure that the user enters required inputs in a legal format. To accomplish this we must convert the BoundFields into TemplateFields and add the validation controls to the appropriate template(s). In this tutorial we extended the example from the Examining the Events Associated with Inserting, Updating, and Deleting tutorial, adding validation controls to both the DetailsView's inserting interface and the GridView's editing interface. Moreover, we saw how to display summary validation information using the ValidationSummary control and how to partition the validation controls on the page into distinct validation groups. As we saw in this tutorial, TemplateFields allow the editing and inserting interfaces to be augmented to include validation controls. TemplateFields can also be extended to include additional input Web controls, enabling the TextBox to be replaced by a more suitable Web control. In our next tutorial we'll see how to replace the TextBox control with a data-bound DropDownList control, which is ideal when editing a foreign key (such as CategoryID orSupplierID in the Products table). Happy Programming!

Customizing the Data Modification Interface (VB)


In this tutorial we'll look at how to customize the interface of an editable GridView, by replacing the standard TextBox and CheckBox controls with alternative input Web controls.

Introduction
The BoundFields and CheckBoxFields used by the GridView and DetailsView controls simplify the process of modifying data due to their ability to render read-only, editable, and insertable interfaces. These interfaces can be rendered without the need for adding any additional declarative markup or code. However, the BoundField and CheckBoxField's interfaces lack the customizability often needed in real-world scenarios. In order to customize the editable or insertable interface in a GridView or DetailsView we need to instead use a TemplateField. In the preceding tutorial we saw how to customize the data modification interfaces by adding validation Web controls. In this tutorial we'll look at how to customize the actual data collection Web controls, replacing the BoundField and CheckBoxField's standard TextBox and CheckBox controls with alternative input Web controls. In particular, we'll build an editable GridView that allows a product's name, category, supplier, and discontinued status to be updated. When editing a particular row, the category and supplier fields will render as DropDownLists, containing the set of available categories and suppliers to choose from. Furthermore, we'll replace the CheckBoxField's default CheckBox with a RadioButtonList control that offers two options: "Active" and "Discontinued".

Figure 1: The GridView's Editing Interface Includes DropDownLists and RadioButtons (Click to view full-size image)

Step 1: Creating the Appropriate UpdateProduct Overload


In this tutorial we will build an editable GridView that permits editing of a product's name, category, supplier, and discontinued status. Therefore, we need an UpdateProduct overload that accepts five input parameters these four product values plus the ProductID. Like in our previous overloads, this one will: 1. Retrieve the product information from the database for the specified ProductID, 2. Update the ProductName, CategoryID, SupplierID, and Discontinued fields, and 3. Send the update request to the DAL through the TableAdapter's Update() method.

For brevity, for this particular overload I've omitted the business rule check that ensures a product being marked as discontinued isn't the only product offered by its supplier. Feel free to add it in if you prefer, or, ideally, refactor out the logic to a separate method. The following code shows the new UpdateProduct overload in the ProductsBLL class: <System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Upda te, False)> Public Function UpdateProduct( ByVal productName As String, ByVal categoryID As Nullable(Of Integer), ByVal supplierID As Nullable(Of Integer), ByVal discontinued As Boolean, ByVal productID As Integer) As Boolean Dim products As Northwind.ProductsDataTable = Adapter.GetProductByProductID(productID) If products.Count = 0 Then Return False End If Dim product As Northwind.ProductsRow = products(0) product.ProductName = productName If Not supplierID.HasValue Then product.SetSupplierIDNull() Else product.SupplierID = supplierID.Value End If If Not categoryID.HasValue Then product.SetCategoryIDNull() Else product.CategoryID = categoryID.Value End If product.Discontinued = discontinued Dim rowsAffected As Integer = Adapter.Update(product) Return rowsAffected = 1 End Function

Step 2: Crafting the Editable GridView


With the UpdateProduct overload added, we're ready to create our editable GridView. Open theCustomizedUI.aspx page in the EditInsertDelete folder and add a GridView control to the Designer. Next, create a new ObjectDataSource from the GridView's smart tag. Configure the ObjectDataSource to retrieve product information via the ProductBLL class's GetProducts() method and to update product data using theUpdateProduct overload we just created. From the INSERT and DELETE tabs, select (None) from the drop-down lists.

Figure 2: Configure the ObjectDataSource to Use the UpdateProduct Overload Just Created (Click to view full-size image) As we've seen throughout the data modification tutorials, the declarative syntax for the ObjectDataSource created by Visual Studio assigns the OldValuesParameterFormatString property to original_{0}. This, of course, won't work with our Business Logic Layer since our methods don't expect the original ProductID value to be passed in. Therefore, as we've done in previous tutorials, take a moment to remove this property assignment from the declarative syntax or, instead, set this property's value to {0}. After this change, the ObjectDataSource's declarative markup should look like the following: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="discontinued" Type="Boolean" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource> Note that the OldValuesParameterFormatString property has been removed and that there is a Parameter in theUpdateParameters collection for each of the input parameters expected by our UpdateProduct overload. While the ObjectDataSource is configured to update only a subset of product values, the GridView currently showsall of the product fields. Take a moment to edit the GridView so that: It only includes the ProductName, SupplierName, CategoryName BoundFields and the DiscontinuedCheckBoxField The CategoryName and SupplierName fields to appear before (to the left of) the Discontinued CheckBoxField The CategoryName and SupplierName BoundFields' HeaderText property is set to "Category" and "Supplier", respectively

Editing support is enabled (check the Enable Editing checkbox in the GridView's smart tag)

After these changes, the Designer will look similar to Figure 3, with the GridView's declarative syntax shown below.

Figure 3: Remove the Unneeded Fields from the GridView (Click to view full-size image) <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="CategoryName" HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="Supplier" ReadOnly="True" SortExpression="SupplierName" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> </Columns> </asp:GridView> At this point the GridView's read-only behavior is complete. When viewing the data, each product is rendered as a row in the GridView, showing the product's name, category, supplier, and discontinued status.

Figure 4: The GridView's Read-Only Interface is Complete (Click to view full-size image) Note: As discussed in An Overview of Inserting, Updating, and Deleting Data tutorial, it is vitally important that the GridView s view state be enabled (the default behavior). If you set the GridView s EnableViewState property tofalse, you run the risk of having concurrent users unintentionally deleting or editing records. See WARNING: Concurrency Issue with ASP.NET 2.0 GridViews/DetailsView/FormViews that Support Editing and/or Deleting and Whose View State is Disabled for more information.

Step 3: Using a DropDownList for the Category and Supplier Editing Interfaces
Recall that the ProductsRow object contains CategoryID, CategoryName, SupplierID, and SupplierNameproperties, which provide the actual foreign-key ID values in the Products database table and the correspondingName values in the Categories and Suppliers tables. The ProductRow's CategoryID and SupplierID can both be read from and written to, while the CategoryName and SupplierName properties are marked read-only. Due to the read-only status of the CategoryName and SupplierName properties, the corresponding BoundFields have had their ReadOnly property set to True, preventing these values from being modified when a row is edited. While we can set the ReadOnly property to False, rendering the CategoryName and SupplierName BoundFields as TextBoxes during editing, such an approach will result in an exception when the user attempts to update the product since there is no UpdateProduct overload that takes in CategoryName and SupplierName inputs. In fact, we don't want to create such an overload for two reasons: The Products table doesn't have SupplierName or CategoryName fields, but SupplierID and CategoryID. Therefore, we want our method to be passed these particular ID values, not their lookup tables' values. Requiring the user to type in the name of the supplier or category is less than ideal, as it requires the user to know the available categories and suppliers and their correct spellings. The supplier and category fields should display the category and suppliers' names when in read-only mode (as it does now) and a drop-down list of applicable options when being edited. Using a drop-down list, the end user can quickly see what categories and suppliers are available to choose among and can more easily make their selection. To provide this behavior, we need to convert the SupplierName and CategoryName BoundFields into TemplateFields whose ItemTemplate emits the SupplierName and CategoryName values and whoseEditItemTemplate uses a DropDownList control to list the available categories and suppliers.

Adding the Categories and Suppliers DropDownLists


Start by converting the SupplierName and CategoryName BoundFields into TemplateFields by: clicking on the Edit Columns link from the GridView's smart tag; selecting the BoundField from the list in the lower left; and clicking the "Convert this field into a TemplateField" link. The conversion process will create a TemplateField with both anItemTemplate and an EditItemTemplate, as shown in the declarative syntax below: <asp:TemplateField HeaderText="Category" SortExpression="CategoryName"> <EditItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Eval("CategoryName") %>'></asp:Label> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("CategoryName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> Since the BoundField was marked as read-only, both the ItemTemplate and EditItemTemplate contain a Label Web control whose Text property is bound to the applicable data field (CategoryName, in the syntax above). We need to modify the EditItemTemplate, replacing the Label Web control with a DropDownList control. As we've seen in previous tutorials, the template can be edited through the Designer or directly from the declarative syntax. To edit it through the Designer, click on the Edit Templates link from the GridView's smart tag and choose to work with the Category field's EditItemTemplate. Remove the Label Web control and replace it with a DropDownList control, setting the DropDownList's ID property to Categories.

Figure 5: Remove the TexBox and Add a DropDownList to the EditItemTemplate (Click to view full-size image)

We next need to populate the DropDownList with the available categories. Click on the Choose Data Source link from the DropDownList's smart tag and opt to create a new ObjectDataSource named CategoriesDataSource.

Figure 6: Create a New ObjectDataSource Control Named CategoriesDataSource (Click to view full-size image) To have this ObjectDataSource return all of the categories, bind it to the CategoriesBLL class's GetCategories()method.

Figure 7: Bind the ObjectDataSource to the CategoriesBLL's GetCategories() Method (Click to view full-size image) Finally, configure the DropDownList's settings such that the CategoryName field is displayed in each DropDownListListItem with the CategoryID field used as the value.

Figure 8: Have the CategoryName Field Displayed and the CategoryID Used as the Value (Click to view full-size image) After making these changes the declarative markup for the EditItemTemplate in the CategoryName TemplateField will include both a DropDownList and an ObjectDataSource: <asp:TemplateField HeaderText="Category" SortExpression="CategoryName"> <EditItemTemplate> <asp:DropDownList ID="Categories" runat="server" DataSourceID="CategoriesDataSource" DataTextField="CategoryName" DataValueField="CategoryID"> </asp:DropDownList> <asp:ObjectDataSource ID="CategoriesDataSource" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" TypeName="CategoriesBLL"> </asp:ObjectDataSource> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("CategoryName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> Note: The DropDownList in the EditItemTemplate must have its view state enabled. We will soon add databinding syntax to the DropDownList's declarative syntax and databinding commands like Eval() and Bind() can only appear in controls whose view state is enabled. Repeat these steps to add a DropDownList named Suppliers to the SupplierName TemplateField'sEditItemTemplate. This will involve adding a DropDownList to the EditItemTemplate and creating another ObjectDataSource. The Suppliers DropDownList's ObjectDataSource, however, should be configured to invoke theSuppliersBLL class's GetSuppliers() method. Additionally, configure the Suppliers DropDownList to display theCompanyName field and use the SupplierID field as the value for its ListItems.

After adding the DropDownLists to the two EditItemTemplates, load the page in a browser and click the Edit button for the Chef Anton's Cajun Seasoning product. As Figure 9 shows, the product's category and supplier columns are rendered as drop-down lists containing the available categories and suppliers to choose from. However, note that the first items in both drop-down lists are selected by default (Beverages for the category and Exotic Liquids as the supplier), even though Chef Anton's Cajun Seasoning is a Condiment supplied by New Orleans Cajun Delights.

Figure 9: The First Item in the Drop-Down Lists is Selected by Default (Click to view full-size image) Furthermore, if you click Update, you'll find that the product's CategoryID and SupplierID values are set to NULL. Both of these undesired behaviors are caused because the DropDownLists in the EditItemTemplates are not bound to any data fields from the underlying product data.

Binding the DropDownLists to the CategoryID and SupplierID Data Fields


In order to have the edited product's category and supplier drop-down lists set to the appropriate values and to have these values sent back to the BLL's UpdateProduct method upon clicking Update, we need to bind the DropDownLists' SelectedValue properties to the CategoryID and SupplierID data fields using two-way databinding. To accomplish this with the Categories DropDownList, you can add SelectedValue='<%# Bind("CategoryID") %>' directly to the declarative syntax. Alternatively, you can set the DropDownList's databindings by editing the template through the Designer and clicking the Edit DataBindings link from the DropDownList's smart tag. Next, indicate that the SelectedValueproperty should be bound to the CategoryID field using two-way databinding (see Figure 10). Repeat either the declarative or Designer process to bind the SupplierID data field to the Suppliers DropDownList.

Figure 10: Bind the CategoryID to the DropDownList's SelectedValue Property Using Two-Way Databinding (Click to view full-size image) Once the bindings have been applied to the SelectedValue properties of the two DropDownLists, the edited product's category and supplier columns will default to the current product's values. Upon clicking Update, theCategoryID and SupplierID values of the selected drop-down list item will be passed to the UpdateProductmethod. Figure 11 shows the tutorial after the databinding statements have been added; note how the selected drop-down list items for Chef Anton's Cajun Seasoning are correctly Condiment and New Orleans Cajun Delights.

Figure 11: The Edited Product's Current Category and Supplier Values are Selected by Default (Click to view full-size image)

Handling NULL Values


The CategoryID and SupplierID columns in the Products table can be NULL, yet the DropDownLists in theEditItemTemplates don't include a list item to represent a NULL value. This has two consequences: User cannot use our interface to change a product's category or supplier from a non-NULL value to a NULLone

If a product has a NULL CategoryID or SupplierID, clicking the Edit button will result in an exception. This is because the NULL value returned by CategoryID (or SupplierID) in the Bind() statement does not map to a value in the DropDownList (the DropDownList throws an exception when its SelectedValue property is set to a value not in its collection of list items). In order to support NULL CategoryID and SupplierID values, we need to add another ListItem to each DropDownList to represent the NULL value. In the Master/Detail Filtering With a DropDownList tutorial, we saw how to add an additional ListItem to a databound DropDownList, which involved setting the DropDownList'sAppendDataBoundItems property to True and manually adding the additional ListItem. In that previous tutorial, however, we added a ListItem with a Value of -1. The databinding logic in ASP.NET, however, will automatically convert a blank string to a NULL value and vice-a-versa. Therefore, for this tutorial we want the ListItem's Value to be an empty string. Start by setting both DropDownLists' AppendDataBoundItems property to True. Next, add the NULL ListItem by adding the following <asp:ListItem> element to each DropDownList so that the declarative markup looks like: <asp:DropDownList ID="Categories" runat="server" DataSourceID="CategoriesDataSource" DataTextField="CategoryName" DataValueField="CategoryID" SelectedValue='<%# Bind("CategoryID") %>' AppendDataBoundItems="True"> <asp:ListItem Value="">(None)</asp:ListItem> </asp:DropDownList> I've chosen to use "(None)" as the Text value for this ListItem, but you can change it to also be a blank string if you'd like. Note: As we saw in the Master/Detail Filtering With a DropDownList tutorial, ListItems can be added to a DropDownList through the Designer by clicking on the DropDownList's Items property in the Properties window (which will display the ListItem Collection Editor). However, be sure to add the NULL ListItem for this tutorial through the declarative syntax. If you use the ListItem Collection Editor, the generated declarative syntax will omit the Value setting altogether when assigned a blank string, creating declarative markup like: <asp:ListItem>(None)</asp:ListItem>. While this may look harmless, the missing Value causes the DropDownList to use theText property value in its place. That means that if this NULL ListItem is selected, the value "(None)" will be attempted to be assigned to the CategoryID, which will result in an exception. By explicitly setting Value="", a NULLvalue will be assigned to CategoryID when the NULL ListItem is selected. Repeat these steps for the Suppliers DropDownList. With this additional ListItem, the editing interface can now assign NULL values to a Product's CategoryID andSupplierID fields, as shown in Figure 12.

Figure 12: Choose (None) to Assign a NULL Value for a Product's Category or Supplier (Click to view full-size image)

Step 4: Using RadioButtons for the Discontinued Status


Currently the products' Discontinued data field is expressed using a CheckBoxField, which renders a disabled checkbox for the read-only rows and an enabled checkbox for the row being edited. While this user interface is often suitable, we can customize it if needed using a TemplateField. For this tutorial, let's change the CheckBoxField into a TemplateField that uses a RadioButtonList control with two options "Active" and "Discontinued" from which the user can specify the product's Discontinued value. Start by converting the Discontinued CheckBoxField into a TemplateField, which will create a TemplateField with anItemTemplate and EditItemTemplate. Both templates include a CheckBox with its Checked property bound to theDiscontinued data field, the only difference between the two being that the ItemTemplate's CheckBox's Enabledproperty is set to False. Replace the CheckBox in both the ItemTemplate and EditItemTemplate with a RadioButtonList control, setting both RadioButtonLists' ID properties to DiscontinuedChoice. Next, indicate that the RadioButtonLists should each contain two radio buttons, one labeled "Active" with a value of "False" and one labeled "Discontinued" with a value of "True". To accomplish this you can either enter the <asp:ListItem> elements in directly through the declarative syntax or use the ListItem Collection Editor from the Designer. Figure 13 shows the ListItem Collection Editor after the two radio button options have been specified.

Figure 13: Add Active and Discontinued Options to the RadioButtonList (Click to view full-size image) Since the RadioButtonList in the ItemTemplate shouldn't be editable, set its Enabled property to False, leaving theEnabled property to True (the default) for the RadioButtonList in the EditItemTemplate. This will make the radio buttons in the non-edited row as read-only, but will allow the user to change the RadioButton values for the edited row. We still need to assign the RadioButtonList controls' SelectedValue properties so that the appropriate radio button is selected based upon the product's Discontinued data field. As with the DropDownLists examined earlier in this tutorial, this databinding syntax can either be added directly into the declarative markup or through the Edit DataBindings link in the RadioButtonLists' smart tags. After adding the two RadioButtonLists and configuring them, the Discontinued TemplateField's declarative markup should look like: <asp:TemplateField HeaderText="Discontinued" SortExpression="Discontinued"> <ItemTemplate> <asp:RadioButtonList ID="DiscontinuedChoice" runat="server" Enabled="False" SelectedValue='<%# Bind("Discontinued") %>'> <asp:ListItem Value="False">Active</asp:ListItem> <asp:ListItem Value="True">Discontinued</asp:ListItem> </asp:RadioButtonList> </ItemTemplate> <EditItemTemplate> <asp:RadioButtonList ID="DiscontinuedChoice" runat="server" SelectedValue='<%# Bind("Discontinued") %>'> <asp:ListItem Value="False">Active</asp:ListItem> <asp:ListItem Value="True">Discontinued</asp:ListItem> </asp:RadioButtonList> </EditItemTemplate> </asp:TemplateField> With these changes, the Discontinued column has been transformed from a list of checkboxes to a list of radio button pairs (see Figure 14). When editing a product, the appropriate radio button is selected and the product's discontinued status can be updated by selecting the other radio button and clicking Update.

Figure 14: The Discontinued CheckBoxes Have Been Replaced by Radio Button Pairs (Click to view full-size image) Note: Since the Discontinued column in the Products database cannot have NULL values, we do not need to worry about capturing NULL information in the interface. If, however, Discontinued column could contain NULLvalues we'd want to add a third radio button to the list with its Value set to an empty string (Value=""), just like with the category and supplier DropDownLists.

Summary
While the BoundField and CheckBoxField automatically render read-only, editing, and inserting interfaces, they lack the ability for customization. Often, though, we'll need to customize the editing or inserting interface, perhaps adding validation controls (as we saw in the preceding tutorial) or by customizing the data collection user interface (as we saw in this tutorial). Customizing the interface with a TemplateField can be summed up in the following steps: 1. 2. 3. Add a TemplateField or convert an existing BoundField or CheckBoxField into a TemplateField Augment the interface as needed Bind the appropriate data fields to the newly added Web controls using two-way databinding

In addition to using the built-in ASP.NET Web controls, you can also customize the templates of a TemplateField with custom, compiled server controls and User Controls. Happy Programming!

Implementing Optimistic Concurrency (VB)


For a web application that allows multiple users to edit data, there is the risk that two users may be editing the same data at the same time. In this tutorial we'll implement optimistic concurrency control to handle this risk.

Introduction

For web applications that only allow users to view data, or for those that include only a single user who can modify data, there's no threat of two concurrent users accidentally overwriting one another's changes. For web applications that allow multiple users to update or delete data, however, there's the potential for one user's modifications to clash with another concurrent user's. Without any concurrency policy in place, when two users are simultaneously editing a single record, the user who commits her changes last will override the changes made by the first. For example, imagine that two users, Jisun and Sam, were both visiting a page in our application that allowed visitors to update and delete the products through a GridView control. Both click the Edit button in the GridView around the same time. Jisun changes the product name to "Chai Tea" and clicks the Update button. The net result is an UPDATE statement that is sent to the database, which sets all of the product's updateable fields (even though Jisun only updated one field, ProductName). At this point in time, the database has the values "Chai Tea," the category Beverages, the supplier Exotic Liquids, and so on for this particular product. However, the GridView on Sam's screen still shows the product name in the editable GridView row as "Chai". A few seconds after Jisun's changes have been committed, Sam updates the category to Condiments and clicks Update. This results in anUPDATE statement sent to the database that sets the product name to "Chai," the CategoryID to the corresponding Beverages category ID, and so on. Jisun's changes to the product name have been overwritten. Figure 1 graphically depicts this series of events.

Figure 1: When Two Users Simultaneously Update a Record There s Potential for One User 's Changes to Overwrite the Other 's (Click to view full-size image) Similarly, when two users are visiting a page, one user might be in the midst of updating a record when it is deleted by another user. Or, between when a user loads a page and when they click the Delete button, another user may have modified the contents of that record. There are three concurrency control strategies available: Do Nothing -if concurrent users are modifying the same record, let the last commit win (the default behavior) Optimistic Concurrency - assume that while there may be concurrency conflicts every now and then, the vast majority of the time such conflicts won't arise; therefore, if a conflict does arise, simply inform the user that their changes can't be saved because another user has modified the same data Pessimistic Concurrency - assume that concurrency conflicts are commonplace and that users won't tolerate being told their changes weren't saved due to another user's concurrent activity; therefore, when one user starts updating a record, lock it, thereby preventing any other users from editing or deleting that record until the user commits their modifications

All of our tutorials thus far have used the default concurrency resolution strategy - namely, we've let the last write win. In this tutorial we'll examine how to implement optimistic concurrency control. Note: We won't look at pessimistic concurrency examples in this tutorial series. Pessimistic concurrency is rarely used because such locks, if not properly relinquished, can prevent other users from updating data. For example, if a user locks a record for editing and then leaves for the day before unlocking it, no other user will be able to update that record until the original user returns and completes his update. Therefore, in situations where pessimistic concurrency is used, there's typically a timeout that, if reached, cancels the lock. Ticket sales websites, which lock a particular seating location for short period while the user completes the order process, is an example of pessimistic concurrency control.

Step 1: Looking at How Optimistic Concurrency is Implemented


Optimistic concurrency control works by ensuring that the record being updated or deleted has the same values as it did when the updating or deleting process started. For example, when clicking the Edit button in an editable GridView, the record's values are read from the database and displayed in TextBoxes and other Web controls. These original values are saved by the GridView. Later, after the user makes her changes and clicks the Update button, the original values plus the new values are sent to the Business Logic Layer, and then down to the Data Access Layer. The Data Access Layer must issue a SQL statement that will only update the record if the original values that the user started editing are identical to the values still in the database. Figure 2 depicts this sequence of events.

Figure 2: For the Update or Delete to Succeed, the Original Values Must Be Equal to the Current Database Values(Click to view full-size image) There are various approaches to implementing optimistic concurrency (see Peter A. Bromberg's Optmistic Concurrency Updating Logic for a brief look at a number of options). The ADO.NET Typed DataSet provides one implementation that can be configured with just the tick of a checkbox. Enabling optimistic concurrency for a TableAdapter in the Typed DataSet augments the TableAdapter's UPDATE and DELETE statements to include a comparison of all of the original values in the WHERE clause. The following UPDATE statement, for example, updates the name and price of a product only if the current database values are equal to the values that were originally retrieved when updating the record in the GridView. The @ProductName and @UnitPrice parameters contain the new values entered by the user, whereas @original_ProductName and @original_UnitPrice contain the values that were originally loaded into the GridView when the Edit button was clicked: UPDATE Products SET ProductName = @ProductName, UnitPrice = @UnitPrice

WHERE ProductID = @original_ProductID AND ProductName = @original_ProductName AND UnitPrice = @original_UnitPrice Note: This UPDATE statement has been simplified for readability. In practice, the UnitPrice check in the WHEREclause would be more involved since UnitPrice can contain NULLs and checking if NULL = NULL always returns False (instead you must use IS NULL). In addition to using a different underlying UPDATE statement, configuring a TableAdapter to use optimistic concurrency also modifies the signature of its DB direct methods. Recall from our first tutorial, Creating a Data Access Layer, that DB direct methods were those that accepts a list of scalar values as input parameters (rather than a strongly-typed DataRow or DataTable instance). When using optimistic concurrency, the DB direct Update() andDelete() methods include input parameters for the original values as well. Moreover, the code in the BLL for using the batch update pattern (the Update() method overloads that accept DataRows and DataTables rather than scalar values) must be changed as well. Rather than extend our existing DAL's TableAdapters to use optimistic concurrency (which would necessitate changing the BLL to accommodate), let's instead create a new Typed DataSet namedNorthwindOptimisticConcurrency, to which we'll add a Products TableAdapter that uses optimistic concurrency. Following that, we'll create a ProductsOptimisticConcurrencyBLL Business Logic Layer class that has the appropriate modifications to support the optimistic concurrency DAL. Once this groundwork has been laid, we'll be ready to create the ASP.NET page.

Step 2: Creating a Data Access Layer That Supports Optimistic Concurrency


To create a new Typed DataSet, right-click on the DAL folder within the App_Code folder and add a new DataSet named NorthwindOptimisticConcurrency. As we saw in the first tutorial, doing so will add a new TableAdapter to the Typed DataSet, automatically launching the TableAdapter Configuration Wizard. In the first screen, we're prompted to specify the database to connect to - connect to the same Northwind database using theNORTHWNDConnectionString setting from Web.config.

Figure 3: Connect to the Same Northwind Database (Click to view full-size image)

Next, we are prompted as to how to query the data: through an ad-hoc SQL statement, a new stored procedure, or an existing stored procedure. Since we used ad-hoc SQL queries in our original DAL, use this option here as well.

Figure 4: Specify the Data to Retrieve Using an Ad-Hoc SQL Statement (Click to view full-size image) On the following screen, enter the SQL query to use to retrieve the product information. Let's use the exact same SQL query used for the Products TableAdapter from our original DAL, which returns all of the Product columns along with the product's supplier and category names: 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 5: Use the Same SQL Query from the Products TableAdapter in the Original DAL (Click to view full-size image) Before moving onto the next screen, click the Advanced Options button. To have this TableAdapter employ optimistic concurrency control, simply check the "Use optimistic concurrency" checkbox.

Figure 6: Enable Optimistic Concurrency Control by Checking the "Use optimistic concurrency" CheckBox (Click to view full-size image) Lastly, indicate that the TableAdapter should use the data access patterns that both fill a DataTable and return a DataTable; also indicate that the DB direct methods should be created. Change the method name for the Return a DataTable pattern from GetData to GetProducts, so as to mirror the naming conventions we used in our original DAL.

Figure 7: Have the TableAdapter Utilize All Data Access Patterns (Click to view full-size image) After completing the wizard, the DataSet Designer will include a strongly-typed Products DataTable and TableAdapter. Take a moment to rename the DataTable from Products to ProductsOptimisticConcurrency, which you can do by right-clicking on the DataTable's title bar and choosing Rename from the context menu.

Figure 8: A DataTable and TableAdapter Have Been Added to the Typed DataSet (Click to view full-size image) To see the differences between the UPDATE and DELETE queries between the ProductsOptimisticConcurrencyTableAdapter (which uses optimistic concurrency) and the Products TableAdapter (which doesn't), click on the TableAdapter and go to the Properties window. In the DeleteCommand and UpdateCommand properties'CommandText subproperties you can see the actual SQL syntax that

is sent to the database when the DAL's update or delete-related methods are invoked. For the ProductsOptimisticConcurrency TableAdapter the DELETEstatement used is: DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID) AND ([ProductName] = @Original_ProductName) AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL) OR ([SupplierID] = @Original_SupplierID)) AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL) OR ([CategoryID] = @Original_CategoryID)) AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL) OR ([QuantityPerUnit] = @Original_QuantityPerUnit)) AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL) OR ([UnitPrice] = @Original_UnitPrice)) AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL) OR ([UnitsInStock] = @Original_UnitsInStock)) AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL) OR ([UnitsOnOrder] = @Original_UnitsOnOrder)) AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL) OR ([ReorderLevel] = @Original_ReorderLevel)) AND ([Discontinued] = @Original_Discontinued)) Whereas the DELETE statement for the Product TableAdapter in our original DAL is the much simpler: DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID)) As you can see, the WHERE clause in the DELETE statement for the TableAdapter that uses optimistic concurrency includes a comparison between each of the Product table's existing column values and the original values at the time the GridView (or DetailsView or FormView) was last populated. Since all fields other than ProductID,ProductName, and Discontinued can have NULL values, additional parameters and checks are included to correctly compare NULL values in the WHERE clause. We won't be adding any additional DataTables to the optimistic concurrency-enabled DataSet for this tutorial, as our ASP.NET page will only provide updating and deleting product information. However, we do still need to add the GetProductByProductID(productID) method to the ProductsOptimisticConcurrency TableAdapter. To accomplish this, right-click on the TableAdapter's title bar (the area right above the Fill and GetProductsmethod names) and choose Add Query from the context menu. This will launch the TableAdapter Query Configuration Wizard. As with our TableAdapter's initial configuration, opt to create theGetProductByProductID(productID) method using an ad-hoc SQL statement (see Figure 4). Since theGetProductByProductID(productID) method returns information about a particular product, indicate that this query is a SELECT query type that returns rows.

Figure 9: Mark the Query Type as a "SELECT which returns rows" (Click to view full-size image) On the next screen we're prompted for the SQL query to use, with the TableAdapter's default query pre-loaded. Augment the existing query to include the clause WHERE ProductID = @ProductID, as shown in Figure 10.

Figure 10: Add a WHERE Clause to the Pre-Loaded Query to Return a Specific Product Record (Click to view full-size image) Finally, change the generated method names to FillByProductID and GetProductByProductID.

Figure 11: Rename the Methods to FillByProductID and GetProductByProductID (Click to view full-size image) With this wizard complete, the TableAdapter now contains two methods for retrieving data: GetProducts(), which returns all products; and GetProductByProductID(productID), which returns the specified product.

Step 3: Creating a Business Logic Layer for the Optimistic ConcurrencyEnabled DAL
Our existing ProductsBLL class has examples of using both the batch update and DB direct patterns. TheAddProduct method and UpdateProduct overloads both use the batch update pattern, passing in a ProductRowinstance to the TableAdapter's Update method. The DeleteProduct method, on the other hand, uses the DB direct pattern, calling the TableAdapter's Delete(productID) method. With the new ProductsOptimisticConcurrency TableAdapter, the DB direct methods now require that the original values also be passed in. For example, the Delete method now expects ten input parameters: the originalProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnO rder,ReorderLevel, and Discontinued. It uses these additional input parameters' values in WHERE clause of the DELETEstatement sent to the database, only deleting the specified record if the database's current values map up to the original ones. While the method signature for the TableAdapter's Update method used in the batch update pattern hasn't changed, the code needed to record the original and new values has. Therefore, rather than attempt to use the optimistic concurrency-enabled DAL with our existing ProductsBLL class, let's create a new Business Logic Layer class for working with our new DAL. Add a class named ProductsOptimisticConcurrencyBLL to the BLL folder within the App_Code folder.

Figure 12: Add the ProductsOptimisticConcurrencyBLL Class to the BLL Folder Next, add the following code to the ProductsOptimisticConcurrencyBLL class: Imports NorthwindOptimisticConcurrencyTableAdapters <System.ComponentModel.DataObject()> _ Public Class ProductsOptimisticConcurrencyBLL Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter Get If _productsAdapter Is Nothing Then _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter() End If Return _productsAdapter End Get End Property <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Select, True)> _ Public Function GetProducts() As _ NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable Return Adapter.GetProducts() End Function End Class Note the using NorthwindOptimisticConcurrencyTableAdapters statement above the start of the class declaration. The NorthwindOptimisticConcurrencyTableAdapters namespace contains theProductsOptimisticConcurrencyTableAdapter class, which provides the DAL's methods. Also before the class

declaration you'll find the System.ComponentModel.DataObject attribute, which instructs Visual Studio to include this class in the ObjectDataSource wizard's drop-down list. The ProductsOptimisticConcurrencyBLL's Adapter property provides quick access to an instance of theProductsOptimisticConcurrencyTableAdapter class, and follows the pattern used in our original BLL classes (ProductsBLL, CategoriesBLL, and so on). Finally, the GetProducts() method simply calls down into the DAL'sGetProducts() method and returns a ProductsOptimisticConcurrencyDataTable object populated with aProductsOptimisticConcurrencyRow instance for each product record in the database.

Deleting a Product Using the DB Direct Pattern with Optimistic Concurrency


When using the DB direct pattern against a DAL that uses optimistic concurrency, the methods must be passed the new and original values. For deleting, there are no new values, so only the original values need be passed in. In our BLL, then, we must accept all of the original parameters as input parameters. Let's have the DeleteProduct method in the ProductsOptimisticConcurrencyBLL class use the DB direct method. This means that this method needs to take in all ten product data fields as input parameters, and pass these to the DAL, as shown in the following code: <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Delete, True)> _ Public Function DeleteProduct( _ ByVal original_productID As Integer, ByVal original_productName As String, _ ByVal original_supplierID As Nullable(Of Integer), _ ByVal original_categoryID As Nullable(Of Integer), _ ByVal original_quantityPerUnit As String, _ ByVal original_unitPrice As Nullable(Of Decimal), _ ByVal original_unitsInStock As Nullable(Of Short), _ ByVal original_unitsOnOrder As Nullable(Of Short), _ ByVal original_reorderLevel As Nullable(Of Short), _ ByVal original_discontinued As Boolean) _ As Boolean Dim rowsAffected As Integer = Adapter.Delete( original_productID, _ original_productName, _ original_supplierID, _ original_categoryID, _ original_quantityPerUnit, _ original_unitPrice, _ original_unitsInStock, _ original_unitsOnOrder, _ original_reorderLevel, _ original_discontinued) ' Return true if precisely one row was deleted, otherwise false Return rowsAffected = 1 End Function If the original values - those values that were last loaded into the GridView (or DetailsView or FormView) - differ from the values in the database when the user clicks the Delete button the WHERE clause won't match up with any database record and no records will be affected. Hence, the TableAdapter's Delete method will return 0 and the BLL's DeleteProduct method will return false.

Updating a Product Using the Batch Update Pattern with Optimistic Concurrency

As noted earlier, the TableAdapter's Update method for the batch update pattern has the same method signature regardless of whether or not optimistic concurrency is employed. Namely, the Update method expects a DataRow, an array of DataRows, a DataTable, or a Typed DataSet. There are no additional input parameters for specifying the original values. This is possible because the DataTable keeps track of the original and modified values for its DataRow(s). When the DAL issues its UPDATE statement, the @original_ColumnName parameters are populated with the DataRow's original values, whereas the @ColumnName parameters are populated with the DataRow's modified values. In the ProductsBLL class (which uses our original, non-optimistic concurrency DAL), when using the batch update pattern to update product information our code performs the following sequence of events: 1. Read the current database product information into a ProductRow instance using the TableAdapter'sGetProductByProductID(productID) method 2. Assign the new values to the ProductRow instance from Step 1 3. Call the TableAdapter's Update method, passing in the ProductRow instance This sequence of steps, however, won't correctly support optimistic concurrency because the ProductRowpopulated in Step 1 is populated directly from the database, meaning that the original values used by the DataRow are those that currently exist in the database, and not those that were bound to the GridView at the start of the editing process. Instead, when using an optimistic concurrency-enabled DAL, we need to alter the UpdateProductmethod overloads to use the following steps: 1. Read the current database product information into a ProductsOptimisticConcurrencyRow instance using the TableAdapter's GetProductByProductID(productID) method 2. Assign the original values to the ProductsOptimisticConcurrencyRow instance from Step 1 3. Call the ProductsOptimisticConcurrencyRow instance's AcceptChanges() method, which instructs the DataRow that its current values are the "original" ones 4. Assign the new values to the ProductsOptimisticConcurrencyRow instance 5. Call the TableAdapter's Update method, passing in the ProductsOptimisticConcurrencyRow instance Step 1 reads in all of the current database values for the specified product record. This step is superfluous in theUpdateProduct overload that updates all of the product columns (as these values are overwritten in Step 2), but is essential for those overloads where only a subset of the column values are passed in as input parameters. Once the original values have been assigned to the ProductsOptimisticConcurrencyRow instance, theAcceptChanges() method is called, which marks the current DataRow values as the original values to be used in the @original_ColumnName parameters in the UPDATE statement. Next, the new parameter values are assigned to the ProductsOptimisticConcurrencyRow and, finally, the Update method is invoked, passing in the DataRow. The following code shows the UpdateProduct overload that accepts all product data fields as input parameters. While not shown here, the ProductsOptimisticConcurrencyBLL class included in the download for this tutorial also contains an UpdateProduct overload that accepts just the product's name and price as input parameters. Protected Sub AssignAllProductValues( _ ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _ ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _ ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _ ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _ ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _ ByVal discontinued As Boolean) product.ProductName = productName If Not supplierID.HasValue Then product.SetSupplierIDNull() Else product.SupplierID = supplierID.Value End If If Not categoryID.HasValue Then product.SetCategoryIDNull()

Else product.CategoryID = categoryID.Value End If If quantityPerUnit Is Nothing Then product.SetQuantityPerUnitNull() Else product.QuantityPerUnit = quantityPerUnit End If If Not unitPrice.HasValue Then product.SetUnitPriceNull() Else product.UnitPrice = unitPrice.Value End If If Not unitsInStock.HasValue Then product.SetUnitsInStockNull() Else product.UnitsInStock = unitsInStock.Value End If If Not unitsOnOrder.HasValue Then product.SetUnitsOnOrderNull() Else product.UnitsOnOrder = unitsOnOrder.Value End If If Not reorderLevel.HasValue Then product.SetReorderLevelNull() Else product.ReorderLevel = reorderLevel.Value End If product.Discontinued = discontinued End Sub <System.ComponentModel.DataObjectMethodAttribute( _ System.ComponentModel.DataObjectMethodType.Update, True)> _ Public Function UpdateProduct( ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _ ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _ ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _ ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _ ByVal discontinued As Boolean, ByVal productID As Integer, _ _ ByVal original_productName As String, _ ByVal original_supplierID As Nullable(Of Integer), _ ByVal original_categoryID As Nullable(Of Integer), _ ByVal original_quantityPerUnit As String, _ ByVal original_unitPrice As Nullable(Of Decimal), _ ByVal original_unitsInStock As Nullable(Of Short), _ ByVal original_unitsOnOrder As Nullable(Of Short), _ ByVal original_reorderLevel As Nullable(Of Short), _ ByVal original_discontinued As Boolean, _ ByVal original_productID As Integer) _

As Boolean 'STEP 1: Read in the current database product information Dim products As _ NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _ Adapter.GetProductByProductID(original_productID) If products.Count = 0 Then ' no matching record found, return false Return False End If Dim product As _ NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0) 'STEP 2: Assign the original values to the product instance AssignAllProductValues( _ product, original_productName, original_supplierID, _ original_categoryID, original_quantityPerUnit, original_unitPrice, _ original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _ original_discontinued) 'STEP 3: Accept the changes product.AcceptChanges() 'STEP 4: Assign the new values to the product instance AssignAllProductValues( _ product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _ unitsInStock, unitsOnOrder, reorderLevel, discontinued) 'STEP 5: Update the product record Dim rowsAffected As Integer = Adapter.Update(product) ' Return true if precisely one row was updated, otherwise false Return rowsAffected = 1 End Function

Step 4: Passing the Original and New Values From the ASP.NET Page to the BLL Methods
With the DAL and BLL complete, all that remains is to create an ASP.NET page that can utilize the optimistic concurrency logic built in to the system. Specifically, the data Web control (the GridView, DetailsView, or FormView) must remember its original values and the ObjectDataSource must pass both sets of values to the Business Logic Layer. Furthermore, the ASP.NET page must be configured to gracefully handle concurrency violations. Start by opening the OptimisticConcurrency.aspx page in the EditInsertDelete folder and adding a GridView to the Designer, setting its ID property to ProductsGrid. From the GridView's smart tag, opt to create a new ObjectDataSource named ProductsOptimisticConcurrencyDataSource. Since we want this ObjectDataSource to use the DAL that supports optimistic concurrency, configure it to use the ProductsOptimisticConcurrencyBLLobject.

Figure 13: Have the ObjectDataSource Use the ProductsOptimisticConcurrencyBLL Object (Click to view full-size image) Choose the GetProducts, UpdateProduct, and DeleteProduct methods from drop-down lists in the wizard. For the UpdateProduct method, use the overload that accepts all of the product's data fields.

Configuring the ObjectDataSource Control's Properties


After completing the wizard, the ObjectDataSource's declarative markup should look like the following: <asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server" DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL" UpdateMethod="UpdateProduct"> <DeleteParameters> <asp:Parameter Name="original_productID" Type="Int32" /> <asp:Parameter Name="original_productName" Type="String" /> <asp:Parameter Name="original_supplierID" Type="Int32" /> <asp:Parameter Name="original_categoryID" Type="Int32" /> <asp:Parameter Name="original_quantityPerUnit" Type="String" /> <asp:Parameter Name="original_unitPrice" Type="Decimal" /> <asp:Parameter Name="original_unitsInStock" Type="Int16" /> <asp:Parameter Name="original_unitsOnOrder" Type="Int16" /> <asp:Parameter Name="original_reorderLevel" Type="Int16" /> <asp:Parameter Name="original_discontinued" Type="Boolean" /> </DeleteParameters> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" />

<asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" /> <asp:Parameter Name="discontinued" Type="Boolean" /> <asp:Parameter Name="productID" Type="Int32" /> <asp:Parameter Name="original_productName" Type="String" /> <asp:Parameter Name="original_supplierID" Type="Int32" /> <asp:Parameter Name="original_categoryID" Type="Int32" /> <asp:Parameter Name="original_quantityPerUnit" Type="String" /> <asp:Parameter Name="original_unitPrice" Type="Decimal" /> <asp:Parameter Name="original_unitsInStock" Type="Int16" /> <asp:Parameter Name="original_unitsOnOrder" Type="Int16" /> <asp:Parameter Name="original_reorderLevel" Type="Int16" /> <asp:Parameter Name="original_discontinued" Type="Boolean" /> <asp:Parameter Name="original_productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource> As you can see, the DeleteParameters collection contains a Parameter instance for each of the ten input parameters in the ProductsOptimisticConcurrencyBLL class's DeleteProduct method. Likewise, theUpdateParameters collection contains a Parameter instance for each of the input parameters in UpdateProduct. For those previous tutorials that involved data modification, we'd remove the ObjectDataSource'sOldValuesParameterFormatString property at this point, since this property indicates that the BLL method expects the old (or original) values to be passed in as well as the new values. Furthermore, this property value indicates the input parameter names for the original values. Since we are passing in the original values into the BLL, do not remove this property. Note: The value of the OldValuesParameterFormatString property must map to the input parameter names in the BLL that expect the original values. Since we named these parameters original_productName,original_supplierID, and so on, you can leave the OldValuesParameterFormatString property value asoriginal_{0}. If, however, the BLL methods' input parameters had names like old_productName, old_supplierID, and so on, you'd need to update the OldValuesParameterFormatString property to old_{0}. There's one final property setting that needs to be made in order for the ObjectDataSource to correctly pass the original values to the BLL methods. The ObjectDataSource has a ConflictDetection property that can be assigned toone of two values: OverwriteChanges - the default value; does not send the original values to the BLL methods' original input parameters CompareAllValues - does send the original values to the BLL methods; choose this option when using optimistic concurrency Take a moment to set the ConflictDetection property to CompareAllValues.

Configuring the GridView's Properties and Fields


With the ObjectDataSource's properties properly configured, let's turn our attention to setting up the GridView. First, since we want the GridView to support editing and deleting, click the Enable Editing and Enable Deleting checkboxes from the GridView's smart tag. This will add a CommandField whose ShowEditButton andShowDeleteButton are both set to true. When bound to the ProductsOptimisticConcurrencyDataSource ObjectDataSource, the GridView contains a field for each of the product's data fields. While such a GridView can be edited, the user experience is anything but acceptable. The CategoryID and SupplierID BoundFields will render as TextBoxes, requiring the user to enter the

appropriate category and supplier as ID numbers. There will be no formatting for the numeric fields and no validation controls to ensure that the product's name has been supplied and that the unit price, units in stock, units on order, and reorder level values are both proper numeric values and are greater than or equal to zero. As we discussed in the Adding Validation Controls to the Editing and Inserting Interfaces and Customizing the Data Modification Interface tutorials, the user interface can be customized by replacing the BoundFields with TemplateFields. I've modified this GridView and its editing interface in the following ways: Removed the ProductID, SupplierName, and CategoryName BoundFields Converted the ProductName BoundField to a TemplateField and added a RequiredFieldValidation control. Converted the CategoryID and SupplierID BoundFields to TemplateFields, and adjusted the editing interface to use DropDownLists rather than TextBoxes. In these TemplateFields' ItemTemplates, the CategoryName andSupplierName data fields are displayed. Converted the UnitPrice, UnitsInStock, UnitsOnOrder, and ReorderLevel BoundFields to TemplateFields and added CompareValidator controls. Since we've already examined how to accomplish these tasks in previous tutorials, I'll just list the final declarative syntax here and leave the implementation as practice. <asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource" OnRowUpdated="ProductsGrid_RowUpdated"> <Columns> <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" /> <asp:TemplateField HeaderText="Product" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="EditProductName" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="EditProductName" ErrorMessage="You must enter a product name." runat="server">*</asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Category" SortExpression="CategoryName"> <EditItemTemplate> <asp:DropDownList ID="EditCategoryID" runat="server" DataSourceID="CategoriesDataSource" AppendDataBoundItems="true" DataTextField="CategoryName" DataValueField="CategoryID" SelectedValue='<%# Bind("CategoryID") %>'> <asp:ListItem Value=">(None)</asp:ListItem> </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" TypeName="CategoriesBLL"> </asp:ObjectDataSource> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server"

Text='<%# Bind("CategoryName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName"> <EditItemTemplate> <asp:DropDownList ID="EditSuppliersID" runat="server" DataSourceID="SuppliersDataSource" AppendDataBoundItems="true" DataTextField="CompanyName" DataValueField="SupplierID" SelectedValue='<%# Bind("SupplierID") %>'> <asp:ListItem Value=">(None)</asp:ListItem> </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetSuppliers" TypeName="SuppliersBLL"> </asp:ObjectDataSource> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label3" runat="server" Text='<%# Bind("SupplierName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" SortExpression="QuantityPerUnit" /> <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice"> <EditItemTemplate> <asp:TextBox ID="EditUnitPrice" runat="server" Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" /> <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="EditUnitPrice" ErrorMessage="Unit price must be a valid currency value without the currency symbol and must have a value greater than or equal to zero." Operator="GreaterThanEqual" Type="Currency" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label4" runat="server" Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock"> <EditItemTemplate> <asp:TextBox ID="EditUnitsInStock" runat="server" Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox> <asp:CompareValidator ID="CompareValidator2" runat="server" ControlToValidate="EditUnitsInStock" ErrorMessage="Units in stock must be a valid number greater than or equal to zero." Operator="GreaterThanEqual" Type="Integer" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate>

<ItemTemplate> <asp:Label ID="Label5" runat="server" Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder"> <EditItemTemplate> <asp:TextBox ID="EditUnitsOnOrder" runat="server" Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox> <asp:CompareValidator ID="CompareValidator3" runat="server" ControlToValidate="EditUnitsOnOrder" ErrorMessage="Units on order must be a valid numeric value greater than or equal to zero." Operator="GreaterThanEqual" Type="Integer" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label6" runat="server" Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel"> <EditItemTemplate> <asp:TextBox ID="EditReorderLevel" runat="server" Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox> <asp:CompareValidator ID="CompareValidator4" runat="server" ControlToValidate="EditReorderLevel" ErrorMessage="Reorder level must be a valid numeric value greater than or equal to zero." Operator="GreaterThanEqual" Type="Integer" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label7" runat="server" Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> </Columns> </asp:GridView> We're very close to having a fully-working example. However, there are a few subtleties that will creep up and cause us problems. Additionally, we still need some interface that alerts the user when a concurrency violation has occurred. Note: In order for a data Web control to correctly pass the original values to the ObjectDataSource (which are then passed to the BLL), it's vital that the GridView's EnableViewState property is set to true (the default). If you disable view state, the original values are lost on postback.

Passing the Correct Original Values to the ObjectDataSource

There are a couple of problems with the way the GridView has been configured. If the ObjectDataSource'sConflictDetection property is set to CompareAllValues (as is ours), when the ObjectDataSource's Update() orDelete() methods are invoked by the GridView (or DetailsView or FormView), the ObjectDataSource attempts to copy the GridView's original values into its appropriate Parameter instances. Refer back to Figure 2 for a graphical representation of this process. Specifically, the GridView's original values are assigned the values in the two-way databinding statements each time the data is bound to the GridView. Therefore, it's essential that the required original values all are captured via two-way databinding and that they are provided in a convertible format. To see why this is important, take a moment to visit our page in a browser. As expected, the GridView lists each product with an Edit and Delete button in the leftmost column.

Figure 14: The Products are Listed in a GridView (Click to view full-size image) If you click the Delete button for any product, a FormatException is thrown.

Figure 15: Attempting to Delete Any Product Results in a FormatException (Click to view full-size image) The FormatException is raised when the ObjectDataSource attempts to read in the original UnitPrice value. Since the ItemTemplate has the UnitPrice formatted as a currency (<%# Bind("UnitPrice", "{0:C}") %>), it includes a currency symbol, like $19.95. The FormatException occurs as the ObjectDataSource attempts to convert this string into a decimal. To circumvent this problem, we have a number of options: Remove the currency formatting from the ItemTemplate. That is, instead of using <%# Bind("UnitPrice", "{0:C}") %>, simply use <%# Bind("UnitPrice") %>. The downside of this is that the price is no longer formatted. Display the UnitPrice formatted as a currency in the ItemTemplate, but use the Eval keyword to accomplish this. Recall that Eval performs one-way databinding. We still need to provide the UnitPrice value for the original values, so we'll still need a two-way databinding statement in the ItemTemplate, but this can be placed in a Label Web control whose Visible property is set to false. We could use the following markup in the ItemTemplate: <ItemTemplate> <asp:Label ID="DummyUnitPrice" runat="server" Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label> <asp:Label ID="Label4" runat="server" Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label> </ItemTemplate> Remove the currency formatting from the ItemTemplate, using <%# Bind("UnitPrice") %>. In the GridView'sRowDataBound event handler, programmatically access the Label Web control within which the UnitPricevalue is displayed and set its Text property to the formatted version. Leave the UnitPrice formatted as a currency. In the GridView's RowDeleting event handler, replace the existing original UnitPrice value ($19.95) with an actual decimal value using Decimal.Parse. We saw how to accomplish something similar in the RowUpdating event handler in the Handling BLL- and DAL-Level Exceptions in an ASP.NET Page tutorial. For my example I chose to go with the second approach, adding a hidden Label Web control whose Text property is two-way data bound to the unformatted UnitPrice value.

After solving this problem, try clicking the Delete button for any product again. This time you'll get anInvalidOperationException when the ObjectDataSource attempts to invoke the BLL's UpdateProduct method.

Figure 16: The ObjectDataSource Cannot Find a Method with the Input Parameters it Wants to Send (Click to view fullsize image) Looking at the exception's message, it's clear that the ObjectDataSource wants to invoke a BLL DeleteProductmethod that includes original_CategoryName and original_SupplierName input parameters. This is because theItemTemplates for the CategoryID and SupplierID TemplateFields currently contain two-way Bind statements with the CategoryName and SupplierName data fields. Instead, we need to include Bind statements with the CategoryIDand SupplierID data fields. To accomplish this, replace the existing Bind statements with Eval statements, and then add hidden Label controls whose Text properties are bound to the CategoryID and SupplierID data fields using two-way databinding, as shown below: <asp:TemplateField HeaderText="Category" SortExpression="CategoryName"> <EditItemTemplate> ... </EditItemTemplate> <ItemTemplate> <asp:Label ID="DummyCategoryID" runat="server" Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label> <asp:Label ID="Label2" runat="server" Text='<%# Eval("CategoryName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName"> <EditItemTemplate> ... </EditItemTemplate> <ItemTemplate> <asp:Label ID="DummySupplierID" runat="server" Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label> <asp:Label ID="Label3" runat="server"

Text='<%# Eval("SupplierName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> With these changes, we are now able to successfully delete and edit product information! In Step 5 we'll look at how to verify that concurrency violations are being detected. But for now, take a few minutes to try updating and deleting a few records to ensure that updating and deleting for a single user works as expected.

Step 5: Testing the Optimistic Concurrency Support


In order to verify that concurrency violations are being detected (rather than resulting in data being blindly overwritten), we need to open two browser windows to this page. In both browser instances, click on the Edit button for Chai. Then, in just one of the browsers, change the name to "Chai Tea" and click Update. The update should succeed and return the GridView to its pre-editing state, with "Chai Tea" as the new product name. In the other browser window instance, however, the product name TextBox still shows "Chai". In this second browser window, update the UnitPrice to 25.00. Without optimistic concurrency support, clicking update in the second browser instance would change the product name back to "Chai", thereby overwriting the changes made by the first browser instance. With optimistic concurrency employed, however, clicking the Update button in the second browser instance results in a DBConcurrencyException.

Figure 17: When a Concurrency Violation is Detected, a DBConcurrencyException is Thrown (Click to view full-size image) The DBConcurrencyException is only thrown when the DAL's batch update pattern is utilized. The DB direct pattern does not raise an exception, it merely indicates that no rows were affected. To illustrate this, return both browser instances' GridView to their pre-editing state. Next, in the first browser instance, click the Edit button and change the product name from "Chai Tea" back to "Chai" and click Update. In the second browser window, click the Delete button for Chai. Upon clicking Delete, the page posts back, the GridView invokes the ObjectDataSource's Delete() method, and the ObjectDataSource calls down into the ProductsOptimisticConcurrencyBLL class's DeleteProduct method, passing along the original values. The original ProductName value for the second browser instance is "Chai Tea", which doesn't match up with the current ProductName value in the database. Therefore the DELETE statement issued to the database affects zero rows since there's no record in the database that the WHERE clause satisfies. TheDeleteProduct method returns false and the ObjectDataSource's data is rebound to the GridView. From the end user's perspective, clicking on the Delete button for Chai Tea in the second browser window caused the screen to flash and, upon coming back, the product is still there, although now it's listed as "Chai" (the product name

change made by the first browser instance). If the user clicks the Delete button again, the Delete will succeed, as the GridView's original ProductName value ("Chai") now matches up with the value in the database. In both of these cases, the user experience is far from ideal. We clearly don't want to show the user the nitty-gritty details of the DBConcurrencyException exception when using the batch update pattern. And the behavior when using the DB direct pattern is somewhat confusing as the users command failed, but there was no precise indication of why. To remedy these two issues, we can create Label Web controls on the page that provide an explanation to why an update or delete failed. For the batch update pattern, we can determine whether or not aDBConcurrencyException exception occurred in the GridView's post-level event handler, displaying the warning label as needed. For the DB direct method, we can examine the return value of the BLL method (which is true if one row was affected, false otherwise) and display an informational message as needed.

Step 6: Adding Informational Messages and Displaying Them in the Face of a Concurrency Violation
When a concurrency violation occurs, the behavior exhibited depends on whether the DAL's batch update or DB direct pattern was used. Our tutorial uses both patterns, with the batch update pattern being used for updating and the DB direct pattern used for deleting. To get started, let's add two Label Web controls to our page that explain that a concurrency violation occurred when attempting to delete or update data. Set the Label control's Visibleand EnableViewState properties to false; this will cause them to be hidden on each page visit except for those particular page visits where their Visible property is programmatically set to true. <asp:Label ID="DeleteConflictMessage" runat="server" Visible="False" EnableViewState="False" CssClass="Warning" Text="The record you attempted to delete has been modified by another user since you last visited this page. Your delete was cancelled to allow you to review the other user's changes and determine if you want to continue deleting this record." /> <asp:Label ID="UpdateConflictMessage" runat="server" Visible="False" EnableViewState="False" CssClass="Warning" Text="The record you attempted to update has been modified by another user since you started the update process. Your changes have been replaced with the current values. Please review the existing values and make any needed changes." /> In addition to setting their Visible, EnabledViewState, and Text properties, I've also set the CssClass property to Warning, which causes the Label's to be displayed in a large, red, italic, bold font. This CSS Warning class was defined and added to Styles.css back in the Examining the Events Associated with Inserting, Updating, and Deletingtutorial. After adding these Labels, the Designer in Visual Studio should look similar to Figure 18.

Figure 18: Two Label Controls Have Been Added to the Page (Click to view full-size image) With these Label Web controls in place, we're ready to examine how to determine when a concurrency violation has occurred, at which point the appropriate Label's Visible property can be set to true, displaying the informational message.

Handling Concurrency Violations When Updating


Let's first look at how to handle concurrency violations when using the batch update pattern. Since such violations with the batch update pattern cause a DBConcurrencyException exception to be thrown, we need to add code to our ASP.NET page to determine whether a DBConcurrencyException exception occurred during the update process. If so, we should display a message to the user explaining that their changes were not saved because another user had modified the same data between when they started editing the record and when they clicked the Update button. As we saw in the Handling BLL- and DAL-Level Exceptions in an ASP.NET Page tutorial, such exceptions can be detected and suppressed in the data Web control's post-level event handlers. Therefore, we need to create an event handler for the GridView's RowUpdated event that checks if a DBConcurrencyException exception has been thrown. This event handler is passed a reference to any exception that was raised during the updating process, as shown in the event handler code below: Protected Sub ProductsGrid_RowUpdated _ (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _ Handles ProductsGrid.RowUpdated If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then ' Display the warning message and note that the exception has ' been handled...

UpdateConflictMessage.Visible = True e.ExceptionHandled = True End If End If End Sub In the face of a DBConcurrencyException exception, this event handler displays the UpdateConflictMessage Label control and indicates that the exception has been handled. With this code in place, when a concurrency violation occurs when updating a record, the user's changes are lost, since they would have overwritten another user's modifications at the same time. In particular, the GridView is returned to its pre-editing state and bound to the current database data. This will update the GridView row with the other user's changes, which were previously not visible. Additionally, the UpdateConflictMessage Label control will explain to the user what just happened. This sequence of events is detailed in Figure 19.

Figure 19: A User s Updates are Lost in the Face of a Concurrency Violation (Click to view full-size image) Note: Alternatively, rather than returning the GridView to the pre-editing state, we could leave the GridView in its editing state by setting the KeepInEditMode property of the passed-in GridViewUpdatedEventArgs object to true. If you take this approach, however, be certain to rebind the data to the GridView (by invoking its DataBind()method) so that the other user's values are loaded into the editing interface. The code available for download with this tutorial has these two lines of code in the RowUpdated event handler commented out; simply uncomment these lines of code to have the GridView remain in edit mode after a concurrency violation.

Responding to Concurrency Violations When Deleting

With the DB direct pattern, there is no exception raised in the face of a concurrency violation. Instead, the database statement simply affects no records, as the WHERE clause does not match with any record. All of the data modification methods created in the BLL have been designed such that they return a Boolean value indicating whether or not they affected precisely one record. Therefore, to determine if a concurrency violation occurred when deleting a record, we can examine the return value of the BLL's DeleteProduct method. The return value for a BLL method can be examined in the ObjectDataSource's post-level event handlers through the ReturnValue property of the ObjectDataSourceStatusEventArgs object passed into the event handler. Since we are interested in determining the return value from the DeleteProduct method, we need to create an event handler for the ObjectDataSource's Deleted event. The ReturnValue property is of type object and can be null if an exception was raised and the method was interrupted before it could return a value. Therefore, we should first ensure that the ReturnValue property is not null and is a Boolean value. Assuming this check passes, we show theDeleteConflictMessage Label control if the ReturnValue is false. This can be accomplished by using the following code: Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _ (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _ Handles ProductsOptimisticConcurrencyDataSource.Deleted If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean) If deleteReturnValue = False Then ' No row was deleted, display the warning message DeleteConflictMessage.Visible = True End If End If End Sub In the face of a concurrency violation, the user's delete request is canceled. The GridView is refreshed, showing the changes that occurred for that record between the time the user loaded the page and when he clicked the Delete button. When such a violation transpires, the DeleteConflictMessage Label is shown, explaining what just happened (see Figure 20).

Figure 20: A User s Delete is Canceled in the Face of a Concurrency Violation (Click to view full-size image)

Summary
Opportunities for concurrency violations exist in every application that allows multiple, concurrent users to update or delete data. If such violations are not accounted for, when two users simultaneously update the same data whoever gets in the last write "wins," overwriting the other user's changes changes. Alternatively, developers can implement either optimistic or pessimistic concurrency control. Optimistic concurrency control assumes that concurrency violations are infrequent and simply disallows an update or delete command that would constitute a concurrency violation. Pessimistic concurrency control assumes that concurrency violations are frequent and simply rejecting one user's update or delete command is not acceptable. With pessimistic concurrency control, updating a record involves locking it, thereby preventing any other users from modifying or deleting the record while it is locked. The Typed DataSet in .NET provides functionality for supporting optimistic concurrency control. In particular, theUPDATE and DELETE statements issued to the database include all of the table's columns, thereby ensuring that the update or delete will only occur if the record's current data matches with the original data the user had when performing their update or delete. Once the DAL has been configured to support optimistic concurrency, the BLL methods need to be updated. Additionally, the ASP.NET page that calls down into the BLL must be configured such that the ObjectDataSource retrieves the original values from its data Web control and passes them down into the BLL. As we saw in this tutorial, implementing optimistic concurrency control in an ASP.NET web application involves updating the DAL and BLL and adding support in the ASP.NET page. Whether or not this added work is a wise investment of your time and effort depends on your application. If you infrequently have concurrent users updating data, or the data they are updating is different from one another, then concurrency control is not a key issue. If, however, you routinely have multiple users on your site working with the same data, concurrency control can help prevent one user's updates or deletes from unwittingly overwriting another's. Happy Programming!

Adding Client-Side Confirmation When Deleting (VB)


In the interfaces we've created so far, a user can accidentally delete data by clicking the Delete button when they meant to click the Edit button. In this tutorial we'll add a client-side confirmation dialog box that appears when the Delete button is clicked.

Introduction
Over the past several tutorials we ve seen how to use our application architecture, ObjectDataSource, and the data Web controls in concert to provide inserting, editing, and deleting capabilities. The deleting interfaces we ve examined thus far have been composed of a Delete button that, when clicked, causes a postback and invokes the ObjectDataSource s Delete() method. The Delete() method then invokes the configured method from the Business Logic Layer, which propagates the call down to the Data Access Layer, issuing the actual DELETE statement to the database. While this user interface enables visitors to delete records through the GridView, DetailsView, or FormView controls, it lacks any sort of confirmation when the user clicks the Delete button. If a user accidentally clicks the Delete button when they meant to click Edit, the record they meant to update will instead be deleted. To help prevent this, in this tutorial we ll add a client-side confirmation dialog box that appears when the Delete button is clicked. The JavaScript confirm(string) function displays its string input parameter as the text inside a modal dialog box that comes equipped with two buttons - OK and Cancel (see Figure 1). The confirm(string) function returns a Boolean value depending on what button is clicked (true, if the user clicks OK, and false if they click Cancel).

Figure 1: The JavaScript confirm(string) Method Displays a Modal, Client-Side Messagebox During a form submission, if a value of false is returned from a client-side event handler then the form submission is cancelled. Using this feature, we can have the Delete button s client-side onclick event handler return the value of a call to confirm("Are you sure you want to delete this product?"). If the user clicks Cancel,confirm(string) will return false, thereby causing the form submission to cancel. With no postback, the product whose Delete button was clicked won t be deleted. If, however, the user clicks OK in the confirmation dialog box, the postback will continue unabated and the product will be deleted. Consult Using JavaScript s confirm() Method to Control Form Submission for more information on this technique. Adding the necessary client-side script differs slightly if using templates than when using a CommandField. Therefore, in this tutorial we will look at both a FormView and GridView example. Note: Using client-side confirmation techniques, like the ones discussed in this tutorial, assumes that your users are visiting with browsers that support JavaScript and that they have JavaScript enabled. If either of these assumptions are not true for a particular user, clicking the Delete button will immediately cause a postback (not displaying a confirm messagebox).

Step 1: Creating a FormView That Supports Deletion


Start by adding a FormView to the ConfirmationOnDelete.aspx page in the EditInsertDelete folder, binding it to a new ObjectDataSource that pulls back the product information via the ProductsBLL class s GetProducts()method. Also configure the ObjectDataSource so that the ProductsBLL class s DeleteProduct(productID)method is mapped to the ObjectDataSource s Delete() method; ensure that the INSERT and UPDATE tabs drop-down lists are set to (None). Finally, check the Enable Paging checkbox in the FormView s smart tag. After these steps, the new ObjectDataSource s declarative markup will look like the following: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts" TypeName="ProductsBLL"> <DeleteParameters> <asp:Parameter Name="productID" Type="Int32" /> </DeleteParameters> </asp:ObjectDataSource> As in our past examples that did not use optimistic concurrency, take a moment to clear out the ObjectDataSource s OldValuesParameterFormatString property. Since it has been bound to an ObjectDataSource control that only supports deleting, the FormView s ItemTemplateoffers only the Delete button, lacking the New and Update buttons. The FormView s declarative markup, however, includes a superfluous EditItemTemplate and InsertItemTemplate, which can be removed. Take a moment to customize the ItemTemplate so that is shows only a subset of the product data fields. I ve configured mine to show the product s name in an <h3> heading above its supplier and category names (along with the Delete button). <asp:FormView ID="FormView1" AllowPaging="True" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" runat="server">

<ItemTemplate> <h3><i><%# Eval("ProductName") %></i></h3> <b>Category:</b> <asp:Label ID="CategoryNameLabel" runat="server" Text='<%# Eval("CategoryName") %>'> </asp:Label><br /> <b>Supplier:</b> <asp:Label ID="SupplierNameLabel" runat="server" Text='<%# Eval("SupplierName") %>'> </asp:Label><br /> <asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete"> </asp:LinkButton> </ItemTemplate> </asp:FormView> With these changes, we have a fully functional web page that allows a user to toggle through the products one at a time, with the ability to delete a product by simply clicking the Delete button. Figure 2 shows a screen shot of our progress thus far when viewed through a browser.

Figure 2: The FormView Shows Information About a Single Product (Click to view full-size image)

Step 2: Calling the confirm(string) Function from the Delete Buttons ClientSide onclick Event
With the FormView created, the final step is to configure the Delete button such that when it s clicked by the visitor, the JavaScript confirm(string) function is invoked. Adding client-side script to a Button, LinkButton, or ImageButton s client-side onclick event can be accomplished through the use of the OnClientClick property, which is new to ASP.NET 2.0. Since we want to have the value of the confirm(string) function returned, simply set this property to: return confirm('Are you certain that you want to delete this product?'); After this change the Delete LinkButton s declarative syntax should look something like:

<asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete" OnClientClick="return confirm('Are you certain you want to delete this product?');"> </asp:LinkButton> That s all there is to it! Figure 3 shows a screen shot of this confirmation in action. Clicking the Delete button brings up the confirm dialog box. If the user clicks Cancel, the postback is cancelled and the product is not deleted. If, however, the user clicks OK, the postback continues and the ObjectDataSource s Delete() method is invoked, culminating in the database record being deleted. Note: The string passed into the confirm(string) JavaScript function is delimited with apostrophes (rather than quotation marks). In JavaScript, strings can be delimited using either character. We use apostrophes here so that the delimiters for the string passed into confirm(string) do not introduce an ambiguity with the delimiters used for the OnClientClick property value.

Figure 3: A Confirmation is Now Displayed When Clicking the Delete Button (Click to view full-size image)

Step 3: Configuring the OnClientClick Property for the Delete Button in a CommandField
When working with a Button, LinkButton, or ImageButton directly in a template, a confirmation dialog box can be associated with it by simply configuring its OnClientClick property to return the results of the JavaScriptconfirm(string) function. However, the CommandField - which adds a field of Delete buttons to a GridView or DetailsView - does not have an OnClientClick property that can be set declaratively. Instead, we must programmatically reference the Delete button in the GridView or DetailsView s appropriate DataBound event handler, and then set its OnClientClick property there. Note: When setting the Delete button s OnClientClick property in the appropriate DataBound event handler, we have access to the data was bound to the current record. This means we can extend the confirmation message to include details about the particular record, such as, "Are you sure you want to delete the Chai product?" Such customization is also possible in templates using databinding syntax. To practice setting the OnClientClick property for the Delete button(s) in a CommandField, let s add a GridView to the page. Configure this GridView to use the same ObjectDataSource control that the FormView uses. Also limit the GridView s BoundFields to only include the product s name, category, and supplier. Lastly, check the Enable Deleting checkbox from the GridView s smart tag. This will add a CommandField to the GridView s Columnscollection with its ShowDeleteButton property set to true. After making these changes, your GridView s declarative markup should look like the following:

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1"> <Columns> <asp:CommandField ShowDeleteButton="True" /> <asp:BoundField DataField="ProductName" HeaderText="Product" SortExpression="ProductName" /> <asp:BoundField DataField="CategoryName" HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="Supplier" ReadOnly="True" SortExpression="SupplierName" /> </Columns> </asp:GridView> The CommandField contains a single Delete LinkButton instance that can be accessed programmatically from the GridView s RowDataBound event handler. Once referenced, we can set its OnClientClick property accordingly. Create an event handler for the RowDataBound event using the following code: Protected Sub GridView1_RowDataBound(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _ Handles GridView1.RowDataBound If e.Row.RowType = DataControlRowType.DataRow Then ' reference the Delete LinkButton Dim db As LinkButton = CType(e.Row.Cells(0).Controls(0), LinkButton) ' Get information about the product bound to the row Dim product As Northwind.ProductsRow = _ CType(CType(e.Row.DataItem, System.Data.DataRowView).Row, _ Northwind.ProductsRow) db.OnClientClick = String.Format( _ "return confirm('Are you certain you want to delete the {0} product?');", _ product.ProductName.Replace("'", "\'")) End If End Sub This event handler works with data rows (those that will have the Delete button) and begins by programmatically referencing the Delete button. In general use the following pattern: Dim obj As ButtonType = _ CType(e.Row.Cells(commandFieldIndex).Controls(controlIndex), ButtonType) ButtonType is the type of button being used by the CommandField - Button, LinkButton, or ImageButton. By default, the CommandField uses LinkButtons, but this can be customized via the CommandField s ButtonType property. The commandFieldIndex is the ordinal index of the CommandField within the GridView s Columns collection, whereas the controlIndex is the index of the Delete button within the CommandField s Controls collection. The controlIndexvalue depends on the button s position relative to other buttons in the CommandField. For example, if the only button displayed in the CommandField is the Delete button, use an index of 0. If, however, there s an Edit button that precedes the Delete button, use an index of 2. The reason an index of 2 is used is because two controls are added by the CommandField before the Delete button: the Edit button and a LiteralControl that s used to add some space between the Edit and Delete buttons.

For our particular example, the CommandField uses LinkButtons and, being the left-most field, has acommandFieldIndex of 0. Since there are no other buttons but the Delete button in the CommandField, we use acontrolIndex of 0. After referencing the Delete button in the CommandField, we next grab information about the product bound to the current GridView row. Finally, we set the Delete button s OnClientClick property to the appropriate JavaScript, which includes the product s name. Since the JavaScript string passed into the confirm(string) function is delimited using apostrophes we must escape any apostrophes that appear within the product s name. In particular, any apostrophes in the product s name are escaped with "\'". With these changes complete, clicking on a Delete button in the GridView displays a customized confirmation dialog box (see Figure 4). As with the confirmation messagebox from the FormView, if the user clicks Cancel the postback is cancelled, thereby preventing the deletion from occurring. Note: This technique can also be used to programmatically access the Delete button in the CommandField in a DetailsView. For the DetailsView, however, you d create an event handler for the DataBound event, since the DetailsView does not have a RowDataBound event.

Figure 4: Clicking the GridView s Delete Button Displays a Customized Confirmation Dialog Box (Click to view full-size image)

Using TemplateFields
One of the disadvantages of the CommandField is that its buttons must be accessed through indexing and that the resulting object must be cast to the appropriate button type (Button, LinkButton, or ImageButton). Using "magic numbers" and hard-coded types invites problems that cannot be discovered until runtime. For example, if you, or another developer, adds new buttons to the CommandField at some point in the future (such as an Edit button) or changes the ButtonType property, the existing code will still compile without error, but visiting the page may cause an exception or unexpected behavior, depending on how your code was written and what changes were made. An alternative approach is to convert the GridView and DetailsView s CommandFields into TemplateFields. This will generate a TemplateField with an ItemTemplate that has a LinkButton (or Button or ImageButton) for each button in the CommandField. These buttons OnClientClick properties can be assigned declaratively, as we saw with the FormView, or can be programmatically accessed in the appropriate DataBound event handler using the following pattern: Dim obj As ButtonType = CType(e.Row.FindControl("controlID"), ButtonType)

Where controlID is the value of the button s ID property. While this pattern still requires a hard-coded type for the cast, it removes the need for indexing, allowing for the layout to change without resulting in a runtime error.

Summary
The JavaScript confirm(string) function is a commonly used technique for controlling form submission workflow. When executed, the function displays a modal, client-side dialog box that includes two buttons, OK and Cancel. If the user clicks OK, the confirm(string) function returns true; clicking Cancel returns false. This functionality, coupled with a browser s behavior to cancel a form submission if an event handler during the submission process returns false, can be used to display a confirmation messagebox when deleting a record. The confirm(string) function can be associated with a Button Web control s client-side onclick event handler through the control s OnClientClick property. When working with a Delete button in a template - either in one of the FormView s templates or in a TemplateField in the DetailsView or GridView - this property can be set either declaratively or programmatically, as we saw in this tutorial. Happy Programming!

Limiting Data Modification Functionality Based on the User (VB)


In a web application that allows users to edit data, different user accounts may have different data-editing privileges. In this tutorial we'll examine how to dynamically adjust the data modification capabilities based on the visiting user.

Introduction
A number of web applications support user accounts and provide different options, reports, and functionality based on the logged in user. For example, with our tutorials we might want to allow users from the supplier companies to log on to the site and update general information about their products - their name and quantity per unit, perhaps - along with supplier information, such as their company name, address, the contact person s information, and so on. Furthermore, we might want to include some user accounts for people from our company so that they can log on and update product information like units on stock, reorder level, and so forth. Our web application might also allow anonymous users to visit (people who have not logged on), but would limit them to just viewing data. With such a user account system in place, we would want the data Web controls in our ASP.NET pages to offer the inserting, editing, and deleting capabilities appropriate for the currently logged on user. In this tutorial we ll examine how to dynamically adjust the data modification capabilities based on the visiting user. In particular, we ll create a page that displays the suppliers information in an editable DetailsView along with a GridView that lists the products provided by the supplier. If the user visiting the page is from our company, they can: view any supplier s information; edit their address; and edit the information for any product provided by the supplier. If, however, the user is from a particular company, they can only view and edit their own address information and can only edit their products that have not been marked as discontinued.

Figure 1: A User from Our Company Can Edit Any Supplier s Information (Click to view full-size image)

Figure 2: A User from a Particular Supplier Can Only View and Edit their Information (Click to view full-size image) Let s get started!

Note: ASP.NET 2.0 s membership system provides a standardized, extensible platform for creating, managing, and validating user accounts. Since an examination of the membership system is beyond the scope of these tutorials, this tutorial instead "fakes" membership by allowing anonymous visitors to choose whether they are from a particular supplier or from our company. For more on membership, refer to my Examining ASP.NET 2.0 s Membership, Roles, and Profile article series.

Step 1: Allowing the User to Specify their Access Rights


In a real-world web application, a user s account information would include whether they worked for our company or for a particular supplier, and this information would be programmatically accessible from our ASP.NET pages once the user has logged on to the site. This information could be captured through ASP.NET 2.0 s roles system, as user-level account information through the profile system, or through some custom means. Since the aim of this tutorial is to demonstrate adjusting the data modification capabilities based on the logged on user, and is not meant to showcase ASP.NET 2.0 s membership, roles, and profile systems, we ll use a very simply mechanism to determine the capabilities for the user visiting the page - a DropDownList from which the user can indicate if they should be able to view and edit any of the suppliers information or, alternatively, what particular supplier s information they can view and edit. If the user indicates that she can view and edit all supplier information (the default), she can page through all suppliers, edit any supplier s address information, and edit the name and quantity per unit for any product provided by the selected supplier. If the user indicates that she can only view and edit a particular supplier, however, then she can only view the details and products for that one supplier and can only update the name and quantity per unit information for those products that are not discontinued. Our first step in this tutorial, then, is to create this DropDownList and populate it with the suppliers in the system. Open the UserLevelAccess.aspx page in the EditInsertDelete folder, add a DropDownList whose ID property is set to Suppliers, and bind this DropDownList to a new ObjectDataSource named AllSuppliersDataSource.

Figure 3: Create a New ObjectDataSource Named AllSuppliersDataSource (Click to view full-size image) Since we want this DropDownList to include all suppliers, configure the ObjectDataSource to invoke theSuppliersBLL class s GetSuppliers() method. Also ensure that the ObjectDataSource s Update() method is mapped to the SuppliersBLL class s UpdateSupplierAddress method, as this ObjectDataSource will also be used by the DetailsView we ll be adding in Step 2.

After completing the ObjectDataSource wizard, complete the steps by configuring the Suppliers DropDownList such that it shows the CompanyName data field and uses the SupplierID data field as the value for each ListItem.

Figure 4: Configure the Suppliers DropDownList to Use the CompanyName and SupplierID Data Fields (Click to view full-size image) At this point, the DropDownList lists the company names of the suppliers in the database. However, we also need to include a "Show/Edit ALL Suppliers" option to the DropDownList. To accomplish this, set the SuppliersDropDownList s AppendDataBoundItems property to true and then add a ListItem whose Text property is "Show/Edit ALL Suppliers" and whose value is -1. This can be added directly through the declarative markup or through the Designer by going to the Properties window and clicking on the ellipses in the DropDownList s Itemsproperty. Note: Refer back to the Master/Detail Filtering With a DropDownList tutorial for a more detailed discussion on adding a Select All item to a databound DropDownList. After the AppendDataBoundItems property has been set and the ListItem added, the DropDownList s declarative markup should look like: <asp:DropDownList ID="Suppliers" runat="server" AppendDataBoundItems="True" DataSourceID="AllSuppliersDataSource" DataTextField="CompanyName" DataValueField="SupplierID"> <asp:ListItem Value="-1">Show/Edit ALL Suppliers</asp:ListItem> </asp:DropDownList> Figure 5 shows a screen shot of our current progress, when viewed through a browser.

Figure 5: The Suppliers DropDownList Contains a Show ALL ListItem, Plus One for Each Supplier (Click to view fullsize image) Since we want to update the user interface immediately after the user has changed their selection, set theSuppliers DropDownList s AutoPostBack property to true. In Step 2 we ll create a DetailsView control that will show the information for the supplier(s) based on the DropDownList selection. Then, in Step 3, we ll create an event handler for this DropDownList s SelectedIndexChanged event, in which we ll add code that binds the appropriate supplier information to the DetailsView based upon the selected supplier.

Step 2: Adding a DetailsView Control


Let s use a DetailsView to show supplier information. For the user who can view and edit all suppliers, the DetailsView will support paging, allowing the user to step through the supplier information one record at a time. If the user works for a particular supplier, however, the DetailsView will show only that particular supplier s information and will not include a paging interface. In either case, the DetailsView needs to allow the user to edit the supplier s address, city, and country fields.

Add a DetailsView to the page beneath the Suppliers DropDownList, set its ID property to SupplierDetails, and bind it to the AllSuppliersDataSource ObjectDataSource created in the previous step. Next, check the Enable Paging and Enable Editing checkboxes from the DetailsView s smart tag. Note: If you don t see an Enable Editing option in the DetailsView s smart tag it s because you did not map the ObjectDataSource s Update() method to the SuppliersBLL class s UpdateSupplierAddress method. Take a moment to go back and make this configuration change, after which the Enable Editing option should appear in the DetailsView s smart tag. Since the SuppliersBLL class s UpdateSupplierAddress method only accepts four parameters supplierID,address, city, and country - modify the DetailsView s BoundFields so that the CompanyName and PhoneBoundFields are read-only. Additionally, remove the SupplierID BoundField altogether. Finally, theAllSuppliersDataSource ObjectDataSource currently has its OldValuesParameterFormatString property set tooriginal_{0}. Take a moment to remove this property setting from the declarative syntax altogether, or set it to the default value, {0}. After configuring the SupplierDetails DetailsView and AllSuppliersDataSource ObjectDataSource, we will have the following declarative markup: <asp:ObjectDataSource ID="AllSuppliersDataSource" runat="server" SelectMethod="GetSuppliers" TypeName="SuppliersBLL" UpdateMethod="UpdateSupplierAddress"> <UpdateParameters> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="address" Type="String" /> <asp:Parameter Name="city" Type="String" /> <asp:Parameter Name="country" Type="String" /> </UpdateParameters> </asp:ObjectDataSource> <asp:DetailsView ID="SupplierDetails" runat="server" AllowPaging="True" AutoGenerateRows="False" DataKeyNames="SupplierID" DataSourceID="AllSuppliersDataSource"> <Fields> <asp:BoundField DataField="CompanyName" HeaderText="Company" ReadOnly="True" SortExpression="CompanyName" /> <asp:BoundField DataField="Address" HeaderText="Address" SortExpression="Address" /> <asp:BoundField DataField="City" HeaderText="City" SortExpression="City" /> <asp:BoundField DataField="Country" HeaderText="Country" SortExpression="Country" /> <asp:BoundField DataField="Phone" HeaderText="Phone" ReadOnly="True" SortExpression="Phone" /> <asp:CommandField ShowEditButton="True" /> </Fields> </asp:DetailsView> At this point the DetailsView can be paged through and the selected supplier s address information can be updated, regardless of the selection made in the Suppliers DropDownList (see Figure 6).

Figure 6: Any Suppliers Information Can Be Viewed, and Its Address Updated (Click to view full-size image)

Step 3: Displaying Only the Selected Supplier s Information


Our page currently displays the information for all suppliers regardless of whether a particular supplier has been selected from the Suppliers DropDownList. In order to display just the supplier information for the selected supplier we need to add another ObjectDataSource to our page, one that retrieves information about a particular supplier. Add a new ObjectDataSource to the page, naming it SingleSupplierDataSource. From its smart tag, click the Configure Data Source link and have it use the SuppliersBLL class s GetSupplierBySupplierID(supplierID)method. As with the AllSuppliersDataSource ObjectDataSource, have the SingleSupplierDataSourceObjectDataSource s Update() method mapped to the SuppliersBLL class s UpdateSupplierAddress method.

Figure 7: Configure the SingleSupplierDataSource ObjectDataSource to Use theGetSupplierBySupplierID(supplierID) Method (Click to view full-size image) Next, we re prompted to specify the parameter source for the GetSupplierBySupplierID(supplierID) method ssupplierID input parameter. Since we want to show the information for the supplier selected from the DropDownList, use the Suppliers DropDownList s SelectedValue property as the parameter source.

Figure 8: Use the Suppliers DropDownList as the supplierID Parameter Source (Click to view full-size image) Even with this second ObjectDataSource added, the DetailsView control is currently configured to always use theAllSuppliersDataSource ObjectDataSource. We need to add logic to adjust the data source used by the DetailsView depending on the Suppliers DropDownList item selected. To accomplish this, create

aSelectedIndexChanged event handler for the Suppliers DropDownList. This can most easily be created by doubleclicking the DropDownList in the Designer. This event handler needs to determine what data source to use and must rebind the data to the DetailsView. This is accomplished with the following code: Protected Sub Suppliers_SelectedIndexChanged _ (ByVal sender As Object, ByVal e As System.EventArgs) _ Handles Suppliers.SelectedIndexChanged If Suppliers.SelectedValue = "-1" Then ' The "Show/Edit ALL" option has been selected SupplierDetails.DataSourceID = "AllSuppliersDataSource" ' Reset the page index to show the first record SupplierDetails.PageIndex = 0 Else ' The user picked a particular supplier SupplierDetails.DataSourceID = "SingleSupplierDataSource" End If ' Ensure that the DetailsView and GridView are in read-only mode SupplierDetails.ChangeMode(DetailsViewMode.ReadOnly) ' Need to "refresh" the DetailsView SupplierDetails.DataBind() End Sub The event handler begins by determining whether the "Show/Edit ALL Suppliers" option was selected. If it was, it sets the SupplierDetails DetailsView s DataSourceID to AllSuppliersDataSource and returns the user to the first record in the set of suppliers by setting the PageIndex property to 0. If, however, the user has selected a particular supplier from the DropDownList, the DetailsView s DataSourceID is assigned toSingleSuppliersDataSource. Regardless of what data source is used, the SuppliersDetails mode is reverted back to the read-only mode and the data is rebound to the DetailsView by a call to the SuppliersDetails control s DataBind() method. With this event handler in place, the DetailsView control now shows the selected supplier, unless the "Show/Edit ALL Suppliers" option was selected, in which case all of the suppliers can be viewed through the paging interface. Figure 9 shows the page with the "Show/Edit ALL Suppliers" option selected; note that the paging interface is present, allowing the user to visit and update any supplier. Figure 10 shows the page with the Ma Maison supplier selected. Only Ma Maison s information is viewable and editable in this case.

Figure 9: All of the Suppliers Information Can Be Viewed and Edited (Click to view full-size image)

Figure 10: Only the Selected Supplier s Information Can Be Viewed and Edited (Click to view full-size image) Note: For this tutorial, both the DropDownList and DetailsView control s EnableViewState must be set to true (the default) because the DropDownList s SelectedIndex and the DetailsView s DataSourceID property s changes must be remembered across postbacks.

Step 4: Listing the Suppliers Products in an Editable GridView

With the DetailsView complete, our next step is to include an editable GridView that lists those products provided by the selected supplier. This GridView should allow edits to only the ProductName and QuantityPerUnit fields. Moreover, if the user visiting the page is from a particular supplier, it should only allow updates to those products that are not discontinued. To accomplish this we ll need to first add an overload of the ProductsBLL class sUpdateProducts method that takes in just the ProductID, ProductName, and QuantityPerUnit fields as inputs. We ve stepped through this process beforehand in numerous tutorials, so let s just look at the code here, which should be added to ProductsBLL: <System.ComponentModel.DataObjectMethodAttribute _ (System.ComponentModel.DataObjectMethodType.Update, False)> _ Public Function UpdateProduct(ByVal productName As String, _ ByVal quantityPerUnit As String, ByVal productID As Integer) As Boolean Dim products As Northwind.ProductsDataTable = Adapter.GetProductByProductID(productID) If products.Count = 0 Then ' no matching record found, return false Return False End If Dim product As Northwind.ProductsRow = products(0) product.ProductName = productName If quantityPerUnit Is Nothing Then product.SetQuantityPerUnitNull() Else product.QuantityPerUnit = quantityPerUnit End If ' Update the product record Dim rowsAffected As Integer = Adapter.Update(product) ' Return true if precisely one row was updated, otherwise false Return rowsAffected = 1 End Function With this overload created, we re ready to add the GridView control and its associated ObjectDataSource. Add a new GridView to the page, set its ID property to ProductsBySupplier, and configure it to use a new ObjectDataSource named ProductsBySupplierDataSource. Since we want this GridView to list those products by the selected supplier, use the ProductsBLL class s GetProductsBySupplierID(supplierID) method. Also map theUpdate() method to the new UpdateProduct overload we just created.

Figure 11: Configure the ObjectDataSource to Use the UpdateProduct Overload Just Created (Click to view full-size image) We re prompted to select the parameter source for the GetProductsBySupplierID(supplierID) method ssupplierID input parameter. Since we want to show the products for the supplier selected in the DetailsView, use the SuppliersDetails DetailsView control s SelectedValue property as the parameter source.

Figure 12: Use the SuppliersDetails DetailsView s SelectedValue Property as the Parameter Source (Click to view full-size image) Returning to the GridView, remove all of the GridView fields except for ProductName, QuantityPerUnit, andDiscontinued, marking the Discontinued CheckBoxField as read-only. Also, check the Enable Editing option from

the GridView s smart tag. After these changes have been made, the declarative markup for the GridView and ObjectDataSource should look similar to the following: <asp:GridView ID="ProductsBySupplier" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsBySupplierDataSource"> <Columns> <asp:CommandField ShowEditButton="True" /> <asp:BoundField DataField="ProductName" HeaderText="Product" SortExpression="ProductName" /> <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" SortExpression="QuantityPerUnit" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" ReadOnly="True" SortExpression="Discontinued" /> </Columns> </asp:GridView> <asp:ObjectDataSource ID="ProductsBySupplierDataSource" runat="server" OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL" SelectMethod="GetProductsBySupplierID" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> <SelectParameters> <asp:ControlParameter ControlID="SupplierDetails" Name="supplierID" PropertyName="SelectedValue" Type="Int32" /> </SelectParameters> </asp:ObjectDataSource> As with our previous ObjectDataSources, this one s OldValuesParameterFormatString property is set tooriginal_{0}, which will cause problems when attempting to update a product s name or quantity per unit. Remove this property from the declarative syntax altogether or set it to its default, {0}. With this configuration complete, our page now lists the products provided by the supplier selected in the GridView (see Figure 13). Currently any product s name or quantity per unit can be updated. However, we need to update our page logic so that such functionality is prohibited for discontinued products for users associated with a particular supplier. We ll tackle this final piece in Step 5.

Figure 13: The Products Provided by the Selected Supplier are Displayed (Click to view full-size image) Note: With the addition of this editable GridView the Suppliers DropDownList s SelectedIndexChanged event handler should be updated to return the GridView to a read-only state. Otherwise, if a different supplier is selected while in the middle of editing product information, the corresponding index in the GridView for the new supplier will also be editable. To prevent this, simply set the GridView s EditIndex property to -1 in theSelectedIndexChanged event handler. Also, recall that it is important that the GridView s view state be enabled (the default behavior). If you set the GridView s EnableViewState property to false, you run the risk of having concurrent users unintentionally deleting or editing records. See WARNING: Concurrency Issue with ASP.NET 2.0 GridViews/DetailsView/FormViews that Support Editing and/or Deleting and Whose View State is Disabled for more information.

Step 5: Disallow Editing for Discontinued Products When Show/Edit ALL Suppliers is Not Selected
While the ProductsBySupplier GridView is fully functional, it currently grants too much access to those users who are from a particular supplier. Per our business rules, such users should not be able to update discontinued products. To enforce this, we can hide (or disable) the Edit button in those GridView rows with discontinued products when the page is being visited by a user from a supplier. Create an event handler for the GridView s RowDataBound event. In this event handler we need to determine whether or not the user is associated with a particular supplier, which, for this tutorial, can be determined by checking the Suppliers DropDownList s SelectedValue property - if it s something other than -1, then the user is associated with a particular supplier. For such users, we then need to determine whether or not the product is discontinued. We can grab a reference to the actual ProductRow instance bound to the GridView row via thee.Row.DataItem property, as discussed in the Displaying Summary Information in the GridView s Footer tutorial. If the product is discontinued, we can grab a programmatic reference to the Edit button in the GridView s CommandField using the techniques discussed in the previous tutorial, Adding Client-Side Confirmation When Deleting. Once we have a reference we can then hide or disable the button.

Protected Sub ProductsBySupplier_RowDataBound _ (ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _ Handles ProductsBySupplier.RowDataBound If e.Row.RowType = DataControlRowType.DataRow Then ' Is this a supplier-specific user? If Suppliers.SelectedValue <> "-1" Then ' Get a reference to the ProductRow Dim product As Northwind.ProductsRow = _ CType(CType(e.Row.DataItem, System.Data.DataRowView).Row, _ Northwind.ProductsRow) ' Is this product discontinued? If product.Discontinued Then ' Get a reference to the Edit LinkButton Dim editButton As LinkButton = _ CType(e.Row.Cells(0).Controls(0), LinkButton) ' Hide the Edit button editButton.Visible = False End If End If End If End Sub With this event handler in place, when visiting this page as a user from a specific supplier those products that are discontinued are not editable, as the Edit button is hidden for these products. For example, Chef Anton s Gumbo Mix is a discontinued product for the New Orleans Cajun Delights supplier. When visiting the page for this particular supplier, the Edit button for this product is hidden from sight (see Figure 14). However, when visiting using the "Show/Edit ALL Suppliers," the Edit button is available (see Figure 15).

Figure 14: For Supplier-Specific Users the Edit Button for Chef Anton s Gumbo Mix is Hidden (Click to view full-size image)

Figure 15: For Show/Edit ALL Suppliers Users, the Edit Button for Chef Anton s Gumbo Mix is Displayed (Click to view full-size image)

Checking for Access Rights in the Business Logic Layer


In this tutorial the ASP.NET page handles all logic with regards to what information the user can see and what products he can update. Ideally, this logic would also be present at the Business Logic Layer. For example, theSuppliersBLL class s GetSuppliers() method (which returns all suppliers) might include a check to ensure that the currently logged on user is not associated with a specific supplier. Likewise, the UpdateSupplierAddressmethod could include a check to ensure that the currently logged on user either worked for our company (and therefore can update all suppliers address information) or is associated with the supplier whose data is being updated. I did not include such BLL-layer checks here because in our tutorial the user s rights are determined by a DropDownList on the page, which the BLL classes cannot access. When using the membership system or one of the out-of-the box authentication schemes provided by ASP.NET (such as Windows authentication), the currently logged on user s information and roles information can be accessed from the BLL, thereby making such access rights checks possible at both the presentation and BLL layers.

Summary
Most sites that provide user accounts need to customize the data modification interface based upon the logged in user. Administrative users may be able to delete and edit any record, whereas non-administrative users may be limited to only updating or deleting records they created themselves. Whatever the scenario may be, the data Web controls, ObjectDataSource, and Business Logic Layer classes can be extended to add or deny certain functionality based on the

logged on user. In this tutorial we saw how to limit the viewable and editable data depending on whether the user was associated with a particular supplier or if they worked for our company. This tutorial concludes our examination of inserting, updating, and deleting data using the GridView, DetailsView, and FormView controls. Starting with the next tutorial, we ll turn our attention to adding paging and sorting support. Happy Programming!

Vous aimerez peut-être aussi