|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis article is my own approach on how a multi-tier application could look like in C# and ADO.NET. The program has a GUI (Graphical User Interface), BO (Business Object) and DO (Data Object). Multi-tier is in my vision a program where the GUI doesn't have logic (no code that makes decisions), the DO only reads and save data to the database and all the rest is done by the BO. That's what I tried to accomplish with this code. After having solved the multi-tier problem, I wanted to create a multi-b platform. In the code, I have added a different DO for each DataProvider or DBMS (Database Management System). That means that the BO just calls the correct version of the DO and combines these to a In my code, it's even possible to load data from an Access database and save the changes to a MS SQL Server database. But I don't recommend this. Last but not least, I want to create a framework that will solve DB Concurrency errors. I think it is possible to handle all DB Concurrency errors the same way, whatever database and table is used. Using the exampleIn the zip-file, there is a folder Multi-Tier which contains the solution-file. Just open it in VS.NET 2003 and run it. To test the code on an Access database, nothing extra is required. The Northwind database is also in the Muti-Tier folder.
On the toolbar, there are 2 buttons enabled. The first is the load-button; the other is the save-button. The load button has a dropdown menu where a database and the connection can be selected. After selecting the source, click the load-button and the data should be loaded in the grid. Now rows can be changed, added or deleted. After all the changes have been done, press the save-button and the changes are saved to the database. If errors have occurred during load or save, a message is set in the statusbar. * Note when using MS SQL and SqlClient. If SqlClient is used, then the following query must be executed in the Northwind database. This is to test the DB Concurrency. ALTER TABLE dbo.Customers ADD Timestamp timestamp NULL.
Using the codeMy example contains 4 projects. The first (MultiTier.Data) contains functions that are related to the DO, but are the same for every DO. The other 3 are example applications that use the Northwind database.
The GUI has 2 buttons, one loads the data from the database and the other saves the changes to the database. In the statusbar, the last action output is shown 5 seconds. The Data classThe using System;
using System.Data;
namespace MultiTier.Data {
public abstract class Data {
private DataTable dtDataTable;
private Exception exException;
private long lngAffectedRecords;
private string strConnectionString;
private string strTableName;
private Data() {
}
protected Data(string connection) {
this.ConnectionString = connection;
this.ConstructDataAdapter();
}
protected DataTier(string connection, string tableName) {
this.ConnectionString = connection;
this.TableName = tableName;
this.ConstructDataAdapter();
}
protected DataTable Table {
get { return dtDataTable; }
set { dtDataTable = value; }
}
public string ConnectionString {
get { return strConnectionString; }
set { strConnectionString = value; }
}
public string TableName {
get { return strTableName; }
set { strTableName = value; }
}
public long AffectedRecords {
get { return lngAffectedRecords; }
set { lngAffectedRecords = value; }
}
public Exception CurrentException {
get { return exException; }
set { exException = value; }
}
protected abstract void ConstructDataAdapter();
public abstract DataTable ReadData();
public abstract void SaveData(DataTable dataTable);
public abstract void InitializeConnection();
}
}
The derived classes implement database-specific code. There is an public override DataTable ReadData() {
/*--- Create a new DataSet ---*/
this.Table = new DataTable(this.TableName);
this.Table.Locale = CultureInfo.InvariantCulture;
try {
/*--- Fill the DataSet with the Customers ---*/
daDataAdapter.Fill(this.Table);
/*--- Return the DataSet ---*/
return this.Table;
}
catch (OleDbException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
/*--- Return the DataSet ---*/
return this.Table;
}
}public override void SaveData(DataTable dataTable) {
this.Table = dataTable;
OleDbTransaction trTransaction = null;
/*--- Save the data ---*/
try {
/*--- Set up the conection manually ---*/
InitializeConnection();
cnConnection.Open();
/*--- Begin a transaction ---*/
trTransaction = cnConnection.BeginTransaction();
/*--- Make all database changes ---*/
this.AffectedRecords = daDataAdapter.Update(this.Table);
/*--- Commit the changes ---*/
trTransaction.Commit();
}
catch (DBConcurrencyException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
trTransaction.Rollback();
}
catch (OleDbException ex) {
/*--- An error occurred, so we roll the transaction back ---*/
this.CurrentException = ex;
trTransaction.Rollback();
}
finally {
/*--- Close the connection that we manually opened ---*/
trTransaction = null;
cnConnection.Close();
cnConnection = null;
}
}
How it worksThe GUI is just a form with a grid which calls When the GUI calls the Data Object Code (DO)using System;
using System.Data;
using System.Data.OleDb;
namespace MultiTier.Example.Data {
public class CustomersOle : MultiTier.Data.OleDB {
public CustomersOle(string connection) : base(connection) {
}
protected override void ConstructDataAdapter() {
string strQuery = "";
OleDbCommand cmSelect;
OleDbCommand cmUpdate;
OleDbCommand cmInsert;
OleDbCommand cmDelete;
/*--- Set up the Connection ---*/
InitializeConnection();
/*--- Set up the SELECT Command ---*/
strQuery = @"SELECT CustomerID,
CompanyName, ContactName, City, Region
FROM Customers
ORDER BY CompanyName";
cmSelect = null;
cmSelect = new OleDbCommand(strQuery, this.Connection);
cmSelect.CommandType = CommandType.Text;
/*--- Set up the UPDATE Command ---*/
strQuery = @"UPDATE Customers
SET CompanyName = @CompanyName ,
ContactName = @ContactName, City = @City, Region = @Region
WHERE CustomerID = @CustomerID";
cmUpdate = null;
cmUpdate = new OleDbCommand(strQuery, this.Connection);
cmUpdate.CommandType = CommandType.Text;
cmUpdate.Parameters.Add(new OleDbParameter("@CompanyName",
OleDbType.VarWChar, 40, "CompanyName"));
cmUpdate.Parameters.Add(new OleDbParameter("@ContactName",
OleDbType.VarWChar, 30, "ContactName"));
cmUpdate.Parameters.Add(new OleDbParameter("@City",
OleDbType.VarWChar, 15, "City"));
cmUpdate.Parameters.Add(new OleDbParameter("@Region",
OleDbType.VarWChar, 15, "Region"));
cmUpdate.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Set up the INSERT Command ---*/
strQuery = @"INSERT INTO Customers (CompanyName,
ContactName, City, Region, CustomerID)
VALUES (@CompanyName, @ContactName,
@City, @Region, @CustomerID)";
cmInsert = null;
cmInsert = new OleDbCommand(strQuery, this.Connection);
cmInsert.CommandType = CommandType.Text;
cmInsert.Parameters.Add(new OleDbParameter("@CompanyName",
OleDbType.VarWChar, 40, "CompanyName"));
cmInsert.Parameters.Add(new OleDbParameter("@ContactName",
OleDbType.VarWChar, 30, "ContactName"));
cmInsert.Parameters.Add(new OleDbParameter("@City",
OleDbType.VarWChar, 15, "City"));
cmInsert.Parameters.Add(new OleDbParameter("@Region",
OleDbType.VarWChar, 15, "Region"));
cmInsert.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Set up the DELETE Command ---*/
strQuery = @"DELETE FROM Customers
WHERE CustomerID = @CustomerID";
cmDelete = null;
cmDelete = new OleDbCommand(strQuery, this.Connection);
cmDelete.CommandType = CommandType.Text;
cmDelete.Parameters.Add(new OleDbParameter("@CustomerID",
OleDbType.WChar, 5, "CustomerID"));
/*--- Create and set up the DataAdapter ---*/
this.DataAdapter = new OleDbDataAdapter();
this.DataAdapter.SelectCommand = cmSelect;
this.DataAdapter.UpdateCommand = cmUpdate;
this.DataAdapter.InsertCommand = cmInsert;
this.DataAdapter.DeleteCommand = cmDelete;
/*--- Destroy connection object ---*/
this.Connection = null;
}
}
}
Business Object Data (BO)public DataSet ReadNorthwind() {
DataSet dsDataSet = new DataSet("Northwind");
DataTable dtCustomers;
DataTier doCustomers;
try {
/*--- Reset Exception ---*/
this.Exception = null;
/*--- Make database choice ---*/
if (this.Provider == DataProvider.OleDB) {
doCustomers = new
MultiTier.Example.Data.CustomersOle(this.ConnectionString,
"Customers");
} else {
doCustomers = new
MultiTier.Example.Data.CustomersSql(this.ConnectionString,
"Customers");
}
/*--- Read Customers ---*/
dtCustomers = doCustomers.ReadData();
/*--- Catch errors ---*/
if (doCustomers.Exception != null) {
this.AffectedRecords = 0;
throw doCustomers.Exception;
} else {
/*--- Set keys on the DataTables ---*/
dtCustomers.PrimaryKey = new DataColumn[]
{ dtCustomers.Columns["CustomerID"] };
/*--- Add Columns to DataTable ---*/
/*--- Add Rows to DataTable ---*/
/*--- Add DataTables to DataSet ---*/
dsDataSet.Tables.Add(dtCustomers);
/*--- Add DataRelations to DataSet ---*/
/*--- Set the total of loaded records ---*/
this.AffectedRecords = doCustomers.AffectedRecords;
}
/*--- Return DataSet ---*/
return dsDataSet;
}
catch (Exception ex) {
this.Exception = ex;
return dsDataSet;
}
finally {
dtCustomers = null;
doCustomers = null;
}
}public void SaveNorthwind(DataSet dsDataSet) {
DataTable dtCustomers;
DataTier doCustomers;
try {
/*--- Reset Exception ---*/
this.Exception = null;
/*--- Check for changes with the HasChanges method first. ---*/
if (dsDataSet != null & dsDataSet.HasChanges()) {
/*--- Grab all changed rows ---*/
dtCustomers = dsDataSet.Tables["Customers"].GetChanges();
/*--- Check for changes in the DataTable. ---*/
if (dtCustomers != null) {
/*--- Make database choice ---*/
if (this.Provider == DataProvider.OleDB) {
doCustomers = new
MultiTier.Example.Data.CustomersOle(this.ConnectionString,
"Customers");
} else {
doCustomers = new
MultiTier.Example.Data.CustomersSql(this.ConnectionString,
"Customers");
}
/*--- Save Customers ---*/
doCustomers.SaveData(dtCustomers);
/*--- Catch errors ---*/
if (doCustomers.Exception != null) {
this.AffectedRecords = 0;
throw doCustomers.Exception;
} else {
this.AffectedRecords = doCustomers.AffectedRecords;
}
}
}
}
catch (Exception ex) {
this.Exception = ex;
}
finally {
dtCustomers = null;
doCustomers = null;
}
}
Graphical User Interface Code (GUI)private void LoadData() {
grdData.DataBindings.Clear();
dsDataSet = boNorthwind.ReadNorthwind();
grdData.DataSource = dsDataSet.Tables["Customers"];
if (boNorthwind.Exception == null)
SetMessage(boNorthwind.AffectedRecords + " records are loaded.");
else
SetMessage("While loading the record(s) a '" +
boNorthwind.Exception.GetType().ToString() + "' occured.");
}
private void SaveData() {
boNorthwind.SaveNorthwind(dsDataSet);
if (boNorthwind.Exception == null)
SetMessage(boNorthwind.AffectedRecords + " records are saved.");
else
SetMessage("While saving the record(s) a '" +
boNorthwind.Exception.GetType().ToString() + "' occured.");
}
Concurrency errorsA concurrency can occur when a record in the database is read by both user A and by user B, change it and save it back to the database. The user that last updated the record will overwrite the data that was updated by the the first user. This can be caught by an exception in ADO.NET, To create such an exception, a record must have a version-number. This can be done by SQL-server by adding a Timestamp column to the table. I use a Timestamp column in my SqlClient example. When you open the program twice and change something in the same row in both grids and try to save the changes back to the grid, a UPDATE Customers
SET CompanyName = @CompanyName ,
ContactName = @ContactName, City = @City, Region = @Region
WHERE CustomerID = @CustomerID
AND Timestamp = @Timestamp
More on Concurrency can be found by Googeling. AutoIncrementWhen working with disconnected data, we need to make sure that identity or auto-increment are unique in the To solve this problem, you can simply set two or three properties on the
When you finally use the Then there are 2 ways to get the IDs the database generated in the disconnected Known issuesThe next list is things that need some more taughts:
The following list shows things that can be added to the sample:
ConclusionIn my opinion, the code can split the data-logic, business-logic and the GUI in separate classes and DLLs. In fact, it should be possible to add a Web-Form with a small amount of code. I hope (with your help) to update the functions and optimize the code. Version history
Revision history
Sources
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||