Be Sharp With C# (Chapter 14, Database Access)
Be Sharp With C# (Chapter 14, Database Access)
14
Database Access
Key concepts
Database
Entity Relationship Diagram
SQL
ADO.NET
Data sources and Data sets
Chapter 14 2 Databases
Database basics
Database system
A database is an organised collection of entities (things) and relationships that exist between
the entities. The database may be used by the application programs of a given enterprise. An
enterprise might be a single individual with a very small private database or a complete
corporation with a very large shared database (or anything in between).
The hardware portion of a database system consist of the secondary storage volumes together
with the associated I/O devices as well as the processor(s) and associated main memory that
are used to support the execution of the database system software.
Between the physical database and the users of the system is a layer of software, the database
management system (DBMS). All requests from users for access to the database are handled
by the DBMS. The DBMS may, in turn, consist of two elements: the application programs and
the database engine (DBE). The DBE provides access to the actual data while the application
programs are user-friendly interfaces between the user and the DBE. The application
programs may provide facilities to add, retrieve, update or delete data through queries, forms,
etc. We can use Microsoft® Access or a C# program to develop the application software.
Microsoft® Access has its own built-in DBE, the so-called JET engine, but if you write a C#
program you will have to use the ADO.NET components to access the data. Through ADO.NET
you can also access data in other database systems such as SQL Server, Oracle, DB2,
Interbase, etc.
There are mostly three classes of users involved with a database system: Application
programmers are responsible for writing application programs that use the database. This is
the main concern of this chapter: Teaching you how to write applications in C# that connects
with the physical database. End users interact with the database system from online
workstations or terminals through either the DBMS (MS Access in our case) or the application
programs mentioned above. The third class of user is the database administrator (DBA). This
person is responsible to create the actual database and to implement the technical controls
needed to enforce management's data policies. These policies determine what data should be
stored in the database and dictates who can perform what operations on what data in what
circumstances. The DBA is also responsible to ensure that the system operates with adequate
performance.
Entities are any of the things of interest to an enterprise and about which the enterprise
collects data, such as PRODUCT, SUPPLIER, CUSTOMER, SALE, etc. (Entities are capitalised when
written in normal text.)
There are several kinds of database systems: hierarchical, network, relational and object-
oriented. We will focus on relational databases. A relational database stores the data for each
entity in a table with rows and columns. The columns in the table are called fields. The rows
are referred to as records. Each table in a relational database must have a primary key, i.e. a
field or combination of fields that are guaranteed to be unique from one record to the other.
For example, a customer number (key fields are underlined when written in normal text) can
be the key field for a CUSTOMER.
Chapter 14 3 Databases
Fields
SUPPLIERS
Supplier code Name Address Telephone
12 John's wholesalers 17 Breyton road 345 1123
11 Edmund clothing PO Box 13 112 4536
23 Mahew PO Box 3323 112 2234
1
Primary key SUPPLIER-SUPPLIES-PRODUCT
PRODUCTS Many
Product code Description Price Supplier code
A1 Sweater 135.56 12
A2 Track suit 250.34 12
A3 Trousers 112.45 23
Foreign key
SALE
Customer number Product code Invoice number
112W3 A1 1
112W3 A2 1
112W3 A3 1
2213S A1 2
2213S A2 3
231A A3 4
Chapter 14 4 Databases
Repeat the process for the other tables, SUPPLIERS, PRODUCTS and SALE.
- The data type for all fields may be text, except for the PRODUCT/Price field which must
be Number / Decimal.
Because a database can include a large number of entities and relationships, database
designers often use an entity relationship diagram (ERD) to document a database's
structure. In Microsoft Access, all of the above relationships can be defined and
represented as follows in an ERD.
- Close all the tables. Click on Database Tools / Relationships. Add all the tables to the
ERD.
- Select one or more fields in the primary table with the mouse and then drag the mouse
to the secondary table. A dialog box similar to the one below will appear.
Chapter 14 5 Databases
Referential integrity is a term used to indicate that no values may exist in the secondary
table of a relationship if there is not a corresponding value in the primary table. For
instance, we may not add a new record to SALE if the corresponding product is not yet
registered in PRODUCTS. The reverse also holds: if a record is deleted from PROUCTS, all
records in SALE for that specific product must be deleted as well. Furthermore, if a product
code is changed in PRODUCTS, all occurrences of that product code in SALE must be
changed as well.
- Note the checkboxes in the dialog box above that must be checked to enforce
referential integrity. If Cascade Update is checked, all changes to values in the primary
table will automatically be written to all occurrences of that value in the secondary
table. If Cascade Delete is checked, a deletion of a record in the primary table will
result in the automatic deletion of all related records in the secondary table.
- We mostly check the Cascade Update box, but you must think very carefully about the
Cascade Delete box. This could be disastrous and sometimes we prefer not to check it.
In this case, when the user tries to delete a record in the primary table, he will get a
message that it is not possible and that he/she must first delete all related records in
the secondary tables.
The key fields are marked with and the one-to-many relationships are represented with
1– combinations. It is good practice to set up an ERD such that the one-to-many
relationships are read from left to right.
ER diagram
Chapter 14 6 Databases
Suppose the shop manager needs the following information from the database:
Q1: For the purposes of a catalogue, the manager needs an alphabetical list of all the
products with their codes, descriptions and prices. Only products with a price of R200
or less must be listed.
Q2: The manager needs a list of all product details, including the respective supplier names
and telephone numbers.
Q3: The manager receives a query from MPN Jones and needs a list of all the product
descriptions and the respective supplier names for all the products bought by him.
Q4: What is the total amount on invoice 1?
Given tables with a relative small amount of data, a human can answer the above queries by
searching the tables manually. This is, however, not always the case and we need a way to
query the database programmatically. For this purpose, a query language, called SQL
(Structured Query Language) (some pronounce it "es kew el", others pronounce it as "sequel")
was developed. SQL is a platform independent language that uses the same syntax
irrespective of the application. This means that the same SQL statement that works in Access
can be embedded in a C# application as well (beware, there are small syntactical catches).
The first question above can be answered by the following SQL statement:
Note that if there are one or more spaces in a field name, the field name must be enclosed
in square brackets.
Add a new query and enter the following SQL statement to answer the second question
(Q2):
The third query (Q3) is a little bit more involved and may look like this:
Add a new query and enter the following SQL statement to answer the fourth question.
SELECT SUM(Price) AS TotalPrice
FROM Products P INNER JOIN Sale S ON P.ProductCode = S.ProductCode
WHERE InvoiceNumber = "1"
We can expand the query a bit to list the totals on all invoices:
SELECT InvoiceNumber, SUM(Price) AS TotalPrice
FROM Products P INNER JOIN Sale S ON P.ProductCode = S.ProductCode
GROUP BY InvoiceNumber
A data source is the primary source of data. A data source is mostly a database, but it can
also be a text file, spreadsheet or XML file. A data set is an in-memory cache of data
retrieved from a data source. For performance purposes, chunks of data are kept in a data
set from where queries are run and manipulations are done. We need a way to connect to
a data source, retrieve the data (data source data set) and update it again (data set
data source).
ADO.NET is an object-oriented set of libraries contained within the .NET framework that
allows you to interact with data sources. Different providers provide component libraries to
act as "adapter" between the specific database and .NET.
There are four core classes for which providers must provide an interface:
Each provider has its own set of components that interacts with these core classes. For the
purposes of connecting to the Microsoft Access database, we will use the components with
prefix OleDb-, for example OleDbDataAdapter. In order to use these classes in your
projects, you need to include the System.Data.OleDb namespace in a using directive.
Chapter 14 9 Databases
The table below lists some of the common providers of ADO.NET components with the
types of data sources that they support.
The in-memory cache of data in the data set is encapsulated by a DataSet object. It may
contain multiple DataTable objects, which contain columns and rows, just like normal
database tables. The DataSet object enables manipulation of data in memory which is
much faster than manipulating data directly on an external data source.
Form controls, such as a DataGridView, get their data from one of the data tables in the
data set.
The following connection diagram provides a summary of the various classes and how they
interact with one another:
Data/table adapter
Update() Fill()
Data set
Note: Visual Studio provides many wizards for data connections. You may follow the
steps in the wizards, but that would take control out of your hands and you may not
understand what was done behind the scenes. We will use the wizard later.
- Name the data adapter with a da prefix and a reference to the table for which data will
be retrieved, e.g. daSuppliers.
Chapter 14 10 Databases
If we decided to use a DataSet object to hold the in-memory data, the last three lines in
the code above would have looked like this. Remember that a DataSet object may
represent the entire database whereas a DataTable object represents only one table in the
database.
4 DataSet dstClothing = new DataSet();
5 daSuppliers.Fill(dstClothing);
6 dgvSuppliers.DataSource = dstClothing.Tables[0];
Run the program, click on Fill button, and make sure that the data is displayed correctly.
Chapter 14 11 Databases
Keep the general connection diagram above in mind. It will help you to understand what
comes after what and what is connected to what.
In the connection diagram below, the brown arrow indicates the direction of data flow:
from the external data source, through the in-memory DataTable object until it is
presented to the user in a DataGridView.
The black arrows indicate how the different components are connected, i.e. who the owners
are of the respective properties or methods and where they point to:
- The connection object, cnClothing, has a ConnectionString property that connects to
the database.
- The data adapter, daSuppliers, has a SelectCommand.Connection property that points
to the connection. The data adapter also has a SelectCommand.CommandText property
that specifies what data to retrieve from the data source.
- The form control, dgvSuppliers, has a DataSource property that points to one of the
tables of the data set.
- The data adapter has a Fill() method that fills the data set with the data that is
retrieved from the data source. The connection object specified with the Connection
property must be valid, but it does not need to be open. If the connection is closed
when Fill() is called, it is opened to retrieve data, then closed. If the connection is
open before Fill() is called, it remains open.
Direction tblSuppliers
of data Fill() DataSource
daSuppliers dgvSuppliers
SelectCommand
Connection
cnClothing
ConnectionString
Data source
(Clothing.accdb)
Resources
General
http://en.csharp-online.net/Working_with_Data%E2%80%94ADO.NET_Classes
Data sets
ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_raddata/html/ee57f4f6-9fe1-4e0a-be9a-
955c486ff427.htm
DataGridView
http://en.csharp-online.net/Working_with_Data%E2%80%94Using_the_DataGridView
http://dotnetperls.com/datagridview-tutorial
http://www.switchonthecode.com/tutorials/csharp-tutorial-binding-a-datagridview-to-a-
database
Chapter 14 12 Databases
Remember that the DataGridView displays data from the in-memory cache (data set) of
the database (data source). In other words, changes in the DataGridView must be
explicitly committed to the database.
We use the data adapter component again to act as bridge between the actual database
and the in-memory copy thereof. In order to update, insert or delete records, we have to
define the UpdateCommand, InsertCommand and DeleteCommand properties of the data
adapter and then call the Update() method. Each one of these properties refers to an
OleDbCommand object with, in turn, a number of properties, the most important of which
are the Connection and CommandText properties.
Note that an OleDbCommandBuilder object can only generate commands for SELECT queries
that are based on single tables. In other words, JOINs are not supported.
Just for interesting sake: A typical update command for a Microsoft Access database might
look like this:
UPDATE Suppliers
SET SupplierCode = ?, SupplierName = ?, Address = ?, Telephone = ?
WHERE ((SupplierCode = ?)
AND ((? = 1 AND SupplierName IS NULL) OR (SupplierName = ?))
AND ((? = 1 AND Address IS NULL) OR (Address = ?))
AND ((? = 1 AND Telephone IS NULL) OR (Telephone = ?))
)
The contents of the in-memory data cache as displayed by the DataGridView can be
retrieved by referencing the DataSource property of the DataGridView. This property
returns an object of the generic Object class and we have to cast it to a DataTable object:
The Update() method of the data adapter calls the respective INSERT, UPDATE, or DELETE
statements for each inserted, updated, or deleted row in the specified DataTable, for
example:
daOrders.Update(tblSuppliers);
It is quite possible that the user could have entered some invalid data, e.g. a string where
a number was expected or a duplicate key value. It is, therefore, essential to add some
error handling as well. An example of the entire update procedure is given below as
defined in an Update() method:
Chapter 14 13 Databases
If you want the user to click a Save or Update button explicitly to commit the changes in
the data set to the database, you can call the Update() method from a button's Click
event handler. If you are worried that users will forget to click the button, you can call the
Update() method from the Validating event handler of the DataGridView. This event
occurs as part of a series of events whenever a control loses or obtains focus.
- It is important that you don't use both event handlers as this might cause concurrency
errors.
Typed datasets have a structure that imitates that of the data source. In other words, the in-
memory cache of data will consist of tables with columns as in the original data source.
Tables, columns and relationships can be instantiated in the code with direct reference to a
class that inherits from the base class, DataSet, and contains specific information about the
structure of the selected database.
Start with a new project in Visual Studio, Clothing. We will gradually expand this project to
serve as example of several database functionalities.
- Rename the form as frmSuppliers.
- Add a Close button.
- Run the program so that the full directory structure will be created.
- Put the database that you are going to work with in the "\bin\Debug" folder.
Chapter 14 14 Databases
Click on Data / Show Data Sources to show a hierarchical view of the data set with tables
and fields.
Right click anywhere in the Data Sources window and select Edit DataSet with Designer.
Chapter 14 15 Databases
- The Dataset Designer is a set of visual tools for creating and editing typed datasets and
the individual items that make up datasets. The Dataset Designer provides visual
representations of the objects contained in typed datasets. You create and modify table
adapters, table adapter queries, DataTables, DataColumns, and DataRelations with
the Dataset Designer.
(ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_raddata/html/cd0dbe93-be9b-41e4-bc39-
e9300678c1f2.htm)
- Note that a similar ERD as the one that was created for the data source in Microsoft
Access has now been generated for the data set.
- The symbols for the one-to-many relationships has been replaced with .
- The tables on the many-sides of the relationships are on the left and the ones on the
one-side are on the right.
- Select the database that was used for the typed data set in the Data Adapter
Configuration Wizard. Click Next.
- Check that the Use SQL statements radio button is checked and click Next.
- Type a basic SELECT statement (SELECT * FROM Suppliers) in the text box and click
Next.
- Be glad that the wizard will now auto-generate all the SQL statements for the
CommandText properties of the InsertCommand, UpdateCommand and DeleteCommand
objects and that you don't have to use an OleDbCommandBuilder object as above. Click
Finish.
- You will notice that the wizard added an OleDbConnection object as well.
- Rename the connection and data adapter objects to cnClothing and daSuppliers
respectively.
Chapter 14 16 Databases
- Inspect the properties of both these components and check that you understand what
the wizard did. Check especially
cnClothing.ConnectionString
daSuppliers.SelectCommand.Connection
daSuppliers.SelectCommand.CommandText
daSuppliers.UpdateCommand.Connection
daSuppliers.UpdateCommand.CommandText
daSuppliers.InsertCommand.Connection
daSuppliers.InsertCommand.CommandText
daSuppliers.DeleteCommand.Connection
daSuppliers.DeleteCommand.CommandText
- Set the DataSource property of the DataGridView to dstClothing and the DataMember
property to SUPPLIERS.
Important: Use the instance on the form. Don’t' use the project data source which is a
class. If you do that, Visual Studio will instantiate some instances on your behalf and
also auto-generate some code in the form's Load() event handler. If you did it
accidentally, just delete the components that VS added as well as the code in the form's
Load event handler.
Call the Fill() method of the data adapter from the form's Load() event handler to fill the
DataGridView with data:
Use a BindingSource
- Rename it with a bs prefix and reference to the table that will be connected, e.g.
bsSupplier.
- Set the DataSource property to dstClothing. Don't use the project data source.
- Set the DataMember property to SUPPLIERS. Visual Studio will add a table adapter
component to the form as well as some code in the form's Load() event handler.
Remove all of this – we have a data adapter that we will use.
- Change the DataSource property of the DataGridView to bsSupplier and remove its
DataMember property.
Run the program and check that it displays the data.
Direction dstClothing
of data Fill() DataSource
daSuppliers bsSuppliers
SelectCommand
DataSource
Connection dgvSuppliers
cnClothing
ConnectionString
Data source
(Clothing.accdb)
Table adapters
Database specific table adapters allow us to get faster access to the database but at the cost
of less control. As an example, let us leave the Clothing project for a while and quickly
develop a single form for the CUSTOMERS table.
- Rename it as dgvCustomers.
- Click on the component's Task Menu at the top-right corner.
- Select CUSTOMERS from the CClothingDataSet class.
Three components are added to the form automatically. Visual Studio names the instances
with the same name as the corresponding class names, except that it starts with a small
letter. Instances may even have the same name as the classes.
- Rename the DataSet component to dstClothing.
- Rename the BindingSource component to bsCustomers.
- Rename the table adapter to taCustomers.
The following code was entered automatically in the form's Load() event handler:
private void Clothing_Load(object sender, EventArgs e)
{
// TODO: This line of code loads data ...
this.taCustomers.Fill(this.dstClothing.CUSTOMERS);
}
- The Fill() method of a table adapter does not take a DataSet object as parameter as
a DataAdapter does. Instead, it takes an instance of a typed DataTable class, in this
case CClothingDataSet.SUPPLIERDataTable.
Run the program. The data of the CUSTOMERS table is displayed and you did not write a
single line of code yourself! It sounds like a bargain but it comes at a price.
Open the Dataset Designer (Right-click in the Data Sources window and select Edit DataSet
with Designer).
- Note that each table has a table adapter that fulfils the role of a customised (or typed)
data adapter. The table adapters do not inherit from a base TableAdapter class in
.NET. (In fact, there is no TableAdapter class.) Upon generation of the typed dataset
Visual Studio creates a class that is specific for every table in the data set. It
encapsulates private data members for a OleDbDataAdapter, OleDBConnection and
OleDbCommand. Click on a table adapter and inspect the properties – you will see the
familiar properties of an OleDbConnection and OleDbDataAdapter.
Chapter 14 19 Databases
Have a look at the design of frmCustomers again. Note that there are no OleDbConnection
or OleDbDataAdapter components since these are encapsulated (built into) the customised
table adapter, taCustomers. The connection diagram below illustrates this.
Direction dstClothing.CUSTOMERS
of data Fill() DataSource
bsCustomers
DataAdapter
taCustomers DataSource
dgvCustomers
Connection
ConnectionString
Data source
(Clothing.accdb)
- It is possible to add queries to a table adapter in the Dataset Designer and then use
parameters for the WHERE clause of a SELECT statement in the code. That's a bit
awkward though, and it is easier to use a data adapter to fill a data set.
- We can also hack the Microsoft code a bit (see the reference below) to provide a public
interface for the SelectCommand, but we will leave that to those of you who like to
meddle on the inside of things.
References
ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_raddata/html/a87c46a0-52ab-432a-a864-9ba55069f9eb.htm
http://www.codeproject.com/KB/database/TableAdapter.aspx
Additional functionality
Important note: The additions that are discussed below are done to illustrate principles. The
idea is not to develop a full fledged application.
Let us return to the Clothing project and add some functionality. We will develop a Multiple
Document Interface (MDI) with a main form that contains a menu structure from where other
forms can be opened.
Add a new form to the project that will act as MDI parent.
- Click on Project / Add Windows Form … . Select Windows Form.
Don't use the built in MDI Parent Form option since it has a lot of components and code
that we will not use and will be in our way.
- Rename the new form as frmMain.cs.
- Set the IsMDIContainer property to true.
- Change the Text property to CLOTHING SHOP.
- Add File, Windows and Help menu items. Note the shortcut keys as indicated by the
underlined characters.
- Add sub-menu-items File/Suppliers, File/Customers and File/Exit.
- Give appropriate names to each menu item and sub-menu tem, e.g. mnuFile,
mnuFileExit, etc.
Double click on the File/Exit menu item and enter the code to exit the program:
private void mnuFileExit_Click(object sender, EventArgs e)
{
this.Close():
}
Open the Program.cs file and change the Main() method to run this form instead of the
Suppliers form:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new frmMain());
}
Enter the following code for the Click() event handler of the Suppliers menu item:
private void mnuFileSuppliers_Click(object sender, EventArgs e)
{
Form frmChild = new frmSuppliers();
frmChild.MdiParent = this;
frmChild.Text = "Suppliers";
frmChild.Show();
}
Users are human. Humans make errors. Errors must be handled. If a user enters a
duplicate value for the supplier code he will get a run-time error, unless:
Currency management
Enter the following code in the ListChanged() event handler of the BindingSource
control.
private void bsSupplier_ListChanged(object sender, ListChangedEventArgs e)
{
this.daSuppliers.Update(this.dstClothing.SUPPLIERS);
}
- The BindingSource has a List property that contains an ArrayList with references to
each row in the data set. Each row is an instance of the DataRowView class.
- The ListChanged event occurs when an item in the underlying list, i.e. a row in the
data set, changes.
- If the external data source is updated in this event handler it means that the internal
data cache and the external data source will stay synchronised. This is called currency
management.
Add two radio buttons, radSortCode and radSortName, which will allow the user to specify
the sort order of records. Enter the following code for the CheckedChanged() event
handler of radSortCode. Set the CheckedChanged event of radSortName to point to the
same event handler.
private void radSortCode_CheckedChanged(object sender, EventArgs e)
{
if (radSortCode.Checked)
bsSuppliers.Sort = "SupplierCode";
else
bsSuppliers.Sort = "SupplierName";
}
- The BindingSource component has a Sort property that sets the column name.
Note that this code was actually unnecessary since the user can click on the column
headers in the DataGridView to sort the data according to the selected column.
Find a record
Add a text box, txtFind, which will allow the user to type a supplier name. Enter the
following code in the TextChanged event handler of the text box.
- The BindingSource component has a Find() method that returns the index of the item
in the list with the specified property name and value.
- The Find() method will only return an index >= 0 if there is an exact match, including
case. If you want to search on substrings, more work would be necessary, for example:
Add a text box, txtFilter, which will allow the user to type a supplier code or name or
part thereof. Enter the following code in the TextChanged event handler of the text box.
private void txtFilter_TextChanged(object sender, EventArgs e)
{
string sql = "SELECT * FROM Suppliers "
+ "WHERE (SupplierCode LIKE '%" + txtFilter.Text + "%') "
+ " OR (SupplierName LIKE '%" + txtFilter.Text + "%')";
daSuppliers.SelectCommand.CommandText = sql;
dstClothing.SUPPLIERS.Clear();
daSuppliers.Fill(dstClothing);
}
- Note that we had to clear the dataset and refill it from scratch with the filtered data.
Parent-Child relationships
- Enter the basic SELECT statement for the data adapter: "SELECT * FROM Products".
- Rename the data adapter to daProducts.
- Rename the BindingSource to bsProducts, set its DataSource to dstClothing and
DataMember to PRODUCTS.
- Remove the table adapter and associated code that Visual Studio added.
- Rename the DataGridView to dgvProducts and set its DataSource property to
bsProducts.
- The Current property of the BindingSource returns the current item in the list. It
returns a generic Object and must be cast to a DataRowView object.
- The CurrentChanged event occurs whenever the Current property changes, i.e. when
another row in the data set becomes active.
- In other words, the list of products is updated whenever the current supplier changes.
The WHERE clause in the SELECT statement for the data adapter refers to the foreign
key, SupplierCode. A look at the ERD at the beginning of the chapter might clear up
any confusion in your mind.
The ListChanged event of the BindingSource for the secondary table will be used to
update any changes to the secondary table:
Enter the code for the Click() event handler of the Customers menu item of the MDI
Parent form (Refer to the code for the Suppliers menu item).
Run the program and check that the empty form opens and that you can close it again.
Add the following data bound controls to the form:
- Add a DataGridView to the form. Use the Task menu and select the CUSTOMERS table
from the Project Data Source. Edit the columns so that only the CustomerName field is
displayed.
- Rename the DataGridView and the controls that were added automatically:
dgvCustomers, dstClothing, bsCustomers, taCustomers.
- Add a BindingNavigator to the form. Rename it to bnCustomers and set its
BindingSource property to bsCustomers.
- Add text boxes and labels to the form so that the form looks as in the screen print
below. Use a RichTextBox control for the Address field to enable multiple lines with
line breaks.
- Set the DataBindings.Text property of each text box to refer to the appropriate fields
of the BindingSource. Don't use the Project Data Source CCustomerClothing.
- Set the TabIndex properties so that controls will be accessed in order with the Tab key.
Ensure that the user can start typing in the CustomerNumber text box as soon as he clicks
on to add a new customer.
private void bsCustomers_AddingNew(object sender, AddingNewEventArgs e)
{
txtNumber.Focus();
}
Chapter 14 26 Databases
Add a button to the BindingNavigator to save changes to the data set. Enter the
necessary code in its Click() event handler.
try
{
bsCustomers.EndEdit();
dgvCustomers.Invalidate();
}
catch (Exception error)
{
MessageBox.Show(error.Message, "CUSTOMERS",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
- Since we entered the data outside the DataGridView in the data bound text boxes, we
need to refill the DataGridView with Invalidate().
Enter code for the Delete button on the BindingNavigator to get confirmation from the
user as on the Suppliers form.
Enter the code for the DataError event hander of dgvCustomers as for dgvSuppliers.
Run the program and make sure that everything works as expected.
Enter the following code for the Click event handler of the Customers item:
//Build message
7 string sMsg = "";
8 while (dbReader.Read())
{
9 sMsg += dbReader["CustomerNumber"].ToString() + "\t"
+ dbReader["CustomerName"].ToString() + "\n";
10 }
MessageBox.Show(sMsg, "CUSTOMERS");
}
}
Run the program and make sure that you get a message box with the numbers and names
of all customers listed.
Chapter 14 27 Databases
The data reader needs a valid and open connection object (lines 1 – 3). Note the use of
the using statement to limit the scope of the connection and ensure that it will be closed
as soon as it goes out of scope.
The Read() method reads one record at a time (line 8). After each Read() the current
record can be accessed through indexing of the OleDbDataReader object (line 9). Indexes
are either zero-based integers or field names as string values. The Read() method
returns true if there are more records to read; false otherwise. Therefore the while loop
(line 8) will keep on reading records until the last one has been read.
Print to printer
In the example above, each row is concatenated to a string which is eventually displayed
in a message box. You can also adapt the code to print the list to a printer. See Chapter
11 again for explanation of the print procedure.
Add a PrintDocument and a PrintPreviewDialog from the Toolbox to the main form.
Rename them as prntdocReport and prntprvReport respectively.
Replace the code for the Click event handler of the Customers item with the following:
private void mnuReportsCustomers_Click(object sender, EventArgs e)
{
prntprvReport.Document = prntdocReport;
prntprvReport.FindForm().WindowState = FormWindowState.Maximized;
prntprvReport.ShowDialog();
}
Enter the following code for the PrintPage event handler of the prntdocReport control.
//Get data
cnClothing.Open();
string sql = "SELECT * FROM Customers";
OleDbCommand cmd = new OleDbCommand(sql, cnClothing);
OleDbDataReader dbReader = cmd.ExecuteReader();
Chapter 14 28 Databases
//Print headers
font = new Font(font, FontStyle.Underline | FontStyle.Bold);
e.Graphics.DrawString("Number", font, brsh, 10, y);
e.Graphics.DrawString("Name", font, brsh, 100, y);
e.Graphics.DrawString("Address", font, brsh, 300, y);
e.Graphics.DrawString("Telephone", font, brsh, 500, y);
y += font.GetHeight() * 2;
//Print data
font = new Font(font, FontStyle.Regular);
while (dbReader.Read())
{
e.Graphics.DrawString(dbReader[0].ToString(), font, brsh, 10, y);
e.Graphics.DrawString(dbReader[1].ToString(), font, brsh, 100, y);
e.Graphics.DrawString(dbReader[2].ToString(), font, brsh, 300, y);
e.Graphics.DrawString(dbReader[3].ToString(), font, brsh, 500, y);
y += font.GetHeight() * 3;
} //while
} //using
} //method
User-defined reports
For this example we will assume that the user knows the basic syntax of SQL.
- Add the following default SQL query in the form's Load() event handler (the user will
be able to change it during run-time):
Add a sub-menu item, User defined query, under the Reports item on the main form. Enter
the code for the Click() event handler of this menu item to create and show the Report
form.
Run the program and check that the form opens and that you can close it again.
Add an OleDbDataAdapter to the form, using the Data Adapter Configuration Wizard.
- Enter any valid query for the SELECT statement.
- Rename it to daReport.
- Rename the accompanying OleDbConnection to cnClothing.
Add a PrintDocument and a PrintPreviewDialog from the Toolbox to your form. Rename
them as prntdocReport and prntprvReport respectively.
Enter the following code for the Click() event handler of the Print button:
private void btnPrint_Click(object sender, EventArgs e)
{
prntdocReport.DefaultPageSettings.Landscape = true;
prntprvReport.Document = prntdocReport;
prntprvReport.FindForm().WindowState = FormWindowState.Maximized;
prntprvReport.ShowDialog();
} //btnPrint_Click
Enter the following code for the PrintPage() event handler of the prntdocReport control.
//Get data
daReport.SelectCommand.CommandText = txtSQL.Text;
DataTable tblReport = new DataTable();
daReport.Fill(tblReport);
int nFields = tblReport.Columns.Count;
//Print headers
font = new Font(font, FontStyle.Underline | FontStyle.Bold);
for (int c = 0; c < nFields; c++)
{
e.Graphics.DrawString(tblReport.Columns[c].Caption,
font, brsh, x, y);
x += 150;
}
y += font.GetHeight() * 2;
//Print data
font = new Font(font, FontStyle.Regular);
foreach (DataRow row in tblReport.Rows)
{
x = 10;
for (int c = 0; c < nFields; c++)
{
e.Graphics.DrawString(row[c].ToString(), font, brsh, x, y);
x += 150;
}
y += font.GetHeight();
}
}
Chapter 14 30 Databases
Study the code above carefully and make sure that you understand everything.
- We used an untyped, local, DataTable object to save the data in memory. Therefore,
we cannot refer to the names of columns and we have to use indexes.
- Note the nested loops to step through the rows of the table and the columns within
each row.
Run the program and check that it works properly.
Keywords
Make sure that you know what each of these items mean or where they are used.
Key:
Concepts : Normal
Classes and Controls : Green, e.g. Color
Properties : Bold, e.g. List
Methods : Bold with brackets, e.g. Remove()
Events : Bold with the word "event", e.g. DataError event
Keywords : Blue, e.g. new
Exercises
1. Consider the Clothing database that was used as example in this chapter. Write SQL
statements for the following queries:
2. Consider the following scenario: A large company has several official vehicles. Each
vehicle is assigned to a specific responsible staff member.
2.2 Write an SQL query that will list all staff member details together with details about the
vehicle that has been assigned to each staff member. The records should be displayed
in order of the year model. Vehicles in the same year must be listed according to the
registration number.
2.3 Develop a form in Visual Studio that will show the content of the above-mentioned
query in a grid. Don't use a typed data set and don't use the drag-and-drop wizards.
2.4 Add a combo box to the form that can be used to filter the listing by make of vehicle.
Chapter 14 32 Databases
3.1 Design a database in Microsoft Access with a single table that contains three Text
fields: Name, Surname and telephone number.
3.2 Design a user interface as in the example. Don't use a typed data set and don't use a
wizard to provide the following functionalities:
The user may enter any surname or part thereof in the text box. Use the
TextChanged() event handler to update the display as the user types. When for
example, the text box contains the characters "Ch", the entries with surnames Chad
and Chiles should be listed. If the text box is empty, all entries in the database
must be listed.
Add a button that will save all changes made in the grid to the underlying database.
Develop an interface with which the results can be queried as in the example. You should
use a typed dataset with a binding source.
Chapter 14 33 Databases
Hints:
1. Right-click on the DataGridView, select Edit Columns … / Time / DefaultCellStyle and set
the Format field to HH:mm:ss to display the times properly.
2. Set the form's WindowState property to Maximized. Enter the following code in the
form's Resize() event handler to ensure that the Close button stays in the bottom-
right hand corner:
btnClose.Left = this.Width - btnClose.Width - 16;
Use a typed dataset and develop an interface with which the data can be filtered as in the
example. The data set should be refreshed when the user clicks on either of the combo
boxes or Clear buttons. Use a typed dataset with a binding source. The combo boxes
should be filled with data from the database in the form's constructor.
Hints:
1. Use the following SQL statement to find all game types in the database:
SELECT DISTINCT Type FROM Games
2. Use the OleDbCommand and OleDbDataReader to fill the combo boxes wit game types
and ratings respectively.
Hint: Create a query in Access, qryDuties, which will be available as part of the typed
data set.
8. Take the previous exercise a bit further: Develop a full-fledged, menu-drive application
which can be used to add, remove and edit staff details and venues. Then create a facility
with which the scheduling can be done, i.e. assigning specific venues to specific staff
members at a specific data and time. Note that no intelligence is required. You only need
to allow the user to enter the date and time for every duty with a staff and venue number.
Hints:
1. Develop an MDI application with a main form that contains a menu strip.
2. Develop separate forms to add, remove and edit the staff members and venues.
3. Develop a form as in the example below to schedule the duties.
4. The DataSource, DisplayMember and ValueMember properties of the ComboBox could be
helpful.
5. Use the OleDBCommand object to define and execute SQL statements to insert or remove
duties from the database. The grid in the form below is only used to view the duties on
the selected date and no editing is done in it.