Click here to Skip to main content
15,886,579 members
Articles / Programming Languages / C#

DataTable Synchronization Manager

Rate me:
Please Sign up or sign in to vote.
4.94/5 (12 votes)
4 Mar 20065 min read 73.2K   1.4K   69  
Adds synchronization to the DataTable Transaction Logger.
/*
Copyright (c) 2006, Marc Clifton
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list
  of conditions and the following disclaimer. 

* Redistributions in binary form must reproduce the above copyright notice, this 
  list of conditions and the following disclaimer in the documentation and/or other
  materials provided with the distribution. 
 
* Neither the name Marc Clifton nor the names of contributors may be
  used to endorse or promote products derived from this software without specific
  prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

// Refactorings:
//		Moved Apply and Revert record manipulation to the DataTableTransactionLog class
//		Moved RestoreRowFields and SaveRowFields to the DataTableTransactionLog class
//		Added indexer
//		Modified the Deleting event to search all the previous transactions and save the current row data in any transaction record that references the row about to be deleted.

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;

// Further reading: http://www.codeproject.com/csharp/practicalguidedatagrids1.asp
// Issues with TableNewRow: http://codebetter.com/blogs/sahil.malik/archive/2005/12/26/135954.aspx
// Regarding field validation: http://msdn2.microsoft.com/en-us/library/0f7ey06d.aspx

namespace Clifton.Data
{
	/// <summary>
	/// Exception class for exceptions occurring in the DataTableTransaction module.
	/// </summary>
	public class DataTableTransactionException : ApplicationException
	{
		public DataTableTransactionException(string msg)
			: base(msg)
		{
		}
	}

	/// <summary>
	/// Encapsulates the transaction record associated with a transaction event.
	/// </summary>
	public class TransactionEventArgs : EventArgs
	{
		/// <summary>
		/// The transaction record.
		/// </summary>
		protected DataTableTransactionRecord record;

		/// <summary>
		/// Gets the transaction record associated with the event.
		/// </summary>
		public DataTableTransactionRecord Record
		{
			get { return record; }
		}

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="record">The transaction record.</param>
		public TransactionEventArgs(DataTableTransactionRecord record)
		{
			this.record = record;
		}
	}

	/// <summary>
	/// Manages row and field changes for a specific DataTable.
	/// </summary>
	public class DataTableTransactionLog
	{
		/// <summary>
		/// Delegate used with the TransactionAdding and TransactionAdded events.
		/// </summary>
		/// <param name="sender">The instance of this class.</param>
		/// <param name="e">The TransactionEVentARgs instance.</param>
		public delegate void TransactionDlgt(object sender, TransactionEventArgs e);

		/// <summary>
		/// Triggered before the transaction is added to the log.
		/// </summary>
		public event TransactionDlgt TransactionAdding;

		/// <summary>
		/// Triggered after the transaction is added to the log.
		/// </summary>
		public event TransactionDlgt TransactionAdded;

		/// <summary>
		/// The log of transactions.
		/// </summary>
		protected List<DataTableTransactionRecord> transactions;

		/// <summary>
		/// The table on which we are tracking transactions.
		/// </summary>
		protected DataTable sourceTable;

		/// <summary>
		/// Enables (the default) or disables logging.  Logging is disabled
		/// during Revert and Apply.
		/// </summary>
		protected bool doLogging;

		/// <summary>
		/// The last transaction record index on which table transactions were accepted.
		/// </summary>
		protected int lastAcceptedChangeIndex;

		protected Dictionary<DataRow, List<int>> uncomittedRows;

		protected List<DataTableTransactionRecord> waitingForChangedEventList;

		protected DataView dataView;

		/// <summary>
		/// Gets/sets the source data table.
		/// </summary>
		public DataTable SourceTable
		{
			get { return sourceTable; }
			set
			{
				if (value == null)
				{
					throw new ArgumentNullException("The source table cannot be null.");
				}

				if (sourceTable != null)
				{
					Unhook();
				}

				sourceTable = value;
				dataView = new DataView(sourceTable);
				Hook();
			}
		}

		/// <summary>
		/// Gets the transaction log for the associated data table.
		/// </summary>
		public List<DataTableTransactionRecord> Log
		{
			get { return transactions; }
		}

		public DataTableTransactionRecord this[int idx]
		{
			get 
			{
				if ((idx < 0) || (idx >= transactions.Count))
				{
					throw new ArgumentOutOfRangeException("Indexer is out of range.");
				}

				return transactions[idx]; 
			}
		}

		public DataView DataView
		{
			get { return dataView; }
		}

		/// <summary>
		/// Constructor.
		/// </summary>
		public DataTableTransactionLog()
		{
			Initialize();
		}

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="sourceTable">The source DataTable.</param>
		public DataTableTransactionLog(DataTable sourceTable)
		{
			SourceTable = sourceTable;
			Initialize();
		}

		protected void Initialize()
		{
			transactions = new List<DataTableTransactionRecord>();
			uncomittedRows = new Dictionary<DataRow, List<int>>();
			waitingForChangedEventList = new List<DataTableTransactionRecord>();
			doLogging = true;
		}

		/// <summary>
		/// Clears the transaction log.  Logging is automatically re-enabled.
		/// The pending ColumnChanged list is also cleared.  This method should not
		/// be called during ColumnChanging.
		/// </summary>
		public void ClearLog()
		{
			lastAcceptedChangeIndex = 0;
			transactions.Clear();
			uncomittedRows.Clear();
			waitingForChangedEventList.Clear();
			transactions.TrimExcess();			// do some memory management.
			doLogging = true;
		}

		/// <summary>
		/// Suspends logging.  Used during Revert and Apply to prevent logging of already
		/// logged transactions.
		/// </summary>
		public void SuspendLogging()
		{
			doLogging = false;
		}

		/// <summary>
		/// Resumes logging.
		/// </summary>
		public void ResumeLogging()
		{
			doLogging = true;
		}

		/// <summary>
		/// Accept all transactions and set the last accepted change index
		/// to the last transaction.
		/// </summary>
		public void AcceptChanges()
		{
			lastAcceptedChangeIndex = transactions.Count;
			sourceTable.AcceptChanges();
		}

		/// <summary>
		/// Remove the transactions to the point where last accepted.
		/// </summary>
		public void RejectChanges()
		{
			int numTran = transactions.Count - lastAcceptedChangeIndex;
			transactions.RemoveRange(lastAcceptedChangeIndex, numTran);
			sourceTable.RejectChanges();
		}

		public void CollectUncommittedRows()
		{
			List<int> allIndices = new List<int>();

			foreach (List<int> indices in uncomittedRows.Values)
			{
				allIndices.AddRange(indices);
			}

			allIndices.Sort();

			for (int n = allIndices.Count - 1; n >= 0; --n)
			{
				transactions.RemoveAt(allIndices[n]);
			}

			uncomittedRows.Clear();
		}

		/// <summary>
		/// Rolls back the transaction occurring at the specified index.
		/// </summary>
		/// <param name="idx">The transaction index to roll back.</param>
		public void Revert(int idx)
		{
			// Validate idx.
			if ((idx < 0) || (idx >= transactions.Count))
			{
				throw new ArgumentOutOfRangeException("Idx cannot be negative or greater than the number of transactions.");
			}

			// Turn off logging, as we don't want to record the undo transactions.
			SuspendLogging();
			DataTableTransactionRecord r = transactions[idx];
			DataRow newRow=r.Revert(sourceTable);

			if (newRow != null)
			{
				FixupRowsReverse(idx, newRow);
			}

			ResumeLogging();
		}

		/// <summary>
		/// Applies the transaction occurring at the specified index.
		/// </summary>
		/// <param name="idx">The transaction index to apply.</param>
		public void Apply(int idx)
		{
			if ((idx < 0) || (idx >= transactions.Count))
			{
				throw new ArgumentOutOfRangeException("Idx cannot be negative or greater than the number of transactions.");
			}

			SuspendLogging();
			DataTableTransactionRecord r = transactions[idx];
			DataRow newRow=r.Apply(dataView);

			if (newRow != null)
			{
				FixupRowsForward(idx, newRow);
			}

			ResumeLogging();
		}

		protected void FixupRowsReverse(int idx, DataRow newRow)
		{
			DataRow row = transactions[idx].Row;

			// Fixup transactions referencing the deleted data row, going backwards
			for (int n = idx; n >= 0; --n)
			{
				if (transactions[n].Row == row)
				{
					transactions[n].Row = newRow;
				}
			}
		}

		protected void FixupRowsForward(int idx, DataRow newRow)
		{
			DataRow row = transactions[idx].Row;

			// Fixup references to the row, going forwards.
			for (int n = idx; n < transactions.Count; ++n)
			{
				if (transactions[n].Row == row)
				{
					transactions[n].Row = newRow;
				}
			}
		}

		protected void Hook()
		{
			sourceTable.ColumnChanging += new DataColumnChangeEventHandler(OnColumnChanging);
			sourceTable.ColumnChanged += new DataColumnChangeEventHandler(OnColumnChanged);
			sourceTable.RowDeleting += new DataRowChangeEventHandler(OnRowDeleting);
			sourceTable.RowChanged += new DataRowChangeEventHandler(OnRowChanged);
			sourceTable.TableNewRow += new DataTableNewRowEventHandler(OnTableNewRow);
			sourceTable.TableCleared += new DataTableClearEventHandler(OnTableCleared);
		}

		/// <summary>
		/// Unhook our event handlers from the source table.
		/// </summary>
		protected void Unhook()
		{
			sourceTable.ColumnChanging -= new DataColumnChangeEventHandler(OnColumnChanging);
			sourceTable.ColumnChanged -= new DataColumnChangeEventHandler(OnColumnChanged);
			sourceTable.RowDeleting -= new DataRowChangeEventHandler(OnRowDeleting);
			sourceTable.RowChanged -= new DataRowChangeEventHandler(OnRowChanged);
			sourceTable.TableNewRow -= new DataTableNewRowEventHandler(OnTableNewRow);
			sourceTable.TableCleared -= new DataTableClearEventHandler(OnTableCleared);
		}

		/// <summary>
		/// We do not support undoing a Clear action.  This simply clears the internal collections and state.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnTableCleared(object sender, DataTableClearEventArgs e)
		{
			ClearLog();
		}

		/// <summary>
		/// Log the new row and add it to the uncommitted row collection.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnTableNewRow(object sender, DataTableNewRowEventArgs e)
		{
			if (doLogging)
			{
				int idx;
				DataTableTransactionRecord record;
				idx = transactions.Count;
				record = new DataTableTransactionRecord(idx, e.Row, DataTableTransactionRecord.RecordType.NewRow);
				OnTransactionAdding(new TransactionEventArgs(record));
				transactions.Add(record);
				OnTransactionAdded(new TransactionEventArgs(record));
				List<int> rowIndices = new List<int>();
				rowIndices.Add(idx);
				uncomittedRows.Add(e.Row, rowIndices);
			}
		}

		protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
		{
			if (doLogging)
			{
				if (e.Action == DataRowAction.Add)
				{
					if (!uncomittedRows.ContainsKey(e.Row))
					{
						throw new DataTableTransactionException("Attempting to commit a row that doesn't exist in the uncommitted row collection.");
					}

					uncomittedRows.Remove(e.Row);
				}
			}
		}

		protected void OnColumnChanging(object sender, DataColumnChangeEventArgs e)
		{
			if (doLogging)
			{
				object oldVal = e.Row[e.Column];
				int trnIdx;
				DataTableTransactionRecord record;

				trnIdx = transactions.Count;
				record = new DataTableTransactionRecord(
					trnIdx,
					e.Row,
					e.Column.ColumnName,
					oldVal,
					e.ProposedValue);
				OnTransactionAdding(new TransactionEventArgs(record));
				transactions.Add(record);
				OnTransactionAdded(new TransactionEventArgs(record));
				waitingForChangedEventList.Add(record);

				if (uncomittedRows.ContainsKey(e.Row))
				{
					uncomittedRows[e.Row].Add(trnIdx);
				}
			}
		}

		/// <summary>
		/// Applies the new value to the transaction record, having given the application
		/// the opportunity to change the ProposedValue.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnColumnChanged(object sender, DataColumnChangeEventArgs e)
		{
			if (doLogging)
			{
				int n = waitingForChangedEventList.Count - 1;

				for (int i = n; i >= 0; i--)
				{
					DataTableTransactionRecord r = waitingForChangedEventList[i];

					if ( (r.ColumnName == e.Column.ColumnName) &&
						 (r.Row==e.Row) )
					{
						r.NewValue = e.ProposedValue;
						waitingForChangedEventList.RemoveAt(i);
						break;
					}
				}
			}
		}

		/// <summary>
		/// The row deleting event fires when the row has being removed fro the collection.
		/// We can't use the row deleted event to record the row field values because the row
		/// has been then marked as deleted and accessing the fields throws an exception.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnRowDeleting(object sender, DataRowChangeEventArgs e)
		{
			if (doLogging)
			{
				DataTableTransactionRecord record;
				record = new DataTableTransactionRecord(transactions.Count, e.Row, DataTableTransactionRecord.RecordType.DeleteRow);
				record.SaveRowFields(e.Row);
				OnTransactionAdding(new TransactionEventArgs(record));
				transactions.Add(record);
				Dictionary<string, object> colVals = record.ColumnValues;

				// Tell all transaction records involving this row to save the row fields.
				// This allows us to access deleted row data in earlier transactions.
				// Alternatively, since the row is deleted, all transactions involving the 
				// deleted row could be removed.  I'm not sure about this approach though--
				// is it possible for transactions to affect other non-deleted data before
				// the row deleted?
				for (int i = 0; i < transactions.Count-1; i++)
				{
					if (transactions[i].Row == e.Row)
					{
						transactions[i].ColumnValues = colVals;
					}
				}

				OnTransactionAdded(new TransactionEventArgs(record));
			}
		}

		/// <summary>
		/// Fires the TransactionAdding event.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnTransactionAdding(TransactionEventArgs e)
		{
			if (TransactionAdding != null)
			{
				TransactionAdding(this, e);
			}
		}

		/// <summary>
		/// Fires the TransactionAdded event.
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnTransactionAdded(TransactionEventArgs e)
		{
			if (TransactionAdded != null)
			{
				TransactionAdded(this, e);
			}
		}
	}
}

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 Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions