Click here to Skip to main content
15,885,890 members
Articles / Web Development / ASP.NET

Web Application Page Patterns

Rate me:
Please Sign up or sign in to vote.
4.47/5 (4 votes)
23 Jan 200710 min read 47.1K   349   53  
Two common design patterns for web application pages: the Single Entity Postback Editor and the Multi-Entity Postback Editor
//==============================================================================
// MyGeneration.dOOdads
//
// TransactionMgr.cs
// Version 5.1
// Updated - 11/17/2005
//------------------------------------------------------------------------------
// Copyright 2004, 2005 by MyGeneration Software.
// All Rights Reserved.
//
// Permission to use, copy, modify, and distribute this software and its 
// documentation for any purpose and without fee is hereby granted, 
// provided that the above copyright notice appear in all copies and that 
// both that copyright notice and this permission notice appear in 
// supporting documentation. 
//
// MYGENERATION SOFTWARE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 
// AND FITNESS, IN NO EVENT SHALL MYGENERATION SOFTWARE BE LIABLE FOR ANY 
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE 
// OR PERFORMANCE OF THIS SOFTWARE. 
//==============================================================================

using System;
using System.Data;
using System.Configuration;
using System.Threading;
using System.Diagnostics;
using System.Collections;

namespace MyGeneration.dOOdads
{
	/// <summary>
	/// TransactionMgr is used to seemlessly enroll BusinessEntity's  into a transaction. TransactionMgr uses
	/// ADO.NET transactions and therefore is not a distributed transaction as you would get with COM+. You only have
	/// to use TransactionMgr if two or more BusinessEntity's need to be saved as a transaction.  The BusinessEntity.Save
	/// method is already protected by a transaction.
	/// </summary>
	/// <remarks>
	///	Transaction Rules:
	/// <list type="bullet">
	///		<item>Your transactions paths do not have to be pre-planned. At any time you can begin a transaction</item>
	///		<item>You can nest BeginTransaction/CommitTransaction any number of times as long as they are sandwiched appropriately</item>
	///		<item>Once RollbackTransaction is called the transaction is doomed, nothing can be committed even it is attempted.</item>
	///		<item>Transactions are stored in the Thread Local Storage.</item>
	///	</list>
	/// Transactions are stored in the Thread Local Storage or
	/// TLS. This way the API isn't intrusive, ie, forcing you
	/// to pass a SqlConnection around everywhere.  There is one
	/// thing to remember, once you call RollbackTransaction you will
	/// be unable to commit anything on that thread until you
	/// call ThreadTransactionMgrReset().
	/// 
	/// In an ASP.NET application each page is handled by a thread
	/// that is pulled from a thread pool. Thus, you need to clear
	/// out the TLS (thread local storage) before your page begins
	/// execution. The best way to do this is to create a base page
	/// that inhertis from System.Web.UI.Page and clears the state
	/// like this:	
	///	</remarks>
	///	<example>
	/// VB.NET
	/// <code>
	/// Dim tx As TransactionMgr
	/// tx = TransactionMgr.ThreadTransactionMgr()
	/// 
	/// Try
	/// 	tx.BeginTransaction()
	/// 	emps.Save()
	/// 	prds.Save()
	/// 	tx.CommitTransaction()
	/// Catch ex As Exception
	/// 	tx.RollbackTransaction()
	/// 	tx.ThreadTransactionMgrReset()
	/// End Try
	/// </code>
	/// C#
	/// <code>
	/// TransactionMgr tx = TransactionMgr.ThreadTransactionMgr();
	/// 
	/// try
	/// {
	/// 	tx.BeginTransaction();
	/// 	emps.Save();
	/// 	prds.Save();
	/// 	tx.CommitTransaction();
	/// }
	/// catch(Exception ex)
	/// {
	/// 	tx.RollbackTransaction();
	/// 	tx.ThreadTransactionMgrReset();
	/// }
	/// </code>
	/// </example>
	public class TransactionMgr
	{
		/// <summary>
		/// You cannot new an instance of the TransactionMgr class, see the static method <see cref="ThreadTransactionMgr"/>
		/// </summary>
		protected TransactionMgr()
		{

		}

		/// <summary>
		/// Returns the number of outstanding calls to <see cref="BeginTransaction"/> without subsequent calls to 
		/// <see cref="CommitTransaction"/>
		/// </summary>
		public int NestingCount
		{
			get
			{
				return this.txCount;
			}
		}

		/// <summary>
		/// True if <see cref="RollbackTransaction"/> has been called on this thread. 
		/// </summary>
		public bool HasBeenRolledBack
		{
			get
			{
				return hasRolledBack;
			}
		}

		/// <summary>
		/// BeginTransaction should always be a followed by a call to CommitTransaction if all goes well, or
		/// RollbackTransaction if problems are detected.  BeginTransaction() can be nested any number of times
		/// as long as each call is unwound with a call to CommitTransaction().
		/// </summary>
		public void BeginTransaction()
		{
			if( hasRolledBack) throw new Exception("Transaction Rolledback");

			txCount = txCount + 1;
		}

		/// <summary>
		/// The final call to CommitTransaction commits the transaction to the database, BeginTransaction and
		/// CommitTransaction calls can be nested, <see cref="BeginTransaction"/>
		/// </summary>
		public void CommitTransaction()
		{
			if(hasRolledBack) throw new Exception("Transaction Rolledback");

			txCount = txCount - 1;

			if(txCount == 0)
			{
				foreach(Transaction tx in this.transactions.Values)
				{
                    tx.sqlTx.Commit();
                    tx.sqlTx.Dispose();
				}

				this.transactions.Clear();

				if(this.objectsInTransaction != null)
				{
					try
					{
						foreach(BusinessEntity entity in this.objectsInTransaction)
						{
							entity.AcceptChanges();
						}
					} 
					catch {}

					this.objectsInTransaction = null;
				}
			}
		}

		/// <summary>
		/// RollbackTransaction dooms the transaction regardless of nested calls to BeginTransaction. Once this method is called
		/// nothing can be done to commit the transaction.  To reset the thread state a call to <see cref="ThreadTransactionMgrReset"/> must be made.
		/// You must call 
		/// </summary>
		public void RollbackTransaction()
		{
			if(false == hasRolledBack && txCount > 0)
			{
				foreach(Transaction tx in this.transactions.Values)
				{
					tx.sqlTx.Rollback();
					tx.sqlTx.Dispose();
			
					IDbConnection cn = tx.sqlTx.Connection;

					if(cn != null && cn.State == ConnectionState.Open)
					{
						cn.Close();
					}
				}

                this.transactions.Clear();
                this.txCount = 0;
				this.objectsInTransaction = null;
			}
		}

		/// <summary>
		/// Enlist by the dOOdads architecture when a IDbCommand (SqlCommand is an IDbCommand). The command may or may not be enrolled 
		/// in a transaction depending on whether or not BeginTransaction has been called. Each call to Enlist must be followed by a
		/// call to <see cref="DeEnlist"/>.
		/// </summary>
		/// <param name="cmd">Your SqlCommand, OleDbCommand, etc ...</param>
		/// <param name="entity">Your business entity, in C# use 'this', VB.NET use 'Me'.</param>
		/// <example>
		/// C#
		/// <code>
		/// txMgr.Enlist(cmd, this);
		/// cmd.ExecuteNonQuery();
		/// txMgr.DeEnlist(cmd, this);
		/// </code>
		/// VB.NET
		/// <code>
		/// txMgr.Enlist(cmd, Me)
		/// cmd.ExecuteNonQuery()
		/// txMgr.DeEnlist(cmd, Me)
		/// </code>
		/// </example>
		public void Enlist(IDbCommand cmd, BusinessEntity entity)
		{
			if(txCount == 0 || entity._notRecommendedConnection != null)
			{
				// NotRecommendedConnections never play in dOOdad transactions
				cmd.Connection = CreateSqlConnection(entity);
			}
			else
			{
				string connStr = entity._config;
				if(entity._raw != "") connStr = entity._raw;

				Transaction tx = this.transactions[connStr] as Transaction;

				if(tx == null)
				{
					tx = new Transaction();
					IDbConnection sqlConn = CreateSqlConnection(entity);
					if(_isolationLevel != IsolationLevel.Unspecified)
					{
						tx.sqlTx = sqlConn.BeginTransaction(_isolationLevel);
					}
					else
					{
						tx.sqlTx = sqlConn.BeginTransaction();
					}
					this.transactions[connStr] = tx;
				}

				cmd.Connection = tx.sqlTx.Connection;
				cmd.Transaction = tx.sqlTx;
			}
		}

		/// <summary>
		/// Each call to Enlist must be followed eventually by a call to DeEnlist.  
		/// </summary>
		/// <param name="cmd"></param>
		/// <param name="entity"></param>
		/// <example>
		/// C#
		/// <code>
		/// txMgr.Enlist(cmd, this);
		/// cmd.ExecuteNonQuery();
		/// txMgr.DeEnlist(cmd, this); 
		/// </code>
		/// VB.NET
		/// <code>>
		/// txMgr.Enlist(cmd, Me)
		/// cmd.ExecuteNonQuery()
		/// txMgr.DeEnlist(cmd, Me)
		/// </code>
		/// </example>
		public void DeEnlist(IDbCommand cmd, BusinessEntity entity)
		{
			if(entity._notRecommendedConnection != null)
			{
				// NotRecommendedConnection never play in dOOdad transactions
				cmd.Connection = null;
			}
			else
			{
				if(txCount == 0)
				{
					cmd.Connection.Dispose();
				}
			}
		}

		/// <summary>
		/// Called internally by BusinessEntity
		/// </summary>
		/// <param name="entity"></param>
		internal void AddBusinessEntity(BusinessEntity entity)
		{
			if(this.objectsInTransaction == null)
			{
				this.objectsInTransaction = new ArrayList();
			}

			this.objectsInTransaction.Add(entity);
		}

		private IDbConnection CreateSqlConnection(BusinessEntity entity)
		{
			IDbConnection cn;

			if(entity._notRecommendedConnection != null)
			{
				// This is assumed to be open
				cn = entity._notRecommendedConnection;
			}
			else
			{
				cn = entity.CreateIDbConnection();

				if(entity._raw != "")
					cn.ConnectionString = entity._raw;
				else
#if(VS2005)
					cn.ConnectionString = ConfigurationManager.AppSettings[entity._config];
#else
                    cn.ConnectionString = ConfigurationSettings.AppSettings[entity._config];
#endif

				cn.Open();
			}

			return cn;
		}

		// We might have multple transactions going at the same time.
		// There's one per connnection string
		private class Transaction
		{
			public IDbTransaction sqlTx = null;
		}

        private Hashtable transactions = new Hashtable();
		private int txCount = 0;
		private bool hasRolledBack  = false;

		// Used to control AcceptChanges()
		internal ArrayList objectsInTransaction = null;

		#region "static"
		/// <summary>
		/// This static method is how you obtain a reference to the TransactionMgr. You cannot call "new" on TransactionMgr.
		/// If a TransactionMgr doesn't exist on the current thread, one is created and returned to you.
		/// </summary>
		/// <returns>The one and only TransactionMgr for this thread.</returns>
		public static TransactionMgr ThreadTransactionMgr()
		{
			TransactionMgr txMgr = null;

			object obj = Thread.GetData(txMgrSlot);

			if(obj != null)
			{
				txMgr = (TransactionMgr)obj;
			}
			else
			{
				txMgr = new TransactionMgr();
				Thread.SetData(txMgrSlot, txMgr);
			}

			return txMgr;
		}

		/// <summary>
		/// This must be called after RollbackTransaction or no futher database activity will happen successfully on the current thread.
		/// </summary>
		public static void ThreadTransactionMgrReset()
		{
			TransactionMgr txMgr = TransactionMgr.ThreadTransactionMgr();

			try
			{
				if(txMgr.txCount > 0 && txMgr.hasRolledBack == false)
				{
					txMgr.RollbackTransaction();
				}
			}
			catch {}

			Thread.SetData(txMgrSlot, null);
		}

		/// <summary>
		/// This is the Transaction's strength. The default is "IsolationLevel.Unspecified, the strongest is "IsolationLevel.Serializable" which is what
		/// is recommended for serious enterprize level projects.
		/// </summary>
		public static IsolationLevel IsolationLevel
		{
			get
			{
				return _isolationLevel;
			}

			set
			{
				_isolationLevel = value;
			}
		}

        private static IsolationLevel _isolationLevel = IsolationLevel.Unspecified;
		private static LocalDataStoreSlot txMgrSlot = Thread.AllocateDataSlot();
		#endregion

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Milliman
United States United States
I have been involved in professional software development for over 15 years, focusing on distributed applications on both Microsoft and Java platforms.

I also like long walks on the beach and a sense of humor and don't like mean people Wink | ;-)

Comments and Discussions