Tall Software Ltd

Ado.Net - this page teaches the basics and reminds on some best practices
(current on Nov 2002)
Hints and snippets for C# coders

Data access and display in .Net Forms

All these notes are taken from the microsoft docn. It is a crib sheet for data access and forms. If you want to go to the original then please follow the links, or just go to the bottom of the page and follow the links there.

You can use datasets or datacommands. This page only worries about datasets. But best practices recons that stateless components/code such as web services and web pages should probably use datacommands, while GUI's should use DataSet's.

Before diving into the text, take a glance at an example file: TheMain.cs

The architecture

Connections connect to the database using a connection string. A dataset is a description of tables and columns, relationships and pkeys. A dataadaptor needs a connection and a select query (or set of queries). Once a dataadaptor has the select query it can Fill the dataset. You can now access the content of the dataset by table name, row number and column name. You can use a command builder to generate the update and delete commands of the dataadaptor.

Additionally, you can bind properties of form components to the column value of the 'current' row within a table in a data set. The current row is set using the CurrencyManager. Every form keeps CurrencyManagers for every bound dataset within it. Lists and so on can be bound to the dataset tables, text boxes and so on can be bound to columns. You can even display a column, preserving a mapping to the value, so the user sees a nice view of the rows. All of this is built in and easy to use (once you have done it two or three times).

The DataSet tied to the GUI components will keep a record of items which have changed, been added and been deleted. You can use the dataadaptor to comit the dataset back into the database - using the commandbuilder to create the sql needed.

Tying it all together

OK, there is loads of detail later in the page, but no single example anywhere of a full implementation. And I may get round to this. But until then here is some natural language showing what you must do.

create a form
create the components in the form

on_loading the form:
   Define a dataset using vs.net - table(s), constraints, fkeys, pkeys etc, any paging needed?
   Connect to the database using a connection string 
   Create a dataadaptor giving it the connection and a select statement 
   Use the dataadaptor to populate the dataset 
   Use a commandBuilder to set the insert,update and delete commands of the dataadaptor    
   Create a guicopy of the dataset 
   Add validating event handlers to column and row update events
   Use GUID's and NOT autoincrement pkeys otherwise you will need a stored proc for inserts.

Bind the form components to the guicopy of the form
Add event handlers to reload the guicopy (ie a revert button)
Add a store button event handler which:
  Starts a transaction
  creates a dataset for child tables first
  uses the dataadaptor to update the db
  completes the transaction
  accepts all changes to the local dataset
  Handles concurrency error and roles back the transaction if there are errors
Add other buttons and menus etc.

How to do it/the details/notes

DataSets

Visual Studio lets you do create DataSets graphically. These have XML schemas. The data sets are NOT tied to a database, they are simply a description of objects. So, VS.Net will create objects that match the XML schema. You must still handle filling the dataset with data.

Data Sets can contain more than one table. DataSets created with VS.Net are 'typed' which means you can access column names and they will be correctly typed - eg

// C# This accesses the CustomerID column in the first row of the Customers table.
string s = dsCustomersOrders1.Customers[0].CustomerID;

The equivalent 'un-typed' dataset access would be (you create untyped ones in code)

string s = (string) dsCustomersOrders1.Tables["Customers"].Rows[0]["CustomerID"];

Paging result sets

Data sets support paging.... use it! Well use it if you have a large data set... Paging is done via properties.

DataBase Connections

SQLConnection class need a connection string, and there are loads of formats for this. There are two kinds

SqlConnection is quicker!

// bad version - mixed mode sqlserver authentication, uid and passwd (blank sa account passwd no less!)
String con_str= "Server=192.168.0.16\\TALLPRIV;database=talltest;uid=sa;pwd=;";    
// Other version using integrated security.
String con_str2= "Provider=SQLOLEDB.1;Data Source=MySQLServer;Initial Catalog=NORTHWIND;Integrated Security=SSPI"; 

SqlConnection con = new SqlConnection(); con.ConnectionString=con_str;

Connection Open/Close:

Data Sets will open and close the connections as it needs to - you do not have to manage this. BUT, you may also open and close them yourself if you want. And ...

From the best practices stuff: "Always explicitly close your Connection or DataReader objects when you are finished using them." Which is kinda wierd because Ado.Net can does this for you automatically - by making a best guess. But stick with best practice and close them yourself!

Pooling:

Connection pooling is supported as part of the .Net framework automatically. "If you are using the SqlConnection class, connection pooling is managed implicitly, but also provides options that allow you to manage pooling yourself. For more information, see Connection Pooling for the SQL Server .NET Data Provider."

Transactions:

Connection objects support transactions with a BeginTransaction method that creates a transaction object (an OleDbTransaction or SqlTransaction object). The transaction object in turn supports methods that allow you to commit or roll back the transactions.

Here is the only example I found on msdn of transaction and dataadaptors. I changed it because theirs would never work, and I have yet to test this, because I've not seen anyone else do this....

//C# - a rubbish example from msdn, I do not believe it works.
public void RunSqlTransaction(SqlDataAdapter da, SqlConnection myConnection,    DataSet ds)
{
  myConnection.Open();
  SqlTransaction myTrans = myConnection.BeginTransaction();
  da.InsertCommand.Transaction = myTrans;
  da.UpdateCommand.Transaction = myTrans;
  da.DeleteCommand.Transaction = myTrans;
  try {
   da.Update(ds);
   myTrans.Commit();
   Console.WriteLine("Update successful.");
  } catch(Exception e) {
   try {
     myTrans.Rollback();
   } catch (SqlException ex) {
     if (myTrans.Connection != null) {
       Console.WriteLine("An exception of type " + ex.GetType() +
               " was encountered while attempting to roll back the transaction.");
     }
   }
   Console.WriteLine(e.ToString());
   Console.WriteLine("Update failed.");
 }
 myConnection.Close();
}

More notes I found are from this bit of msdn:

"For a .NET Framework object to participate in an automatic transaction, the .NET Framework class must be registered with Windows 2000 Component Services. However, not all transactions are automatic. The activities you perform when programming transactions depend on the transaction model you choose. The common language runtime supports both manual and automatic transaction models."

The automated transaction stuff is really really rubbish when compared with java - embedding it in the OS is bad IMHO. Take a read of it and weep. I guess this is why all ms projects resort to stored procs.

AutoIncrement pkeys - there is quite a debate with this issue, which I detail in another page.

This is not my own work, just a summary of others, look at the other page for details.

'when working with auto-incrementing columns at a data source and auto-incrementing columns in a DataSet, you create the column in the DataSet with an AutoIncrementStep of -1 and an AutoIncrementSeed of 0, as well as ensuring that your data source generates auto-incrementing identity values starting from 1 and incrementing with a positive step value. As a result, the DataSet generates negative numbers for auto-incremented values that do not conflict with the positive auto-increment values generated by the data source.'

However, this will not solve the problem of how to ensure your local dataset has the correct pkeys after insert. That is a problem solved by a trick shown below. The other way to do it is to use the update callback - as shown in the example code.

'Change the insert command text to include a select command as well, and make sure the insert command's UpdatedRowSource is set to UpdateRowSource.FirstReturnedRecord. You must also edit the datasourve to make sure that the column is not marked read only.

"INSERT INTO eg_tab(name, addr) VALUES (@name, " +
"@addr); SELECT id, name, addr" +
" FROM eg_tab WHERE (id =@@IDENTITY)";
If you're using SQL Server 7.0 or earlier, use @@IDENTITY instead of SCOPE_IDENTITY(). 
  For more information on the difference between these features, see SQL Server 
  Books OnLine.'

Null values for params

When sending a null value as a Parameter value in a command to the database, you cannot use null (Nothing in Visual Basic® .NET). Instead you need to use DBNull.Value. For example:

//C#
SqlParameter param = new SqlParameter("@Name", SqlDbType.NVarChar, 20);
param.Value = DBNull.Value;

DataAdaptors (note there is a wizard)

A dataadapter takes the connection and a query, and from this populates a dataset, or possibly many queries. Note that the query still has to be written even though you used vs.net to create the dataset by dragging tables etc.

string query = "SELECT id, description FROM CONSOURCE";
SqlDataAdapter da = new SqlDataAdapter(query, con);
da.Fill(dsCustomersOrders1, "CONSource");

The CommandBuilder

SQLCommandBuilder objects can be used to autogenerate update, delete commands etc. It does this from the selects - no joins please. You simply give it the data adaptor and it does its business.

SqlCommandBuilder bld = new SqlCommandBuilder(da);
da.InsertCommand = bld.GetInsertCommand();
da.UpdateCommand = bld.GetUpdateCommand();
da.DeleteCommand = bld.GetDeleteCommand();

System.Console.WriteLine(da.InsertCommand.CommandText);
System.Console.WriteLine(da.UpdateCommand.CommandText);
System.Console.WriteLine(da.DeleteCommand.CommandText);

Tieing data to the GUI

Windows Forms and data is pretty fine when using data sets!

Each form has a binding context which manages all the property and currency managers within it. A PropertyManager maps a property of a GUI component to a column value. A CurrencyManager records the position within a DataSet - ie the row we are on. You can have many CurrencyManagers, you can even create your own BindingContexts for sub-panels/tabs etc.

eg:

text1.DataBindings.Add(new Binding("Text", ds."Customers.Name");

This binds the Text property of a text box to the Name column of the Cusomers table.

this.BindingContext[dspubs1,"authors"].Position=1;

this -> the form

BindingContext -> managers all the CurrencyManager classes in the form, and we are indexing the authors table within the dspubs1 dataset. Setting the Position to 1. So any gui component bound to the authors table will update to the new row (i.e. position 1).

Other ways of binding data to Gui components:

// C#
comboBox1.DataSource = DataSet1.Customers;
comboBox1.DisplayMember = "FirstName";
this.BindingContext[dataSet1.Customers].Position = 1;

DataBinding for display to people!

Basically, lists and combo boses etc can display names of things, and keep a mapping internally to the pkey. Ands this is done using DataSource, DisplayMember and ValueMember properties, followed by adding a data binding. Take a look at the links for full info.

listBox1.DataBindings.Add("SelectedValue", OrderDetailsTable, "ItemID");

Updating datasets

When a dataset is bound to the GUI then you need do nothing. The GUI will update the dataset itself. But, as this is a crib sheet, here is how to do it in code (for typed datasets):

// C# update - with begin/end to turn off constraint checking
dsCustomers1.Customers[4].BeginEdit();
dsCustomers1.Customers[4].CompanyName = "Wingtip Toys";
dsCustomers1.Customers[4].City = "Buffalo";
dsCustomers1.Customers[4].EndEdit();

// C# Insert
DataRow anyRow = DatasetName.ExistingTable.NewRow();
anyRow.FirstName = "Jay";
anyRow.LastName = "Stevens";
ExistingTable.Rows.Add(anyRow);

// C# Delete
DsCustomers1.Customers.Rows[0].Delete();

You can add event handlers to track column, row and table changes. You can add validation to column, row and tables, you can accept changes to rows, tables and datasets. You can suspend checking, resume checking, merge datasets and so on.

Validating the updates (taken direct from msdn)

"By default, each change to a column therefore raises four events: first the ColumnChanging and ColumnChanged events for the specific column being changed, and then the RowChanging and RowChanged event. If multiple changes are being made to the row, the events will be raised for each change.

Note The data row's BeginEdit method turns off the RowChanging and RowChanged events after each individual column change. In that case, the event is not raised until the EndEdit method has been called, when the RowChanging and RowChanged events are raised just once. For more information, see Suspending Update Constraints.
The event you choose depends on how granular you want the validation to be. If it is important that you catch an error immediately when a column is changed, build validation using the ColumnChanging event. Otherwise, use the RowChanging event, which might result in catching several errors at once. Additionally, if your data is structured in such a way that the value of one column is validated based on the contents of another column then you should perform your validation during the RowChanging event."

Persisting the updates

When you want to persist the changes you have to Update the database, and then discard change info from the dataset - ie update, and then accept changes.

// C# - how to update and accept changes - noddy version (see below)
OleDbDataAdapter1.Update(Dataset1, "Customers");
Dataset1.Customers.AcceptChanges(); // or, if needed use Fill, or use the refresh the data set option in the adaptor

If your dataset contains multiple tables, you have to update them individually by calling the Update method of each data adapter separately. If the tables have a parent-child relationship, it is likely that you will have to send updates to the database in a particular order.

In many circumstances, after updating a data source, you will want to refresh the dataset by repopulating it. Doing so has several benefits:

It refreshes the timestamp on records in the dataset, in case you want to use the timestamp for concurrency control. You can refresh a dataset manually by calling an adapter's Fill method after calling its Update method. OR set the Refresh the DataSet option in the Advanced SQL Generations Options dialog box - means the adaptor uses the SELECT statement to refresh the dataset after the update has occurred. It is generated when you set the Refresh the DataSet option in the Advanced SQL Generations Options dialog box.

Example of updating the db :

// C# A better update function, with child tables first, and error handling
void Update_Attempt()
{ // start a transaction? How do you use transactions and data adaptors? Hmmmm. // none of their standard eg's shows transactions and dataadaptors. maybe ms are too thick for this? // take a look at my previous example with transactions.
// Set ds1 to contain only deleted records from the child table.
DataTable ds1 =
anyDataset.ChildTableName.GetChanges(DataRowState.Deleted);
// Set ds2 to contain only new records from the child table.
DataTable ds2 =
anyDataset.ChildTableName.GetChanges(DataRowState.Added);
// Set ds3 to contain only edited records from the child table.
DataTable ds3 =
anyDataset.ChildTableName.GetChanges(DataRowState.Modified);
 try {
   DataAdapter2.Update(ds1);
   DataAdapter1.Update(anyDataset, "ParentTable");
   DataAdapter2.Update(ds2);
   DataAdapter2.Update(ds3);
   anyDataset.AcceptChanges();
   ds1.Dispose();
   ds2.Dispose();
   ds3.Dispose();

 } catch (DBConcurrencyException ex) {
   string customErrorMessage = ex.Message + ex.Row[0].ToString();
MessageBox.Show(customErrorMessage, ex.GetType().ToString()); } catch (Exception x) { MessageBox.Show(x.Message, x.GetType().ToString());
} }

CONCURRENCY

Or dirty data, is a big concern, and one you have to code for in all cases, so make sure you do, and do it as in the link above! Basically they say catch the exception and do something - either discarding the users changes, or allowing them to overwrite the db via loading a new dataset from the db and merging the local one into it and then calling update again.

How to find errors

Note about updates: If your application does not use the RowUpdated or ColumnUpdated event handlers to catch errors during the update process, you will need to find the error programmatically.

// C#
 private void Find_Errors2() {
   DataSet anyDataset = null;
   foreach (DataTable dt in anyDataset.Tables) {
     foreach (DataRow dr in dt.Rows){
       if (dr.HasErrors) {
         // Row found! Add code to reconcile error.
         dr.ClearErrors();
         // Re-attempt the update.?
       }
     }
   }
 }

MANDITORY READING

This crib sheet is the result of about 6 hours reading, mainly from the MSDN site. The links are in the page above, and also 'stuff' to do with this is below:

Read this first! And read ALL subsections.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbcondatanavigationinwindowsforms.asp

And datasource connection stuff:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vboriadonetconnections.asp

And data binding info:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbconinterfacesrelatedtodatabinding.asp

DataGrid usage: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q316303

Creating a lookup form:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbtskcreatinglookuptableforlistboxorcomboboxcontrol.asp

DataSet updates and how it works
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbconusingdatacommandversususingdatasets.asp