
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf
802 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Figure 23-8. Updating the UI to enable removal of rows from the underlying DataTable
The following logic behind the new Button’s Click event handler removes the user-specified row from your in-memory DataTable. Note that we are wrapping the deletion logic within a try scope, to account for the possibility the user has entered a row number not currently in the
DataGridView:
// Remove this row from the DataRowCollection.
private void btnRemoveRow_Click (object sender, EventArgs e)
{
try
{
inventoryTable.Rows[(int.Parse(txtRowToRemove.Text))].Delete(); inventoryTable.AcceptChanges();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The Delete() method might have been better named MarkedAsDeletable(), as the row is not literally removed until the DataTable.AcceptChanges() method is called. In effect, the Delete() method simply sets a flag that says, “I am ready to die when my table tells me to.” Also understand that if a row has been marked for deletion, a DataTable may reject the delete operation via RejectChanges(). We have no need to do so for this example; however, we could update our code base as follows:
// Mark a row as deleted, but reject the changes.
private void btnRemoveRow_Click (object sender, EventArgs e)
{
...
inventoryTable.Rows[(int.Parse(txtRemove.Text))].Delete();
// Do more work
...

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
803 |
// Restore previous RowState value. inventoryTable.RejectChanges();
...
}
You should now be able to run your application and specify a row to delete from the DataTable, based on a given row of the DataGridView (note the internal row collection is zero based). As you remove DataRow objects from the DataTable, you will notice that the grid’s UI is updated immediately, as it is bound to the state of the object. Recall, however, that the row is still literally in the DataTable, but the grid chooses not to display it because of the RowState value.
Selecting Rows Based on Filter Criteria
Many data-centric applications require the need to view a small subset of a DataTable’s data, as specified by some sort of filtering criteria. For example, what if you wish to see only a certain make of automobile from the in-memory DataTable (such as only BMWs)? The Select() method of the DataTable class provides this very functionality. Using the Select() method, you are able to specify a search criteria that supports a syntax intentionally designed to model a normal SQL query. This method will return an array of DataRow objects that represent each entry that matches the criteria.
To illustrate, update your UI once again, this time allowing users to specify a string that represents the make of the automobile they are interested in viewing (see Figure 23-9) using a new
TextBox (named txtMakeToView) and a new Button (named btnDisplayMakes).
Figure 23-9. Updating the UI to enable row filtering
The Select() method has been overloaded a number of times to provide different selection semantics. At its most basic level, the parameter sent to Select() is a string that contains some conditional operation. To begin, observe the following logic for the Click event handler of your new button:
private void btnDisplayMakes_Click(object sender, EventArgs e)
{
// Build a filter based on user input.
string filterStr = string.Format("Make= '{0}'", txtMakeToView.Text);

804 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
// Find all rows matching the filter.
DataRow[] makes = inventoryTable.Select(filterStr);
// Show what we got! if (makes.Length == 0)
MessageBox.Show("Sorry, no cars...", "Selection error!"); else
{
string strMake = null;
for (int i = 0; i < makes.Length; i++)
{
DataRow temp = makes[i];
strMake += temp["PetName"] + "\n";
}
// Now show all matches in a message box.
MessageBox.Show(strMake,
string.Format("{0} type(s):", txtMakeToView.Text));
}
}
Here, you first build a simple filter based on the value in the associated TextBox. If you specify BMW, your filter is Make = 'BMW'. When you send this filter to the Select() method, you get back an array of DataRow types that represent each row that matches the filter (see Figure 23-10).
Figure 23-10. Displaying filtered data
Again, filtering logic is based on standard SQL syntax. To illustrate, assume you wish to obtain the results of the previous Select() invocation alphabetically based on pet name. In terms of SQL, this translates into a sort based on the PetName column. Luckily, the Select() method has been overloaded to send in a sort criterion, as shown here:
// Sort by PetName.
makes = inventoryTable.Select(filterStr, "PetName");
If you want the results in descending order, call Select() as so:
// Return results in descending order.
makes = inventoryTable.Select(filterStr, "PetName DESC");

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
805 |
In general, the sort string contains the column name followed by ASC (ascending, which is the default) or DESC (descending). If need be, multiple columns can be separated by commas. Finally, understand that a filter string can be composed of any number of relational operators. For example, what if you want to find all cars with an ID greater than 5? Here is a helper function that does this very thing:
private void ShowCarsWithIdGreaterThanFive()
{
// Now show the pet names of all cars with ID greater than 5.
DataRow[] properIDs;
string newFilterStr = "ID > 5";
properIDs = inventoryTable.Select(newFilterStr); string strIDs = null;
for(int i = 0; i < properIDs.Length; i++)
{
DataRow temp = properIDs[i]; strIDs += temp["PetName"]
+ " is ID " + temp["ID"] + "\n";
}
MessageBox.Show(strIDs, "Pet names of cars where ID > 5");
}
Updating Rows
The final aspect of the DataTable you should be aware of is the process of updating an existing row with new values. One approach is to first obtain the row(s) that match a given filter criterion using the Select() method. Once you have the DataRow(s) in question, modify them accordingly. For example, assume you have a new Button on your form-derived type named btnChangeBeemersToYugos that (when clicked) searches the DataTable for all rows where Make is equal to BMW. Once you identify these items, you change the Make from BMW to Yugo:
// Find the rows you want to edit with a filter.
private void btnChangeBeemersToYugos_Click(object sender, EventArgs e)
{
// Make sure user has not lost his or her mind. if (DialogResult.Yes ==
MessageBox.Show("Are you sure?? BMWs are much nicer than Yugos!", "Please Confirm!", MessageBoxButtons.YesNo))
{
// Build a filter.
string filterStr = "Make='BMW'"; string strMake = string.Empty;
// Find all rows matching the filter.
DataRow[] makes = inventoryTable.Select(filterStr);
// Change all Beemers to Yugos!
for (int i = 0; i < makes.Length; i++)
{
makes[i]["Make"] = "Yugo";
}
}
}
The DataRow class also provides the BeginEdit(), EndEdit(), and CancelEdit() methods, which allow you to edit the content of a row while temporarily suspending any associated validation rules.

806CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
In the previous logic, each row was validated with each assignment. (Also, if you handle any events from the DataRow, they fire with each modification.) When you call BeginEdit() on a given DataRow, the row is placed in edit mode. At this point you can make your changes as necessary and call either EndEdit() to commit these changes or CancelEdit() to roll back the changes to the original version, for example:
private void UpdateSomeRow()
{
//Assume you have obtained a row to edit.
//Now place this row in edit mode.
rowToUpdate.BeginEdit();
// Send the row to a helper function, which returns a Boolean. if( ChangeValuesForThisRow( rowToUpdate) )
rowToUpdate.EndEdit(); |
// |
OK! |
else |
|
|
rowToUpdate.CancelEdit(); |
// |
Forget it. |
}
Although you are free to manually call these methods on a given DataRow, these members are automatically called when you edit a DataGridView widget that has been bound to a DataTable. For example, when you select a row to edit from a DataGridView, that row is automatically placed in edit mode. When you shift focus to a new row, EndEdit() is called automatically.
Working with the DataView Type
In database nomenclature, a view object is an alternative representation of a table (or set of tables). For example, using Microsoft SQL Server, you could create a view for your Inventory table that returns a new table containing automobiles only of a given color. In ADO.NET, the DataView type allows you to programmatically extract a subset of data from the DataTable into a stand-alone object.
One great advantage of holding multiple views of the same table is that you can bind these views to various GUI widgets (such as the DataGridView). For example, one DataGridView might be bound to a DataView showing all autos in the Inventory, while another might be configured to display only green automobiles.
To illustrate, update the current UI with an additional DataGridView type named dataGridColtsView and a descriptive Label. Next, define a member variable named coltsOnlyView of type DataView:
public partial class MainForm : Form
{
// View of the DataTable.
DataView coltsOnlyView;
...
}
Now, create a new helper function named CreateDataView(), and call this method within the form’s default constructor directly after the DataTable has been fully constructed, as shown here:
public MainForm()
{
...
//Make a data table.
CreateDataTable();
//Make a view.
CreateDataView();
}

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
807 |
Here is the implementation of this new helper function. Notice that the constructor of each DataView has been passed the DataTable that will be used to build the custom set of data rows.
private void CreateDataView()
{
//Set the table that is used to construct this view. coltsOnlyView = new DataView(inventoryTable);
//Now configure the views using a filter. coltsOnlyView.RowFilter = "Make = 'Colt'";
//Bind to the new grid. dataGridColtsView.DataSource = coltsOnlyView;
}
As you can see, the DataView class supports a property named RowFilter, which contains the string representing the filtering criteria used to extract matching rows. Once you have your view established, set the grid’s DataSource property accordingly. Figure 23-11 shows the completed application in action.
Figure 23-11. Displaying a unique view of our data
One Final UI Enhancement: Rendering Row Numbers
Before wrapping up this section, let’s add one small enhancement to the current application. Currently, the grids on this window do not provide any sort of visual cue to the end user about which row number he or she is editing (or possibly deleting). If you wish to display row numbers on a DataGridView, your first step is to handle the RowPostPaint event on the grid itself. This event will fire after all of the data in the grid’s cells have been rendered in the UI, and it gives you a chance to finalize the graphical look and feel of the rows before they are presented to the user.

808 CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER
Once you have handled this event, you are able to take the incoming
DataGridViewRowPostPaintEventArgs parameter to obtain a Graphics object. As you will see in Chapter 27, the Graphics type is part of the GDI+ API, which is the native Windows Forms 2D rendering toolkit. Using this Graphics object, you are able invoke various methods (such as DrawString(), which is appropriate for this example) to render content. Again, Chapter 27 will examine GDI+; however, here is an implementation of the RowPostPaint event that will paint the numbers of each row on the carInventoryGridView object (you could, of course, handle the same event on the dataGridColtsView object for a similar effect):
void carInventoryGridView_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
{
//Paint row numbers using a solid brush, in the
//native font on the current row style.
using (SolidBrush b = new SolidBrush(Color.Black))
{
e.Graphics.DrawString((e.RowIndex).ToString(), e.InheritedRowStyle.Font, b, e.RowBounds.Location.X + 15, e.RowBounds.Location.Y + 4);
}
}
■Source Code The WindowsFormsDataTableViewer project is included under the Chapter 23 subdirectory.
Filling DataSet/DataTable Objects Using
Data Adapters
Now that you understand the ins and outs of manipulating ADO.NET DataSets manually, let’s turn our attention to the topic of data adapter objects. Recall that data adapter objects are used to fill a DataSet with DataTable objects, and they can also send modified DataTables back to the database for processing. Table 23-8 documents the core members of the DbDataAdapter base class, the common parent to every data adapter object.
Table 23-8. Core Members of the DbDataAdapter Class
Members |
Meaning in Life |
Fill() |
Fills a given table in the DataSet with some number of records based on the |
|
command object–specified SelectCommand. |
SelectCommand |
Establish SQL commands that will be issued to the data store when the Fill() |
InsertCommand |
and Update() methods are called. |
UpdateCommand |
|
DeleteCommand |
|
Update() |
Updates a DataTable using command objects within the InsertCommand, |
|
UpdateCommand, or DeleteCommand property. The exact command that is executed |
|
is based on the RowState value for a given DataRow in a given DataTable (of a |
|
given DataSet). |
|
|

CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
809 |
First of all, notice that a data adapter defines four properties (SelectCommand, InsertCommand, UpdateCommand, and DeleteCommand), each of which operates upon discrete command objects. When you create the data adapter object for your particular data provider (e.g., SqlDataAdapter), you are able to pass in a string type that represents the command text used by the SelectCommand’s command object. However, the remaining three command objects (used by the InsertCommand, UpdateCommand, and DeleteCommand properties) must be configured manually.
Assuming each of the four command objects has been properly configured, you are then able to call the Fill() method to obtain a DataSet (or a single DataTable, if you wish). To do so, the data adapter will use whichever command object is found via the SelectCommand property. In a similar manner, when you wish to pass a modified DataSet (or DataTable) object back to the database for processing, you can call the Update() method, which will make use of any of the remaining command objects based on the state of each row in the DataTable (more details in just a bit).
One of the strangest aspects of working with a data adapter object is the fact that you are never required to open or close a connection to the database. Rather, the underlying connection to the database is managed on your behalf. However, you will still need to supply the data adapter with a valid connection object or a connection string (which will be used to build a connection object internally) to inform the data adapter exactly which database you wish to communicate with.
A Simple Data Adapter Example
Before we add new functionality to the AutoLotDAL.dll assembly created in Chapter 22, let’s begin with a very simple example that fills a DataSet with a single table using an ADO.NET data adapter object. Create a new Console Application named FillDataSetWithSqlDataAdapter, and import the System.Data and System.Data.SqlClient namespaces into your initial C# code file.
Now, update your Main() method as follows (for reasons of simplicity, feel free to make use of a hard-coded connection string, as shown here):
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Data Adapters *****\n");
// Hard-coded connection string.
string cnStr = "Integrated Security = SSPI;Initial Catalog=AutoLot;" + @"Data Source=(local)\SQLEXPRESS";
//Caller creates the DataSet object.
DataSet ds = new DataSet("AutoLot");
//Inform adapter of the Select command text and connection.
SqlDataAdapter dAdapt =
new SqlDataAdapter("Select * From Inventory", cnStr);
//Fill our DataSet with a new table, named Inventory. dAdapt.Fill(ds, "Inventory");
//Display contents of DataSet.
PrintDataSet(ds);
Console.ReadLine();
}
Notice that the data adapter has been constructed by specifying a string literal that will map to the SQL Select statement. This value will be used to build a command object internally, which can be later obtained via the SelectCommand property.


CHAPTER 23 ■ ADO.NET PART II: THE DISCONNECTED LAYER |
811 |
Figure 23-12. DataTable objects with custom mappings
Revisiting AutoLotDAL.dll
To illustrate the process of using a data adapter to push modifications in a DataTable back to the database for processing, we will now update the AutoLotDAL.dll assembly created back in Chapter 22 to include a new namespace (named AutoLotDisconnectedLayer). This namespace will contain a new class, InventoryDALDisLayer, that will make use of a data adapter to interact with a DataTable.
Defining the Initial Class Type
Open the AutoLotDAL project in Visual Studio 2008, insert a new class type named InventoryDALDisLayer using the Project Add New Item menu option, and ensure you have a public class type in your new code file. Unlike the connection-centric InventoryDAL type, this new class will not need to provide custom open/close methods, as the data adapter will handle the details automatically.
To begin, add a custom constructor that sets a private string variable representing the connection string. As well, define a private SqlDataAdapter member variable, which will be configured by calling a (yet to be created) helper method called ConfigureAdapter(), which takes a SqlDataAdapter output parameter:
public class InventoryDALDisLayer
{
// Field data.
private string cnString = string.Empty; private SqlDataAdapter dAdapt = null;
public InventoryDALDisLayer(string connectionString)
{
cnString = connectionString;
// Configure the SqlDataAdapter.
ConfigureAdapter(out dAdapt);
}
}