Introduction
I was really fascinated by the fantastic article I came across on the web. The article "Building Layered Web Applications with Microsoft ASP.NET 2.0" by "Imar Spaanjars" Building Layered Web Applications with Microsoft ASP.NET 2.0
helped me come up with my own four-tier, web-based Architecture using Ajax. While Imar's examples were made easy for the readers, I extended the BO (Presentation), BLL (Business Logic Layer), and the DAL (Data Access Layer) into Class Library projects and wrapped the entire application in a more structured way.
The purpose of this application is to implement the four-tier architecture and demonstrate the running of the application using a single housekeeping form.
Let us assume we have a database named "ACCOUNTS". This database contains a table Named "AccountTypes". In the below examples I will demonstrate:
- How the application binds the "Grid View" using our custom List Collection Class (Inside the collection Folder of BO Library).
- How the Insert and Select operations to the "AccountTypes" table are carried out from the client side using the xmlHTTP object. (AJAX)
- How to dynamically build a table using the array retrieved by the XmlHTTP call. (AJAX)
- How to render JavaScript code through code behind files.
Background
I recommend you to go through Imar's Article before going through my illustration.
Building Layered Web Applications with Microsoft ASP.NET 2.0
Using the code
Phase I
I will start off by creating a new Class Library Project for the Business Object (BO).
- I will first create an AccountTypes.cs class. This will hold all the private variables to the class and public properties that correspond to the database fields.
- In this project I have a separate folder called "Collections" for the Item Collections. I'll create a collection class AccountTylesList.cs which will inherit from its generic counterpart (List<accounttypes>)
- I'll keep the constructor code empty, so that this can be filled up dynamically when required.
- Your "BO" Project is ready for compilation. Building the project will give you BO.dll (Save this Library for future references)
Below is the code for the class AccountTypes
:
using System;
using System.Collections.Generic;
using System.Text;
namespace BO
{
public class AccountTypes
{
#region Private Variables
private string accType = String.Empty;
private int accTypeId = -1;
#endregion
#region Public Properties
public int AccountTypeId
{
get
{
return accTypeId;
}
set
{
accTypeId = value;
}
}
public string AccountType
{
get
{
return accType;
}
set
{
accType = value;
}
}
#endregion
}
}
Below is the code for the class AccountTypesList
:
using System;
using System.Collections.Generic;
using System.Text;
namespace BO
{
public class AccountTypesList : List<AccountTypes>
{
public AccountTypesList()
{ }
}
}
Phase II
I will now create a Class Library Project for the Data Access Layer (DAL).
- Add Reference of the previously compiled BO library to this project, and inherit the namespace "BO" in all your classes.
- In the DAL project, I'll have a common data access file AccessDB.cs, which will be used by all other classes inside this project to access ADO components.
- Next thing I do is add a Data Access Layer for my AccountTypes Class. (From BO). I'll name the Class as AccountTypesDB.cs.
- I'll have the database "Select", "Insert" & "Update" operations related to AccoountTypes.cs (From BO) in AccountTypesDB.cs
- AccountTypesDb.cs will initialize and set the values in AccountTypes.cs and eventually these values will be consumed by the AccountTypesManager.cs (BLL)
- Your "DAL" Project is ready for compilation, Building the project will give you DAL.dll (Save this Library for future references)
Below is the code for the class AccessDB
:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace DAL
{
class Database
{
#region Private Variables
private string ConnectionString = string.Empty;
private SqlConnection oConnection = null;
private SqlCommand oCommand = null;
#endregion
#region Public Methods
public Database()
{
ConnectionString = GetDbConnectionString();
}
public static string GetDbConnectionString()
{
string DbServer =
ConfigurationSettings.AppSettings["DatabaseServer"].ToString();
string DbName =
ConfigurationSettings.AppSettings["Database"].ToString();
string UserId =
ConfigurationSettings.AppSettings["DbUserId"].ToString();
string Password =
ConfigurationSettings.AppSettings["DbPassword"].ToString();
string ConnectionString = "Data Source=" + DbServer + ";" +
"Initial Catalog=" + DbName + ";" + "User ID=" + UserId + ";" +
"Password=" + Password;
return ConnectionString;
}
public bool GetDBReader(string SQL, out SqlDataReader SqlDataReader)
{
SqlDataReader = null;
if ((SQL == null) || (SQL == ""))
return false;
oConnection = new SqlConnection(ConnectionString);
oCommand = new SqlCommand(SQL, oConnection);
oConnection.Open();
SqlDataReader =
oCommand.ExecuteReader(CommandBehavior.CloseConnection);
return SqlDataReader.HasRows;
}
public void DisposeDBReader(SqlDataReader oReader)
{
if (oReader != null)
{
if (!oReader.IsClosed)
oReader.Close();
}
if (oCommand != null)
{
oCommand.Dispose();
}
if (oConnection != null)
{
if (oConnection.State != ConnectionState.Closed)
oConnection.Close();
oConnection.Dispose();
}
}
public int ExecuteQuery(string sSQL)
{
SqlConnection oConn = new SqlConnection(ConnectionString);
SqlCommand oComm = new SqlCommand(sSQL, oConn);
int iRecordsAffected = 0;
try
{
oConn.Open();
iRecordsAffected = oComm.ExecuteNonQuery();
oConn.Close();
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (oCommand != null)
{
oCommand.Dispose();
}
if (oConnection != null)
{
if (oConnection.State != ConnectionState.Closed)
oConnection.Close();
oConnection.Dispose();
}
}
return iRecordsAffected;
}
#endregion
}
}
Below is the code for the class AccountTypesDb
:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using BO;
namespace DAL
{
public class AccountTypesDB
{
#region Private Methods
private static AccountTypes FillDataRecord(IDataRecord myDataRecord)
{
AccountTypes accountType = new AccountTypes();
accountType.AccountTypeId =
myDataRecord.GetInt32(
myDataRecord.GetOrdinal("AccountTypeId"));
if (!myDataRecord.IsDBNull(myDataRecord.GetOrdinal("AccountType")))
{
accountType.AccountType = myDataRecord.GetString(
myDataRecord.GetOrdinal("AccountType"));
}
return accountType;
}
private static AccountTypes FillGridData(SqlDataReader myDataReader)
{
AccountTypes accountType = new AccountTypes();
while (myDataReader.Read())
{
if (accountType.GridData == String.Empty ||
accountType.GridData.Length == 0)
{
for (int i = 0; i < myDataReader.FieldCount; i++)
{
if (i == 0)
accountType.GridData = "[[" + "\"" +
myDataReader[i].ToString() + "\"";
else
accountType.GridData = accountType.GridData + ",
\"" + myDataReader[i].ToString() + "\"";
}
accountType.GridData = accountType.GridData + "]";
}
else
{
for (int i = 0; i < myDataReader.FieldCount; i++)
{
if (i == 0)
accountType.GridData = accountType.GridData + ", ["
+ "\"" + myDataReader[i].ToString() + "\"";
else
accountType.GridData = accountType.GridData + ",
\"" + myDataReader[i].ToString() + "\"";
}
accountType.GridData = accountType.GridData + "]";
}
}
if (accountType.GridData != String.Empty ||
accountType.GridData.Length > 0)
accountType.GridData = accountType.GridData + "]";
return accountType;
}
#endregion
#region public Methods
public static AccountTypes GetAccountType(int accountTypeId)
{
string sql = string.Empty;
AccountTypes accountType = null;
Database oDatabase = new Database();
sql = "SELECT * FROM ACCOUNTTYPES WHERE ACCOUNTTYPEID = " +
accountTypeId;
SqlDataReader myReader = null;
oDatabase.GetDBReader(sql, out myReader);
if (myReader.Read())
{
accountType = FillDataRecord(myReader);
}
oDatabase.DisposeDBReader(myReader);
myReader.Dispose();
return accountType;
}
public static AccountTypes GetGridData(string SQL)
{
string sql = string.Empty;
AccountTypes accountType = null;
Database oDatabase = new Database();
sql = SQL;
SqlDataReader myReader = null;
oDatabase.GetDBReader(sql, out myReader);
accountType = FillGridData(myReader);
oDatabase.DisposeDBReader(myReader);
myReader.Dispose();
return accountType;
}
public static int SaveAccountType(AccountTypes accounts)
{
int result = 0;
using (SqlConnection myConnection = new
SqlConnection(Database.GetDbConnectionString()))
{
SqlCommand myCommand = new
SqlCommand("spSaveUpdateAccountType", myConnection);
myCommand.CommandType = CommandType.StoredProcedure;
if (accounts.AccountTypeId == -1)
{
myCommand.Parameters.AddWithValue("@accountTypeId",
DBNull.Value);
}
else
{
myCommand.Parameters.AddWithValue("@accountTypeId",
accounts.AccountTypeId);
}
myCommand.Parameters.AddWithValue("@accountType",
accounts.AccountType);
SqlParameter returnValue;
returnValue = myCommand.CreateParameter();
returnValue.Direction = ParameterDirection.ReturnValue;
myCommand.Parameters.Add(returnValue);
myConnection.Open();
myCommand.ExecuteNonQuery();
result = Convert.ToInt32(returnValue.Value);
myConnection.Close();
}
return result;
}
public static AccountTypesList GetAccountTypeList()
{
AccountTypesList tempList = null;
using (SqlConnection myConnection = new
SqlConnection(Database.GetDbConnectionString()))
{
SqlCommand myCommand = new SqlCommand("SELECT * FROM
ACCOUNTTYPES", myConnection);
myCommand.CommandType = CommandType.Text;
myConnection.Open();
using (SqlDataReader myReader = myCommand.ExecuteReader())
{
if (myReader.HasRows)
{
tempList = new AccountTypesList();
while (myReader.Read())
{
tempList.Add(FillDataRecord(myReader));
}
}
myReader.Close();
}
}
return tempList;
}
#endregion
}
}
Phase III
Finally, I will create a Class Library Project for the Business Logic Layer (BLL).
- Add Reference of the previously compiled BO & DAL library to this project, and inherit the namespace "BO" & "DAL" in all your classes.
- Here, I will add a class AccountTypesManager.cs. Here goes the logic, validations, and other calculations whichever is required before hitting the database.
- The AccountTypesManager.cs class will deal with the DLL and BO to finally output the result to the BO object.
- Your "BLL" Project is ready for compilation, Building the project will give you DAL.dll (Save this Library for future references)
Below is the code for the class AccountTypesManager
:
using System;
using System.Collections.Generic;
using System.Text;
using BO;
using DAL;
namespace BLL
{
public class AccountTypesManager
{
#region Private Methods
#endregion
#region Public Methods
public static AccountTypes GetAccountType(int accountTypeId)
{
return AccountTypesDB.GetAccountType(accountTypeId);
}
public static AccountTypes GetGridData(string SQL)
{
return AccountTypesDB.GetGridData(SQL);
}
public static AccountTypesList GetAccountTypeList()
{
return AccountTypesDB.GetAccountTypeList();
}
public static int SaveAccountType(AccountTypes accounts)
{
ValidateSaveAccountType(accounts);
accounts.AccountTypeId = AccountTypesDB.SaveAccountType(accounts);
return accounts.AccountTypeId;
}
public static void ValidateSaveAccountType(AccountTypes Accounts)
{
if (String.IsNullOrEmpty(Accounts.AccountType))
{
throw new Exception("Cannot save an Account Type without a
valid property.");
}
}
public static bool ValidateSaveAccountType(AccountTypes Accounts,
bool throwError)
{
if (String.IsNullOrEmpty(Accounts.AccountType))
{
if (throwError == true)
throw new Exception("Cannot save an Account Type without a
valid property.");
else
return false;
}
return true;
}
#endregion
}
}
Phase IV
Note: Phase IV can be skipped if you are interested in implementing Ajax in your application. Phase IV shows the basic usage of the functionality through code behind files.
Now that our three-tier architecture is ready, we only have to see the implementation and how things work using the below:
- Create a new Web Project.
- Add Reference of the previously compiled BO & BLL library to this project, and inherit the namespace "BO" & "BLL" in all your classes. Note that the DAL is completely hidden, and will not be accessible unless you add the DAL.dll to your references which is not required.
- In the pageload, call the
GetAccountTypeList
Method using the AccountTypesManager
Class. (Note that you wont have to instantiate any class) This will act as a datasource to your grid view control. "gvAccountTypes"
- call the SaveAccountType Method using the accounttypesmanager class on the button click event in the code behind file.
Below is the code for the Page Default.aspx:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
Inherits="Accounts._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Accounts</title>
<script type='text/javascript' language="javascript">
function fnBindGrid()
{
PostXmlHttp('AjaxAccess.asmx/WsBindGrid', 'CBBindGrid',
'SQL=SELECT * FROM ACCOUNTTYPES');
}
function CBBindGrid()
{
try
{
if (xmlHttp.readyState==4)
{
document.all.item("HiddenDiv").style.display = "none";
ParseXml(xmlHttp.responseText);
if(returnParseXml != null)
{
if(Trim(returnParseXml.childNodes[0].nodeValue) == 'Success')
alert('Saved Successfully');
else
{
var myData = [,];
myData = eval(returnParseXml.childNodes[0].nodeValue);
var htmlTable;
if(myData.length > 0)
{
var rows = myData.length;
var columns = myData[0].length;
htmlTable = '<TABLE>';
for( i = 0 ; i < rows ; i++)
{
htmlTable += '<TR>';
for( j = 0 ; j < columns ; j++ )
{
htmlTable += '<TD>';
htmlTable += myData[i][j];
htmlTable += '</TD>';
}
htmlTable += '</TR>';
}
htmlTable += '</TABLE>';
CustTable.innerHTML = htmlTable;
}
}
}
else
alert('Failed: Return XML not found');
}
}
catch(e)
{
alert(e.Message);
}
}
function fnSaveAccountType()
{
PostXmlHttp('AjaxAccess.asmx/WsSaveAccountType', 'CBSaveAccountType',
'AccountType='+Trim(document.all.item("txtAccountType").value));
}
function CBSaveAccountType()
{
try
{
if (xmlHttp.readyState==4)
{
document.all.item("HiddenDiv").style.display = "none";
ParseXml(xmlHttp.responseText);
if(returnParseXml != null)
{
if(Trim(returnParseXml.childNodes[0].nodeValue) == 'Success')
{
alert('Saved Successfully');
fnBindGrid();
}
else
alert(returnParseXml.childNodes[0].nodeValue);
}
else
alert('Failed: Return XML not found');
}
}
catch(e)
{
alert(e.Message);
}
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="gvAccountTypes" runat="server"></asp:GridView>
<div id='CustTable'></div>
<input id="txtAccountType" type="text" />
<input id="Button1" type="button" value="Save"
onclick="fnSaveAccountType()" />
</div>
</form>
</body>
</html>
Below is the code for the Class Default.aspx.cs:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using BO;
using BLL;
namespace Accounts
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
gvAccountTypes.DataSource =
AccountTypesManager.GetAccountTypeList();
gvAccountTypes.DataBind();
}
}
}
Phase V
I added a common webservice file to the project. I name this as AjaxAccess.asmx.cs
- I added a common class common.cs, for all the common functions to be used in my project.
- I made a class "Script" insided the common class. I prefer putting the script code insided a class, rather than having a .js file.
- the script class contains methods for:
- rendering the common functions
- rendering the ajax functions
- On the page load, I will render the common functions and ajax functions through the common.script.[methodname] functions of my common class.
- Now my application is ready and ajax enabled. All I need to do is call the ajax Get or Post methods rendered by (RenderAjaxScript).
Below is the code for the class AccountTypes
:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using BO;
using BLL;
namespace Accounts
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Common.Script.RenderCommonScript(Page, "CommonScript");
Common.Script.RenderAjaxScript(Page,"AjaxScript");
}
}
}
Below is the code for the class Common
:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace Accounts
{
public class Common
{
public Common()
{
}
public class Script
{
public Script()
{
}
#region Render Common Script
public static void RenderCommonScript(System.Web.UI.Page oPage,
string ScriptName)
{
oPage.RegisterStartupScript(ScriptName, @"
<script type='text/javascript' language="'javascript'">
function Trim(StringValue)
{
try
{
if (StringValue == null)
return '';
//Remove leading spaces, tabs and carriage
//returns
while ((StringValue.substring(0, 1) == ' ') ||
(StringValue.substring(0, 1) == '\n') ||
(StringValue.substring(0, 1) == '\t') ||
(StringValue.substring(0, 1) == '\r'))
{
StringValue = StringValue.substring(1,
StringValue.length);
}
//Remove trailing spaces, tabs and carriage
//returns
while ((StringValue.substring(
StringValue.length-1,
StringValue.length) == ' ') ||
(StringValue.substring(
StringValue.length-1,
StringValue.length) ==
'\n') || (StringValue.substring(
StringValue.length-1, StringValue.length)
== '\t') || (StringValue.substring(
StringValue.length-1,
StringValue.length) == '\r'))
{
StringValue = StringValue.substring(0,
StringValue.length-1);
}
}
catch(e)
{
alert('Error Occured while Triming the
String:\n' + e.message);
}
return StringValue;
}
</script>");
}
# endregion
#region Render AJAX SCRIPT
public static void RenderAjaxScript(System.Web.UI.Page oPage,
string ScriptName)
{
string renderAjaxScript = string.Empty;
renderAjaxScript = @"
// This is an Invisible Div Rendered on the page in order to
// avoid user input while the page is busy retriving data.
// This will be shown and hidden on the page consecutively as
// and when required.
<div id='HiddenDiv' style='DISPLAY: none; LEFT: 0px;
POSITION: absolute; TOP: 0px; WIDTH=100%; HEIGHT=100%'>
<table align='center' width='100%' height='100%'
cellpadding='0' cellspacing='0'>
<tr>
<td width='100%' height='100%' align='center'
valign='middle'><IMG alt='' src='";
renderAjaxScript = renderAjaxScript +
ConfigurationSettings.AppSettings[
"PreLoaderImage"].ToString();
renderAjaxScript = renderAjaxScript + @"'>
</td>
</tr>
</table>
</div>";
oPage.Response.Write(renderAjaxScript);
oPage.RegisterStartupScript(ScriptName, @"
<script type='text/javascript' language="'javascript'">
var xmlHttp;
function PostXmlHttp(URL, CallBackFunction, Parameters)
{
xmlHttp=null;
try
{ // Firefox, Opera 8.0+, Safari
xmlHttp=new XMLHttpRequest();
}
catch (e)
{ // Internet Explorer
try
{
xmlHttp=new
ActiveXObject(
'Msxml2.XMLHTTP');
}
catch (e)
{
try
{
xmlHttp=new
ActiveXObject(
'Microsoft.XMLHTTP');
}
catch (e)
{
alert('Your browser
does not support
AJAX!');
return;
}
}
}
try
{
if (xmlHttp!=null)
{
xmlHttp.onreadystatechange =
eval(CallBackFunction);
}
if(document.all.item('HiddenDiv'))
{
// The Invisible Div appears on
// the user screen to avoid
// extra clicks by the user.
document.all.item(
'HiddenDiv').style.display = '';
}
xmlHttp.open('POST',URL,true);
xmlHttp.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded;
charset=UTF-8');
xmlHttp.send(Parameters);
}
catch(e)
{
alert('Error Occured while Posting
XmlHttp Values:\n' + e.message);
}
}
function GetXmlHttp(URL, CallBackFunction, Parameters)
{
xmlHttp=null;
try
{ // Firefox, Opera 8.0+, Safari
xmlHttp=new XMLHttpRequest();
}
catch (e)
{ // Internet Explorer
try
{
xmlHttp=new ActiveXObject(
'Msxml2.XMLHTTP');
}
catch (e)
{
try
{
xmlHttp=new
ActiveXObject(
'Microsoft.XMLHTTP');
}
catch (e)
{
alert('Your browser
does not support
AJAX!');
return;
}
}
}
try
{
if (xmlHttp!=null)
{
xmlHttp.onreadystatechange =
eval(CallBackFunction);
}
if(document.all.item('HiddenDiv'))
{
// The Invisible Div appears on
// the user screen to avoid
// extra clicks by the user.
document.all.item(
'HiddenDiv').style.display = '';
}
xmlHttp.open('POST',URL,true);
xmlHttp.send(null);
}
catch(e)
{
alert('Error Occured while Getting
XmlHttp Values:\n' + e.message);
}
}
var returnParseXml
function ParseXml(text)
{
// code for IE
if (window.ActiveXObject)
{
var doc=new
ActiveXObject('Microsoft.XMLDOM');
doc.async='false';
doc.loadXML(text);
}
// code for Mozilla, Firefox, Opera, etc.
else
{
var parser=new DOMParser();
var doc=parser.parseFromString(
text,'text/xml');
}// documentElement always represents the root
// node
returnParseXml = doc.documentElement;
}
</script>");
}
# endregion
}
}
}
Below is the code for AjaxAccess.asmx.cs:
using System;
using System.Data;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.ComponentModel;
using BLL;
using BO;
namespace Accounts
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class AjaxAccess : System.Web.Services.WebService
{
[WebMethod]
public string WsSaveAccountType(string AccountType)
{
try
{
AccountTypes oAccountTypes = new AccountTypes();
oAccountTypes.AccountType = AccountType;
AccountTypesManager.SaveAccountType(oAccountTypes);
return "Success";
}
catch (Exception ex)
{
return "Failed: " + ex.Message;
}
}
[WebMethod]
public string WsBindGrid(string SQL)
{
try
{
return AccountTypesManager.GetGridData(SQL).GridData;
}
catch (Exception ex)
{
return "Failed: " + ex.Message;
}
}
}
}
Web Config Entries
<appSettings>
<add key="DatabaseServer" value="(local)" />
<add key="Database" value="ACCOUNTS" />
<add key="DbUserId" value="sa" />
<add key="DbPassword" value="sa" />
<add key="PreLoaderImage" value="Images/loading.gif" />
</appSettings>
Points of Interest
While retriving data to fill up the table dynamically, I was creating a two dimensional array (basically, its a string representing the array structure) and transporting it to the client side using a webservice.
Upon reaching the client side, JavaScript would interpret this as only string and not array.
I went crazy trying to find the problem. I found this really cool key word in JavaScript: "eval". Very Handy to use. eval did the trick for me.
The eval() function evaluates a string and executes it as if it was script code.