422 Thinking in C# www.ThinkingIn.NET
relational table data and XML) as well as making it much easier to program widely-
distributed database applications. However, this model increases the possibility
that two users will make incompatible modifications to related data – they’ll both
reserve the last seat on the flight, one will mark an issue as resolved while the other
will expand the scope of the investigation, etc. So even a minimal introduction to
ADO.NET requires some discussion of the issues of concurrency violations.
Getting a handle on data with DataSet
The DataSet class is the root of a relational view of data. A DataSet has
DataTables, which have DataColumns that define the types in DataRows. The
relational database model was introduced by Edgar F. Codd in the early 1970s. The
concept of tables storing data in rows in strongly-typed columns may seem to be
the very definition of what a database is, but Codd’s formalization of these concepts
and others such such as normalization (a process by which redundant data is
eliminated and thereby ensuring the correctness and consistency of edits) was one
of the great landmarks in the history of computer science.
While normally one creates a DataSet based on existing data, it’s possible to create
one from scratch, as this example shows:
//:c10:BasicDataSetOperations.cs
using System;
using System.Data;
class BasicDataSetOperations {
public static void Main(string[] args){
DataSet ds = BuildDataSet();
PrintDataSetCharacteristics(ds);
}
private static DataSet BuildDataSet() {
DataSet ds = new DataSet("MockDataSet");
Console.WriteLine(
"Column \"{0}\" contains data of type {1}",
col.ColumnName, col.DataType);
}
Console.WriteLine(
"The table contains {0} rows",
table.Rows.Count);
foreach(DataRow r in table.Rows){
Console.Write("Row Data: ");
foreach(DataColumn col in table.Columns){
string colName = col.ColumnName;
Console.Write("[{0}] = {1}",
colName, r[colName]);
}
Console.WriteLine();
}
}
}
}///:~
The .NET classes related to DataSets are in the System.Data namespace, so
naturally we have to include a using statement at the beginning of the program.
424 Thinking in C# www.MindView.net
The Main( ) method is straightforward: it calls BuildDataSet( ) and passes the
object returned by that method to another static method called
PrintDataSetCharacteristics( ).
BuildDataSet( ) introduces several new classes. First comes a DataSet, using a
constructor that allows us to simultaneously name it “MockDataSet.” Then, we
auTable.Rows.Add(larryRow);
After creating another row to contain Bruce’s name, the DataSet is returned to the
Main( ) method, which promptly passes it to PrintDataSetCharacteristics( ).
The output is:
DataSet "MockDataSet" has 1 tables
Table "Authors" has 1 columns
Column "Name" contains data of type System.String
The table contains 2 rows
Row Data: [Name] = Larry
Row Data: [Name] = Bruce
Connecting to a database
The task of actually moving data in and out of a store (either a local file or a
database server on the network) is the task of the IDbConnection interface.
Specifying which data (from all the tables in the underlying database) is the
responsibility of objects which implement IDbCommand. And bridging the gap
between these concerns and the concerns of the DataSet is the responsibility of the
IDbAdapter interface.
Thus, while DataSet and the classes discussed in the previous example
encapsulate the “what” of the relational data, the IDataAdapter, IDbCommand,
and IDbConnection encapsulate the “How”:
What How
DataColumn
DataRow
IDbCommand
IDbConnection
DataTable
1 *1 *
0 *0 *
IDataAdapter
dataset:
//:c10:DBConnect.cs
using System;
using System.Data;
using System.Data.OleDb;
class BasicDataSetOperations {
public static void Main(string[] args){
DataSet ds = Employees("Nwind.mdb");
Console.WriteLine(
"DS filled with {0} rows",
ds.Tables[0].Rows.Count);
}
private static DataSet Employees(string fileName){
OleDbConnection cnctn = new OleDbConnection();
cnctn.ConnectionString=
"Provider=Microsoft.JET.OLEDB.4.0;" +
"data source=" + fileName;
DataSet ds = null;
try {
Chapter 10: Collecting Your Objects 427
cnctn.Open();
string selStr = "SELECT * FROM EMPLOYEES";
IDataAdapter adapter =
new OleDbDataAdapter(selStr, cnctn);
ds = new DataSet("Employees");
adapter.Fill(ds);
DataSet? The answer is actually not defined at the level of IDataAdapter. The
428 Thinking in C# www.MindView.net
OleDbAdapter supports several possibilities, including automatically filling the
DataSet with all, or a specified subset, of records in a given table. The
DBConnect example shows the use of Structured Query Language (SQL), which is
probably the most general solution. In this case, the SQL query
SELECT * FROM
EMPLOYEES
retrieves all the columns and all the data in the EMPLOYEES table of
the database.
The OleDbDataAdapter has a constructor which accepts a string (which it
interprets as a SQL query) and an IDbConnection. This is the constructor we use
and upcast the result to IDataAdapter.
Now that we have our open connection to the database and an IDataAdapter, we
create a new DataSet with the name “Employees.” This empty DataSet is passed
in to the IDataAdapter.Fill( ) method, which executes the query via the
IDbConnection, adds to the passed-in DataSet the appropriate DataTable and
DataColumn objects that represent the structure of the response, and then
creates and adds to the DataSet the DataRow objects that represent the results.
The IDbConnection is Closed within a finally block, just in case an Exception
was thrown sometime during the database operation. Finally, the filled DataSet is
returned to Main( ), which dutifully reports the number of employees in the
Northwind database.
Fast reading with IDataReader
The preferred method to get data is to use an IDataAdapter to specify a view into
the database and use IDataAdapter.Fill( ) to fill up a DataSet. An alternative, if
all you want is a read-only forward read, is to use an IDataReader. An
IDataReader is a direct, connected iterator of the underlying database; it’s likely
to be more efficient than filling a DataSet with an IDataAdapter, but the
} finally {
rdr.Close();
cnctn.Close();
}
}
}///:~
The EnumerateEmployees( ) method starts like the code in the DBConnect
example, but we do not upcast the OleDbConnection to IDbConnection for
reasons we’ll discuss shortly. The connection to the database is identical, but we
declare an IDataReader rdr and initialize it to null before opening the database
connection; this is so that we can use the finally block to Close( ) the
IDataReader as well as the OleDbConnection.
After opening the connection to the database, we create an OleDbCommand
which we upcast to IDbCommand. In the case of the OleDbCommand
constructor we use, the parameters are a SQL statement and an
OleDbConnection (thus, our inability to upcast in the first line of the method).
The next line, rdr = sel.ExecuteReader( ), executes the command and returns a
connected IDataReader. IDataReader.Read( ) reads the next line of the
query’s result, returning false when it runs out of rows. Once all the data is read,
the method enters a finally block, which severs the IDataReader’s connection
with rdr.Close( ) and then closes the database connection entirely with
cnctn.Close( ).
430 Thinking in C# www.ThinkingIn.NET
CRUD with ADO.NET
With DataSets and managed providers in hand, being able to create, read, update,
and delete records in ADO.NET is near at hand. Creating data was covered in the
BasicDataSetOperations example – use DataTable.NewRow( ) to generate
an appropriate DataRow, fill it with your data, and use DataTable.Rows.Add( )
Not only would it please the hard drive manufacturers, it would provide a way around the
second law of thermodynamics. See, for instance, Chapter 10: Collecting Your Objects 431
5. Ben submits the change to the database. Because Ann’s update happened
before Ben’s update, Ben receives a DBConcurrencyException. The
database does not accept Ben’s change.
6. Charlie selects a flight and submits the change. Because the row hasn’t
changed since Charlie read the data, Charlie’s request succeeds.
It is impossible to give even general advice as to what to do after receiving a
DBConcurrencyException. Sometimes you’ll want to take the data and re-insert
it into the database as a new record, sometimes you’ll discard the changes, and
sometimes you’ll read the new data and reconcile it with your changes. There are
even times when such an exception indicates a deep logical flaw that calls for a
system shutdown.
This example performs all of the CRUD operations, rereading the database after the
update so that the subsequent deletion of the new record does not throw a
DBConcurrencyException:
//:c10:Crud.cs
using System;
using System.Data;
using System.Data.OleDb;
class Crud {
public static void Main(string[] args){
Crud myCrud = new Crud();
myCrud.ReadEmployees("NWind.mdb");
myCrud.Create();
myCrud.Update();
adapter.Update(emps);
}
private void Update(){
DataRow aRow = emps.Tables["Table"].Rows[0];
Console.WriteLine("First Name: "
+ aRow["FirstName"]);
string newName = null;
if (aRow["FirstName"].Equals("Nancy")) {
newName = "Adam";
} else {
newName = "Nancy";
}
aRow.BeginEdit();
aRow["FirstName"] = newName;
aRow.EndEdit();
Console.WriteLine("First Name: "
+ aRow["FirstName"]);
//Update only happens now
int iChangedRows = adapter.Update(emps);
Console.WriteLine("{0} rows updated",
iChangedRows);
}
private void Reread(){
adapter.Fill(emps);
Chapter 10: Collecting Your Objects 433
}
InsertCommand, DeleteCommand, and UpdateCommand properties of the
OleDbDataAdapter. These commands are needed to commit to the database
changes made in the DataSet.
The first four lines of method Create( ) show operations on the DataSet emps
that we’ve seen before – the use of Table.NewRow( ), and
DataRowCollection.Add( ) to manipulate the DataSet. The final line calls
IDataAdapter.Update( ), which attempts to commit the changes in the DataSet
434 Thinking in C# www.ThinkingIn.NET
to the backing store (it is this method which requires the SQL commands generated
by the OleDbCommandBuilder).
The method Update( ) begins by reading the first row in the emps DataSet. The
call to DataRow.BeginEdit( ) puts the DataRow in a “Proposed” state. Changes
proposed in a DataRow can either be accepted by a call to DataRow.EndEdit( )
or the AcceptChanges( ) method of either the DataRow, DataTable, or
DataSet. They can be cancelled by a call to DataRow.CancelEdit( ) or the
RejectChanges( ) methods of the classes.
After printing the value of the first row’s “FirstName” column, we put aRow in a
“Proposed” state and change the “FirstName” to “Fred.” We call CancelEdit( )
and show on the console that “Fred” is not the value. If the first name is currently
“Nancy” we’re going to change it to “Adam” and vice versa. This time, after calling
BeginEdit( ) and making the change, we call EndEdit( ). At this point, the data is
changed in the DataSet, but not yet in the database. The database commit is
performed in the next line, with another call to adapter.Update( ).
This call to Update( ) succeeds, as the rows operated on by the two calls to
Update( ) are different. If, however, we were to attempt to update either of these
two rows without rereading the data from the database, we would get the dread
DBConcurrencyException. Since deleting the row we added is exactly our
intent, Main( ) calls Reread( ), which in turn calls adapter.Fill( ) to refill the
emps DataSet.
mindshare in the enterprise market. What has gained mindshare is a hybrid model,
which combines the repetitive structure of tables and rows with a hierarchical
containment model that is closer to the object model. This hybrid model, embodied
in XML, does not directly support the more complicated concepts of relational joins
or object inheritance, but is a good waypoint on the road to object databases. We’ll
discuss XML in more detail in Chapter 17 and revisit ADO.NET in our discussion of
data-bound controls in Chapter 14.
Summary
To review the tools in the .NET Framework that collect objects:
An array associates numerical indices to objects. It holds objects of a known type so
that you don’t have to cast the result when you’re looking up an object. It can be
multidimensional in two ways – rectangular or jagged. However, its size cannot be
changed once you create it.
An IList holds single elements, an IDictionary holds key-value pairs, and a
NameObjectCollectionBase holds string-Collection pairs.
Like an array, an IList also associates numerical indices to objects—you can think
of arrays and ILists as ordered containers. An IDictionary overloads the bracket
operator of the array to make it easy to access values, but the underlying
implementation is not necessarily ordered.
Most collections automatically resize themselves as you add elements, but the
BitArray needs to be explicitly sized.
436 Thinking in C# www.MindView.net
ICollections hold only object references, so primitives are boxed and unboxed
when stored. With the exception of type-specific containers in
System.Collections.Specialized and those you roll yourself, you must always cast
the result when you pull an object reference out of a container. Type-specific
container classes will be supported natively by the .NET run-time sometime in the
future.
Data structures have inherent characteristics distinct from the data that is stored in
container with an ArrayList holding strings.
5. Create a class containing two string objects, and make it comparable so
that the comparison only evaluates the first string. Fill an array and an
ArrayList with objects of your class. Demonstrate that sorting works
properly.
6. Modify the previous exercise so that an alphabetic sort is used.
7. Create a custom indexer for maze running that implements breadth-first
traversal. For every non-visited tunnel out of a room, go to the next room.
If it’s the end, stop traversing. If it’s not the end, return to the original
room and try the next option. If none of the rooms out of the original room
are the final room, investigate the rooms that are two corridors distant
from the original room.
8. Modify the maze-running challenge so that each tunnel traversed has a
weight varying from 0 to 1. Use your depth- and bread-first traversals to
discover the cheapest route from the beginning to the end.
9. (Challenging) Write a maze-generating program that makes mazes
consisting of hundreds or thousands of rooms and tunnels. Find an
efficient way to determine the minimum traversal cost. If you can’t come
up with an efficient way to solve it, prove that there is no efficient way
3
.
10. Write a program to read and write to tables in the Northwind database
other than Employees.
11. Write a program to CRUD data stored in a SQL Server database.
12. (Challenging) Investigate applications of wavelets in domains such as
compression, database retrieval, and signal processing. Develop efficient
tools for investigating wavelet applications
4
.
decision shouldn’t be made at the lowest level (network games, for instance, often
have data of varying importance, some of which must be acknowledged and some
which would be worthless by the time a retry could be made). On the other hand,
a method may have a problem because something is awry with the way it is being
used – perhaps a passed-in parameter has an invalid value (a PrintCalendar
method is called for the month “Eleventember”) or perhaps the method can only
be meaningfully called when the object is in a different state (for instance, a
Cancel method is called when an Itinerary object is not in a “booked” state).
These misuse situations are tricky because there is no way in C# to specify a
method’s preconditions and postconditions as an explicit contract – a way in
source code to say “if you call me with x, y, and z satisfied, I will guarantee that
when I return condition a, b, and c will be satisfied (assuming of course that all
440 Thinking in C# www.MindView.net
the methods I call fulfill their contractual obligations with me).” For instance,
.NET’s Math class has a square root function that takes a double as its parameter.
Since .NET does not have a class to represent imaginary numbers, this function
can only return a meaningful answer if passed a positive value. If this method is
called with a negative value, is that an exceptional condition or a disappointing,
but predictable, situation? There’s no way to tell from the method’s signature:
double Math.Sqrt(double d);
Although preconditions and postconditions are not explicit in C# code, you
should always think in terms of contracts while programming and document pre-
and postconditions in your method’s param and returns XML documentation.
The .NET library writers followed this advice and the documentation for
Math.Sqrt( ) explain that it will return a NaN (Not A Number) value if passed a
negative parameter.
There is no hard-and-fast rule to determine what is an exceptional condition and
what is reasonably foreseeable. Returning a special “invalid value” such as does
command. Perhaps one level of code can go through a sequence of options and
retry, but if those fail, can give up and propagate the code to a higher level of
abstraction, which may perform a clean shutdown. Second, exceptions clean up
error handling code. Instead of checking for a particular rare failure and dealing
with it at multiple places in your program, you no longer need to check at the
point of the method call (since the exception will propagate right out of the
problem area to a block dedicated to catching it). And, you need to handle the
problem in only one place, the so-called exception handler. This saves you code,
and it separates the code that describes what you want to do from the code that is
executed when things go awry. In general, reading, writing, and debugging code
become much clearer with exceptions than with alternative ways of error
handling.
This chapter introduces you to the code you need to write to properly handle
exceptions, and the way you can generate your own exceptions if one of your
methods gets into trouble.
Basic exceptions
When you throw an exception, several things happen. First, the exception object
is created in the same way that any C# object is created: on the heap, with new.
Then the current path of execution (the one you couldn’t continue) is stopped and
the reference for the exception object is ejected from the current context. At this
point the exception handling mechanism takes over and begins to look for an
appropriate place to continue executing the program. This appropriate place is
the exception handler, whose job is to recover from the problem so the program
can either retry the task or cleanup and propagate either the original Exception
or, better, a higher-abstraction Exception.
As a simple example of throwing an exception, consider an object reference called
t that is passed in as a parameter to your method. Your design contract might
require as a precondition that t refer to a valid, initialized object. Since C# has no
syntax for enforcing preconditions, some other piece of code may pass your
method a null reference and compile with no problem. This is an easy
method is designed to return. A simplistic way to think about exception handling
is as an alternate return mechanism, although you get into trouble if you take that
analogy too far. You can also exit from ordinary scopes by throwing an exception.
But a value is returned, and the method or scope exits.
Any similarity to an ordinary return from a method ends here, because where you
return is someplace completely different from where you return for a normal
method call. (You end up in an appropriate exception handler that might be miles
away—many levels away on the call stack—from where the exception was
thrown.)
Chapter 11: Error Handling with Exceptions 443
Typically, you’ll throw a different class of exception for each different type of
error. The information about the error is represented both inside the exception
object and implicitly in the type of exception object chosen, so someone in the
bigger context can figure out what to do with your exception. (Often, it’s fine that
the only information is the type of exception object, and nothing meaningful is
stored within the exception object.)
Catching an exception
If a method throws an exception, it must assume that exception is “caught” and
dealt with. One of the advantages of C#’s exception handling is that it allows you
to concentrate on the problem you’re trying to solve in one place, and then deal
with the errors from that code in another place.
To see how an exception is caught, you must first understand the concept of a
guarded region, which is a section of code that might produce exceptions, and
which is followed by the code to handle those exceptions.
The try block
If you’re inside a method and you throw an exception (or another method you call
within this method throws an exception), that method will exit in the process of
throwing. If you don’t want a throw to exit the method, you can set up a special
block within that method to capture the exception. This is called the try block
Each catch clause (exception handler) is like a little method that takes one and
only one argument of a particular type. The identifier (id1, id2, and so on) can be
used inside the handler, just like a method argument. Sometimes you never use
the identifier because the type of the Exception gives you enough information to
diagnose and respond to the exceptional condition. In that situation, you can
leave the identifier out altogether as is done with the Type3 catch block above.
The handlers must appear directly after the try block. If an exception is thrown,
the exception handling mechanism goes hunting for the first handler with an
argument that matches the type of the exception. Then it enters that catch clause,
and the exception is considered handled. The search for handlers stops once the
catch clause is finished. Only the matching catch clause executes; it’s not like a
switch statement in which you need a break after each case.
Note that, within the try block, a number of different method calls might generate
the same exception, but you need only one handler.
Supertype matching
Naturally, the catch block will match a type descended from the specified type
(since inheritance is an is-a type relationship). So the line
}catch(Exception ex){ … }
will match any type of exception. A not uncommon mistake in Java code is an
overly-general catch block above a more specific catch block, but the C# compiler
detects such mistakes and will not allow this mistake.
Exceptions have a helplink
The Exception class contains a string property called HelpLink. This property
is intended to hold a URI and the .NET Framework SDK documentation suggests
that you might refer to a helpfile explaining the error. On the other hand, as we’ll
Chapter 11: Error Handling with Exceptions 445
discuss in Chapter 18, a URI is all you need to call a Web Service. One can
public static void Main() {
SimpleExceptionDemo sed =
new SimpleExceptionDemo();
try {
sed.F();
} catch (SimpleException ) {
Console.Error.WriteLine("Caught it!");
446 Thinking in C# www.ThinkingIn.NET
}
}
} ///:~
When the compiler creates the default constructor, it automatically (and
invisibly) calls the base-class default constructor. As you’ll see, the most
important thing about an exception is the class name, so most of the time an
exception like the one shown above is satisfactory.
Here, the result is printed to the console standard error stream by writing to
System.Console.Error. This stream can be redirected to any other
TextWriter by calling System.Console.SetError( ) (note that this is
“asymmetric” – the Error property doesn’t support assignment, but there’s a
SetError( ). Why would this be?).
Creating an exception class that overrides the standard constructors is also quite
simple:
//:c11:FullConstructors.cs
using System;
class MyException : Exception {
public MyException() : base() {}
public MyException(string msg) : base(msg) {}