Previous Page
Next Page

Updating a Database Using a DataSet

In the exercises so far in this chapter, you have seen how to fetch data from a database. Now it's time to show you how to update data. First, however, we need to consider some potential problems and how using a DataSet can overcome them.

Databases are intended to support multiple concurrent users, but resources such as the number of concurrent connections allowed might be limited. In an application that fetches and displays data, you never know quite how long the user will be browsing the data, and it is not a good practice to keep a database connection open for an extended period of time. Instead, a better approach is to connect to the database, fetch the data into a DataSet object, and then disconnect again. The user can browse the data in the DataSet and make any changes required. After the user finishes, the program can reconnect to the database and submit any changes. Of course, there are complications that you need to consider, such as what happens if two users have queried and updated the same data, changing it to different values. Which value should be stored in the database? We will come back to this problem shortly.

Managing Connections

In earlier exercises, you have seen that when you define a DataSet you can specify a connection to use for communicating with the database. This information is embedded into the TableAdapter used to retrieve the data and fill the DataSet. When you execute the Fill or GetData methods, the code generated by Visual Studio 2005 examines the state of the connection first. If the connection to be used is already open, it is used to retrieve the data, and is left open at the end of the operation. If the connection is closed, the Fill and GetData methods open it, fetch the data, and then close it again. The DataSet in this case is referred to as a disconnected DataSet as it doesn't maintain an active connection to the database. Disconnected DataSet objects act as a data cache in applications. You can modify the data in the DataSet, and later reopen the connection and send the changes back to the database.

You can manually open a connection to a database by creating a SqlConnection object, setting its ConnectionString property, and then calling its Open method as shown in Chapter 23. You can associate an open connection with a TableAdapter by setting the Connection property. The following code shows how to connect to the database and fill the Suppliers DataTable. In this case, the database connection will remain open after the Fill method completes:

SqlConnection dataConnection = new SqlConnection(); 
dataConnection.ConnectionString = "Integrated Security=true;" + 
                                  "Initial Catalog=Northwind;" + 
                                  "Data Source=YourServer\\SQLExpress"; 
dataConnection.Open(); 
suppliersTableAdapter.Connection = dataConnection;
suppliersTableAdapter.Fill(northwindDataSet.Suppliers);

Unless you have a good reason to do so, you should avoid maintaining connections longer than needed; let the Fill and GetData methods open and close the connection for you and create a disconnected DataSet.

Handling Multi-User Updates

Earlier in this chapter, we mentioned the problem that arises if two users try and update the same data at the same time. There are at least two possible approaches you can adopt to solve this problem. Each approach has its benefits and disadvantages.

The first technique involves the Use optimistic concurrency option in the the Advanced Options dialog box in the TableAdapter Configuration Wizard.

Graphic

If you deselect this option, the rows retrieved into the DataSet will be locked in the database to prevent other users from changing them. This is known as pessimistic concurrency. It guarantees that any changes you make will not conflict with changes made by any other users at the expense of blocking those other users. If you retrieve a large number of rows and only update a small proportion of them, you have potentially prevented other users from modifying any of the rows that you have not changed. There is one other drawback—locking data requires that the connection used to retrieve the data remains open, therefore if you use pessimistic concurrency you also run the risk of consuming a large number of connection resources. The principal advantage of pessimistic concurrency, of course, is simplicity. You don't have to write code that checks for updates made by other users before modifying the database.

If you select the “Use optimistic concurrency” option, data is not locked, and the connection can be closed after the data has been fetched. The disadvantage is that you have to write code that ascertains whether any updates made by the user conflict with those made by other users, and this code can be quite difficult to write and debug. However, the TableAdapter object generated by the TableAdapter Configuration Wizard hides much of this complexity, although you must be prepared to handle the events that can be raised if conflicts occur. We will look at this in more detail in the final part of this chapter.

Using a DataSet with a DataGridView Control

Now that you have a good understanding of how to create DataSet objects, retrieve rows, and display data, you are going to add functionality to the Products Maintenance application to allow the user to update the details of Products in the database. You will use a DataGridView control for displaying and updating the data.

Add the DataGridView control to the form
  1. Display ProductsForm in the Design View window. Delete the supplierList ComboBox control and the supplierID Label control from the form.

  2. In the Code and Text Editor window, remove the supplierList_SelectedIndexChanged method from ProductsForm.cs.

  3. Return to the Design View window. Resize the form; set its Size property to 600, 400.

  4. In the Toolbox, expand the Data category, and click the DataGridView control. Drop the control onto the form. Rename the DataGridView control as productsGrid. Set its Location property to 13, 61 and its Size property to 567, 300.

  5. Add two Button controls to the form above productsGrid and set their Location properties to 402, 22 and 505, 22. Rename them as queryButton and saveButton. Change the Text property of each button to Query and Save, respectively.

The next step is to create a DataAdapter class and bind it to the DataGridView control. You will create the DataAdapter class by using the TableAdapter Configuration Wizard. To add a bit of variation, rather than setting the DataSource and DataMember properties of the DataGridView control by using the Design View window, you will bind to the DataAdapter by writing some code.

Create a DataAdapter for fetching Products and bind it to the DataGridView control
  1. Display the DataSet Design window for NorthwindDataSet.xsd (double-click NorthwindDataSet.xsd in the Solution Explorer).

  2. Add a new TableAdapter to the DataSet by using the TableAdapter Configuration Wizard for fetching the details of Products from the Northwind database. Use the information in the following table to help you.

    Page

    Field

    Value

    Choose Your Data Connection

    Which data connection should your application use to connect to the database?

    NorthwindConnectionString

    Choose a Command Type

    How should the TableAdapter access the database?

    Use SQL statements

    Enter a SQL Statement

    What data should be loaded into the table?

    SELECT * FROM Products

    Advanced Options

    Select all options

    Choose Methods to Generate

    Which methods do you want to add to the TableAdapter?

    Select all options and use default method names

    When you complete the wizard and the DataTable and TableAdapter are generated, notice that the wizard automatically detects that the Suppliers and Products table have a relationship in the database and creates a Relation that links the DataTables together.

  3. In the Build menu, click Rebuild Solution to generate the code for the new DataTable and TableAdapter classes.

  4. Display ProductForm in the Design View window. Double-click the Query Button control.

    Visual Studio 2005 generates a click event handler for the control called queryButton_Click and places you in the Code and Text Editor window.

  5. Add the following statements to the queryButton_Click method:

    NorthwindDataSetTableAdapters.ProductsTableAdapter productsTA =
            new NorthwindDataSetTableAdapters.ProductsTableAdapter();
    productsTA.Fill(northwindDataSet.Products);
    BindingSource productsBS = new BindingSource(northwindDataSet, "Products");
    productsGrid.DataSource = productsBS;

    The first statement creates a new instance of the ProductsTableAdapter class that you defined by using the TableAdapter Configuration Wizard. Notice that this class is defined in a namespace called NorthwindDataSetTableAdapters (as are all the other TableAdapter classes for this DataSet). The second statement uses this object to fill the Products DataTable in the northwindDataSet object. Remember that this statement will automatically disconnect from the database after fetching the data because no prior connection had been established. The third statement creates a new BindingSource object for the Products DataTable in the northwindDataSet object. The fourth statement actually performs the data binding—it sets the DataSource property of the DataGridView control to refer to the new BindingSource object.

  6. Build and run the application.

    When the application starts, the DataGridView control is initially empty.

  7. Click Query. The DataGridView displays a list of Products. Verify that the number of rows in the DataGridView control matches the value displayed by the Number of Products label.

    Graphic
  8. Click the ProductName column header. The rows are sorted by product name and appear in ascending order. Click the ProductName column again. This time the rows appear in descending order. Click the ProductID column to display the rows in their original order.

  9. Click any cell in the DataGridView and overtype the data. By default, you can modify the contents of any cell other than the ProductID. You cannot change the ProductID of a row because this column is marked as the primary key of the table in the database (primary key values should never be updated as they are used to identify a row in the database).

    Try typing an invalid value into a cell—type an alphabetic string into the SupplierID column, for example. When you click away, an error dialog will be displayed. This is the default error handling dialog for the DataGridView control (and is rather ugly). You can replace it with your own code by trapping the DataError event of the DataGridView control. Press the Escape key to undo the change.

  10. Scroll right to display the Discontinued column. Notice that this column appears as a check box. In the database, this column has the bit type, and can only contain two values (1 or 0).

  11. Scroll to the end of the data in the DataGridView control. A row marked with an asterisk appears. You can add a new product by entering its details in this row. Notice that the ProductID is generated automatically.

  12. Click in the gray margin on the left-hand side of row 76. The entire row is highlighted. Press the Delete key. The row disappears from the DataGridView control.

  13. When you have finished browsing the data, close the form and return to Visual Studio 2005. No changes will be saved as you have not yet written the code to do this.

Validating User Input in the DataGridView Control

Before saving changes back to the database, we want to ensure that the changes the user makes are valid. Currently, the user can type any amount of rubbish into the DataGridView. In the next exercises, you will learn how to constrain the user input to eliminate some possible sources of error, and validate the data in the control.

Configure the DataGridView control to constrain user input
  1. Display ProductsForm in the Design View window. Delete the Query button.

  2. Display the ProductsForm.cs file in the Code and Text Editor window. Comment out the queryButton_Click method and its contents.

  3. Return to the Design View window and click the DataGridView control. In the Properties window, set the DataSource property to the Products DataTable in the NorthwindDataSet class (in Project Data Sources, in Other Data Sources).

    The columns in the Products table appear in the DataGridView control.

  4. Select the Columns property in the Properties window and click the ellipses button.

    The Edit Columns dialog box appears. You can use this dialog to view and set the properties of each column in the DataGridView control.

  5. In the Edit Columns dialog box, click the ProductID column. In the list of Bound Column Properties, verify that the ReadOnly property is set to True. The values in this column are generated automatically, so preventing the user from changing this value eliminates one possible source of user errors.

  6. Click the SupplierID column. Set the ColumnType property to DataGridViewComboBoxColumn and set the DisplayStyle property to ComboBox. This will cause the column to be displayed as a combo box rather than a text box.

    NOTE
    The DisplayStyle property does not appear until you set the ColumnType property.
  7. Set the DataSource property to suppliersBindingSource. You will display the list of suppliers in this column by using the same technique that you saw earlier in this chapter when performing complex data binding. Set the DisplayMember property to CompanyName, and the ValueMember property to SupplierID. When the user selects a supplier from the list, the SupplierID will be used behind the scenes to supply the value for this column in the Products table. This eliminates another possible source of user errors.

  8. In the Edit Columns dialog box, click OK.

You have seen how to prevent some basic errors by restricting the user input to a set of valid values. However, you still need to trap the other errors that can occur. You will achieve this by writing handlers for the CellValidating, CellEndEdit, and DataError events. The CellValidating event occurs whenever the user has changed the contents of a cell. The CellEndEdit event occurs after a cell has been validated, and the user attempts to move away.

Handle the CellValidating, CellEndEdit, and DataError event
  1. Click the productsGrid control in the Design View window, and click the Events button in the Properties window.

  2. Double-click the CellValidating event.

    Visual Studio 2005 generates the event method, productsGrid_CellValidating.

  3. Add the following statements to the productsGrid_CellValidating method:

    int newInteger; 
    productsGrid.Rows[e.RowIndex].ErrorText = ""; 
    if ((productsGrid.Columns[e.ColumnIndex].DataPropertyName == "UnitsInStock") || 
        (productsGrid.Columns[e.ColumnIndex].DataPropertyName == "UnitsOnOrder") || 
        (productsGrid.Columns[e.ColumnIndex].DataPropertyName == "ReorderLevel")) 
    { 
        if (!int.TryParse(e.FormattedValue.ToString(), out newInteger) || 
            newInteger < 0) 
        { 
            productsGrid.Rows[e.RowIndex].ErrorText =
                      "Value must be a non-negative number"; 
            e.Cancel = true; 
        } 
    }

    This method ensures that the user types a non-negative integer into the UnitsInStock, UnitsOnOrder, or ReorderLevel columns.

    The second parameter to the method, e, is a DataGridViewCellValidatingEventArgs object. This object contains a number of properties that you can use to identify the cell being edited—e.ColumnIndex contains the number of the column, and e.RowIndex contains the row number (the first column is column 0, and the first row is row 0). The first if statement determines which column the user is currently in. Notice that the DataGridView class contains a Columns collection holding the details of each displayed column. The value of e.ColumnIndex is used as an indexer into this collection, and the value of the Name property is used to identify the column.

    The int.TryParse method is a useful way to determine whether a string contains a value that can be converted to an integer; it returns true if the operation is successful (and also passes back an int containing the converted value as an out parameter), false otherwise. The second if statement uses int.TryParse to determine whether the user has typed a valid integer into the current cell. If this test fails, or if the value typed is a valid int but is less than zero, the ErrorText property of the current row in the grid is set to a suitable error message. The ErrorText property acts like the ErrorProvider control you saw in Chapter 22, “Performing Validation”—it displays an icon when an error occurs, and the user can hover over this icon with the mouse to display the error message in a tooltip. The Cancel property of the e parameter is also set; this prevents the user from being able to move away from the cell until she types in some valid data.

  4. In the Design View window, click the DataGridView control. In the Properties window, click the Events button. Double-click the CellEndEdit event.

    Visual Studio 2005 generates an event handler for the CellEndEdit event called productsGrid_CellEndEdit and displays the Code and Text Editor window.

  5. Add the following statement to the productsGrid_CellEndEdit method:

    productsGrid.Rows[e.RowIndex].ErrorText = "";

    This method is executed when the data has been successfully validated and the user moves to a different cell. The code simply clears any error message that was displayed.

  6. In the Design View window, click the DataGridView control. In the Properties window, click the Events button. Double-click the DataError event.

    Visual Studio 2005 generates an event handler for the DataError event called productsGrid_DataError and displays the Code and Text Editor window.

  7. Add the following statements to the productsGrid_DataError method:

    productsGrid.Rows[e.RowIndex].ErrorText = "Invalid input. Please re-enter";
    e.Cancel = true;

    The DataError event is a catch-all data validation event handler. If the user types an invalid value (such as an alphabetic string into a numeric column), that is not trapped elsewhere, the error will be caught here. This implementation simply outputs a message to the ErrorText property and prevents the user from moving away.

  8. Build and run the application.

    The DataGridView control is filled as soon as the form appears.

  9. Try and change a value in the ProductID column. This column should be read-only.

  10. Change the SupplierID for any row by clicking the drop-down menu that appears, displaying the supplier names.

  11. Type a negative number into the UnitsInStock column and then click a different cell. An error icon is displayed in the row header. Hover the mouse over this icon to display the error message. This is the message from the CellValidating event.

    Graphic
  12. Type a positive integer into the UnitsIsStock column and then click a different cell. The error icon disappears and the data is accepted.

  13. Type an alphabetic string into the CategoryID column and then click a different cell. The error icon appears again. Hover the mouse over the icon to display the error message. This is the message from the DataError event.

  14. Press the Escape key to undo the change.

    The error icon disappears.

  15. Close the form and return to Visual Studio 2005.

Performing Updates by Using a DataSet

The changes made by using the DataGridView control are automatically copied back to the DataSet acting as the data source for the control. Saving changes made to the data involves reconnecting to the database, performing any required SQL INSERT, UPDATE, and DELETE statements, and then disconnecting from the database. You must also be prepared to handle any errors that might occur. Fortunately, the TableAdapter classes generated for the DataSet contain several methods and events that can help you perform these tasks.

Before updating the database, you should ensure that the data is valid. After all, you don't want to waste a round-trip over the network to the database and all those database resources if the operation is going to fail.

Validate the changes and handle errors
  1. Display ProductsForm in the Design View window. Select the Save button. In the Properties window, click Events. Double-click the Click event.

    Visual Studio 2005 geneates an event method called saveButton_Click.

  2. In the Code and Text Editor window, add the following try/catch block to the saveButton_Click method:

    try 
    { 
        NorthwindDataSet changes = (NorthwindDataSet)northwindDataSet.GetChanges(); 
        if (changes == null) 
        { 
            return; 
        } 
     
        // Check for errors 
     
        //  If no errors then update the database, otherwise tell the user 
    } 
    catch(Exception ex) 
    { 
        MessageBox.Show("Error: " + ex.Message, "Errors", 
            MessageBoxButtons.OK, MessageBoxIcon.Error); 
        northwindDataSet.RejectChanges();
    }

    This block of code uses the GetChanges method of northwindDataSet to create a new NorthwindDataSet object that contains only the rows that have changed. (The cast is necessary because the GetChanges method is defined as returning a generic DataSet object.)

    Although this code is not strictly necessary, it makes updating the database quicker because the update routines do not have to calculate which rows have changed and which ones haven't. If there are no changes (the NorthwindDataSet object is null), the method finishes; otherwise the method checks for errors in the data and updates the database (you will write this code shortly). If an exception occurs while performing the updates, the application displays a message to the user, and undoes the changes in northwindDataSet by using the RejectChanges method.

  3. In the Code and Text Editor window, replace the // Check for errors comment in the saveButton_Click method with the following code:

    DataTable dt = changes.Tables["Products"]; 
    DataRow [] badRows = dt.GetErrors();

    The first statement extracts the Products DataTable in the changes DataSet. The GetErrors method of a DataTable object returns an array of all the rows in the table that have one or more validation errors. If there are no errors, GetErrors returns an empty array.

  4. Replace the // If no errors then update the database, otherwise tell the user comment with the following code block:

    if (badRows.Length == 0) 
    { 
        // Update the database 
    } 
    else 
    { 
        // Find the errors and inform the user 
    }

    There are several strategies you can use for reporting errors to the user. One useful technique is to find all the errors and report them in a single (but possibly long) message.

  5. Replace the // Find the errors and inform the user comment with the following statements:

    string errorMsg = null; 
    foreach (DataRow row in badRows) 
    { 
        foreach (DataColumn col in row.GetColumnsInError()) 
        { 
            errorMsg += row.GetColumnError(col) + "\n"; 
        } 
    } 
    MessageBox.Show("Errors in data: " + errorMsg, 
        "Please fix", MessageBoxButtons.OK, 
        MessageBoxIcon.Error);

    This code iterates through all the rows in the badRows array. Each row may have one or more errors, and the GetColumnsInError method returns a collection containing all the columns with bad data. The GetColumnError method retrieves the error message for an individual column. Each error message is appended to the errorMsg string. When all the bad rows and columns have been examined, the application displays a message box showing all the errors. The user should be able to use this information to correct the changes and then resubmit them.

Update the database
  1. Once you are certain that the data seems to be correct, you can send it to the database. Locate the // Update the database comment in the saveButton_Click method and replace it with the following statements:

    int numRows = productsTableAdapter.Update(changes); 
    MessageBox.Show("Updated " + numRows + " rows", "Success"); 
    northwindDataSet.AcceptChanges();

    This code posts the changes by using the Update method of the productsTableAdapter object. When the changes have been applied, the user is told how many rows were affected, and the AcceptChanges method marks the changes as permanent in the DataSet. Notice that this code is all encapsulated within a try/catch block to handle any errors.

    IMPORTANT
    If another user has already changed one or more rows being updated, the Update method will detect this situation and throw an exception. You can then decide how to handle this conflict in your application. For example, you could give the user the option to update the database anyway and overwrite the other user's updates, or discard the conflicting changes and refresh the DataSet with the new data from the database.
  2. Build and run the program. When the Products Maintenance form appears, change the values in the ProductName and SupplierID columns of the first two rows, and then click Save.

    The changes are made to the database and a message tells you that two rows were updated.

  3. Close the form and run the application again.

    The new product name and supplier for the first two rows appear, proving that the data was saved to the database.

  4. Close the form and return to the Visual Studio 2005 programming environment.


Previous Page
Next Page