Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

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.

810 CHAPTER 23 ADO.NET PART II: THE DISCONNECTED LAYER

Next, notice that it is the job of the caller to create an instance of the DataSet type, which is passed into the Fill() method. Optionally, the Fill() method can be passed as a second argument a string name that will be used to set the TableName property of the new DataTable (if you do not specify a table name, the data adapter will simply name the table Table). While in most cases the name you assign a DataTable will be identical to the name of the physical table in the relational database, this is not required.

Note The Fill() method returns an integer that represents the number of rows returned by the SQL query.

Finally, notice that nowhere in the Main() method are you explicitly opening or closing the connection to the database. The Fill() method of a given data adapter has been preprogrammed to open and then close the underlying connection before returning from the Fill() method. Therefore, when you pass the DataSet to the PrintDataSet() method (implemented earlier in this chapter), you are operating on a local copy of disconnected data, incurring no round-trips to

fetch the data.

Mapping Database Names to Friendly Names

As mentioned earlier, database administrators tend to create table and column names that can be less than friendly to end users (e.g., au_id, au_fname, au_lname, etc.). The good news is that data adapter objects maintain an internal strongly typed collection (named DataTableMappingCollection) of System.Data.Common.DataTableMapping types. This collection can be accessed via the TableMappings property of your data adapter object.

If you so choose, you may manipulate this collection to inform a DataTable which “display names” it should use when asked to print its contents. For example, assume that you wish to map the table name Inventory to Current Inventory for display purposes. Furthermore, say you wish to display the CarID column name as Car ID (note the extra space) and the PetName column name as Name of Car. To do so, add the following code before calling the Fill() method of your data adapter object (and be sure to import the System.Data.Common namespace to gain the definition of the

DataTableMapping type):

static void Main(string[] args)

{

...

// Now map DB column names to user-friendly names.

DataTableMapping custMap = dAdapt.TableMappings.Add("Inventory", "Current Inventory");

custMap.ColumnMappings.Add("CarID", "Car ID"); custMap.ColumnMappings.Add("PetName", "Name of Car"); dAdapt.Fill(myDS, "Inventory");

...

}

If you were to run this program once again, you would find that the PrintDataSet() method now displays the “friendly names” of the DataTable and DataRow objects, rather than the names established by the database schema. Figure 23-12 shows the output of the current example.

Source Code The FillDataSetWithSqlDataAdapter project is included under the Chapter 23 subdirectory.

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);

}

}