Click here to Skip to main content
15,895,084 members
Articles / Programming Languages / C#

Disconnected Client Architecture

Rate me:
Please Sign up or sign in to vote.
4.76/5 (65 votes)
14 Feb 2007CPOL22 min read 164.8K   2.7K   332  
A look at an offline client architecture that I've implemented in an application for a client.
/*
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

using Clifton.Collections.Generic;
using Clifton.Tools.Strings;

namespace Clifton.Data
{
	internal class RowColumnKey
	{
		protected DataRow row;
		protected string columnName;
		
		/// <summary>
		/// Gets/sets columnName
		/// </summary>
		public string ColumnName
		{
			get { return columnName; }
		}
		
		/// <summary>
		/// Gets/sets row
		/// </summary>
		public DataRow Row
		{
			get { return row; }
		}

		public RowColumnKey(DataRow row, string columnName)
		{
			this.row = row;
			this.columnName = columnName;
		}

		public override bool Equals(object obj)
		{
			RowColumnKey rck = (RowColumnKey)obj;
			return (row == rck.Row) && (columnName == rck.ColumnName);
		}

		public override int GetHashCode()
		{
			return row.GetHashCode();
		}
	}
	
	/// <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;
		}
	}

	public class RowAddedEventArgs : EventArgs
	{
		protected DataTableTransactionRecord record;
		
		/// <summary>
		/// Gets/sets record
		/// </summary>
		public DataTableTransactionRecord Record
		{
			get { return record; }
		}

		public RowAddedEventArgs(DataTableTransactionRecord record)
		{
			this.record = record;
		}
	}

	public class RowDeletedEventArgs : EventArgs
	{
		protected DataTableTransactionRecord record;

		/// <summary>
		/// Gets/sets record
		/// </summary>
		public DataTableTransactionRecord Record
		{
			get { return record; }
		}

		public RowDeletedEventArgs(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);

		public delegate void RowAddedDlgt(object sender, RowAddedEventArgs e);

		public delegate void RowDeletedDlgt(object sender, RowDeletedEventArgs 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>
		/// Triggered after handling a TableNewRow event.
		/// </summary>
		public event RowAddedDlgt RowAdding;

		/// <summary>
		/// Triggered after handling a RowAdded event, which adds the row to the DataTable's row collection.
		/// </summary>
		public event RowAddedDlgt RowAdded;

		public event RowDeletedDlgt RowDeleting;

		/// <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>
		/// During data table synchronization with a row transaction manager, we want to turn off logging of 
		/// logging of new rows and existing fields, as these create duplicate entries, but we still want
		/// new field assignments to be added to the transaction log, as these result from events firing on
		/// the new row that populate additional fields.
		/// </summary>
		protected bool logOnlyNewFieldChanges;

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

		/// <summary>
		/// Tracks records that haven't been added to the DataTable yet.  These are "Detached" rows.
		/// </summary>
		protected Dictionary<DataRow, List<int>> uncommittedRows;

		protected List<DataTableTransactionRecord> waitingForChangedEventList;

		protected DataView dataView;

		/// <summary>
		/// Returns false if the local logger is disabled or if the source table is in "BlockEvents" mode, which
		/// may have been set by another logger associated with the same table.
		/// </summary>
		protected bool DoLogging
		{
			get
			{
				bool ret = doLogging;

				if (ret)
				{
					ret &= !(bool)sourceTable.ExtendedProperties["BlockEvents"];
				}

				return ret;
			}
		}

		/// <summary>
		/// Gets/sets the source data table.
		/// </summary>
		public DataTable SourceTable
		{
			get { return sourceTable; }
			set
			{
				// Unhook any table currently being monitored.
				if (sourceTable != null)
				{
					Unhook();
				}

				sourceTable = value;

				// If we're monitoring a new table, then hook the events for it.
				if (value != null)
				{
					dataView = new DataView(sourceTable);
					Hook();
					sourceTable.ExtendedProperties["BlockEvents"] = false;
				}
			}
		}

		/// <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>();
			uncommittedRows = 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()
		{
			lock (transactions)
			{
				lastAcceptedChangeIndex = 0;
				transactions.Clear();
				uncommittedRows.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;
			sourceTable.ExtendedProperties["BlockEvents"] = true;
		}

		/// <summary>
		/// Resumes logging.
		/// </summary>
		public void ResumeLogging()
		{
			doLogging = true;
			logOnlyNewFieldChanges = false;
			sourceTable.ExtendedProperties["BlockEvents"] = false;
		}

		public void LogOnlyNewFieldChanges()
		{
			doLogging = false;
			logOnlyNewFieldChanges = 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()
		{
			lock (transactions)
			{
				int numTran = transactions.Count - lastAcceptedChangeIndex;
				transactions.RemoveRange(lastAcceptedChangeIndex, numTran);
				sourceTable.RejectChanges();
			}
		}

		/// <summary>
		/// Remove all transactions for rows that haven't been committed (added to the DataTable).
		/// </summary>
		public void CollectUncommittedRows()
		{
			int i = transactions.Count - 1;

			lock (transactions)
			{
				while (i >= 0)
				{
					// 4/18/06 : Added test for DeleteRow transaction, as without this test, a deleted
					// row was being collected, and therefore never being posted back up to the server.
					if ((transactions[i].TransactionType != DataTableTransactionRecord.RecordType.DeleteRow) &&
						// The Row may be null if excluded by the RTS because the transaction column is read only.
						( (transactions[i].Row==null) || (transactions[i].Row.RowState == DataRowState.Detached)) )
					{
						transactions.RemoveAt(i);
					}

					--i;
				}
			}

			uncommittedRows.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)
			{
				lock (transactions)
				{
					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);
					uncommittedRows.Add(e.Row, rowIndices);
					OnRowAdding(new RowAddedEventArgs(record));
					LogDefaultValues(e.Row);
				}
			}
		}

		/// <summary>
		/// Add transactions for any default values defined in the DataColumn's of the row,
		/// when adding a new row.  This method is called only by the OnTableNewRow handler.
		/// </summary>
		/// <param name="row"></param>
		protected void LogDefaultValues(DataRow row)
		{
			lock (transactions)
			{
				foreach (DataColumn dc in row.Table.Columns)
				{
					if (dc.DefaultValue != DBNull.Value)
					{
						int trnIdx = transactions.Count;
						DataTableTransactionRecord record = new DataTableTransactionRecord(trnIdx, row, dc.ColumnName, DBNull.Value, dc.DefaultValue);
						record.NewValue = dc.DefaultValue;
						OnTransactionAdding(new TransactionEventArgs(record));
						transactions.Add(record);
						OnTransactionAdded(new TransactionEventArgs(record));
						uncommittedRows[row].Add(trnIdx);
					}
				}
			}
		}

		/// <summary>
		/// Event handler for the RowChanged event.  If the event is "add row", the associated
		/// uncommitted rows are removed.  This event also calls the transaction logger's RowAdded
		/// event.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
		{
			if (DoLogging)
			{
				if (e.Action == DataRowAction.Add)
				{
					// Ignore rows not contained in the uncommitted row list.  This occurs when a master table changes the index of a child table
					// when the child table is sandboxed and the row hasn't been logged because no changes were made.  (Is this right?)
					if (uncommittedRows.ContainsKey(e.Row))
					{
						DataTableTransactionRecord record = transactions[uncommittedRows[e.Row][0]];
						uncommittedRows.Remove(e.Row);
						OnRowAdded(new RowAddedEventArgs(record));
					}
					else
					{
						// throw new DataTableTransactionException("Attempting to commit a row that doesn't exist in the uncommitted row collection.");
					}
				}
			}
		}

		/// <summary>
		/// Adds a transaction for the column being changed and adds the column to the
		/// pending list of records waiting for the "Changed" event, which finalizes the
		/// new value (allowing the application to change the new value before the Changed
		/// event records it).
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnColumnChanging(object sender, DataColumnChangeEventArgs e)
		{
			if (DoLogging || logOnlyNewFieldChanges)
			{
				lock (transactions)
				{
					// If only logging for new fields that are being assigned...
					if (logOnlyNewFieldChanges)
					{
						// Verify this really is a new field...
						if (LogExists(e.Column.ColumnName))
						{
							// If not, exit.
							return;
						}
					}

					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));

					// Only create transactions for fields that have actually changed.
					// The application gets the opportunity to change the proposed value however.
					if ((oldVal == null) || (!oldVal.Equals(e.ProposedValue)))
					{
						transactions.Add(record);
						OnTransactionAdded(new TransactionEventArgs(record));
						waitingForChangedEventList.Add(record);
					}

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

		/// <summary>
		/// Returns true if a log entry already exists for the specified column name.
		/// </summary>
		/// <param name="columnName"></param>
		/// <returns></returns>
		protected bool LogExists(string columnName)
		{
			bool ret = false;

			foreach (DataTableTransactionRecord rec in transactions)
			{
				if (rec.ColumnName == columnName)
				{
					ret = true;
					break;
				}
			}

			return ret;
		}

		/// <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 || logOnlyNewFieldChanges)
			{
				lock (transactions)
				{
					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));
				OnRowDeleting(new RowDeletedEventArgs(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);
			}
		}

		protected virtual void OnRowAdding(RowAddedEventArgs e)
		{
			if (RowAdding != null)
			{
				RowAdding(this, e);
			}
		}

		protected virtual void OnRowAdded(RowAddedEventArgs e)
		{
			if (RowAdded != null)
			{
				RowAdded(this, e);
			}
		}

		protected virtual void OnRowDeleting(RowDeletedEventArgs e)
		{
			if (RowDeleting != null)
			{
				RowDeleting(this, e);
			}
		}

		/// <summary>
		/// Eliminate non-unique field changes and remove all transactions
		/// prior to a row delete.
		/// </summary>
		public void Compact()
		{
			// Create a dictionary using the row-column name pair as the key.
			KeyedList<RowColumnKey, DataTableTransactionRecord> columnTransIndexMap=new KeyedList<RowColumnKey, DataTableTransactionRecord>();

			foreach (DataTableTransactionRecord record in transactions)
			{
				RowColumnKey key = new RowColumnKey(record.Row, record.ColumnName);

				switch (record.TransactionType)
				{
					// Accumulate all the changes for this row and column name.
					case DataTableTransactionRecord.RecordType.ChangeField:
						// If a previous entry exists for this row-column pair, then replace the value
						// with the new value.
						if (columnTransIndexMap.ContainsKey(key))
						{
							DataTableTransactionRecord r2 = columnTransIndexMap[key];
							r2.NewValue = record.NewValue;
						}
						else
						{
							// Otherwise, just create the entry for the record.
							columnTransIndexMap.Add(key, record);
						}

						break;

					// Delete all transactions referencing this row.
					case DataTableTransactionRecord.RecordType.DeleteRow:
						for (int i = columnTransIndexMap.Count - 1; i >= 0; --i)
						{
							if (columnTransIndexMap[i].Key.Row == record.Row)
							{
								columnTransIndexMap.RemoveAt(i);
							}
						}

						columnTransIndexMap.Add(key, record);
						break;

					// An add row simply creates a new entry in the dictionary.
					default:
						if (!columnTransIndexMap.ContainsKey(key))
						{
							columnTransIndexMap.Add(key, record);
						}
						else
						{
							throw new DataTableTransactionException("Duplicate row found during compaction.");
						}

						break;
				}
			}

			transactions = new List<DataTableTransactionRecord>(columnTransIndexMap.Values);
		}

		/// <summary>
		/// Deletes transactions in the logger for records where the field value equals the value specified.
		/// </summary>
		public void DeleteTransactions(string field, object val)
		{
			if ( (val == null) || (val==DBNull.Value) )
			{
				throw new ArgumentNullException("The value cannot be null or DBNull.");
			}

			if (StringHelpers.IsNullOrEmpty(field))
			{
				throw new ArgumentNullException("The field name cannot be null or empty.");
			}

			lock (transactions)
			{
				// Iterate in reverse, so we can delete rows as we go without affecting our position in the list.
				for (int i = transactions.Count - 1; i >= 0; i--)
				{
					DataTableTransactionRecord record = transactions[i];

					// Use GetGuaranteedRowValue, as we may be inspecting a deleted row.
					if (val.Equals(record.GetGuaranteedRowValue(field)))
					{
						transactions.RemoveAt(i);
					}
				}
			}
		}
	}
}

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


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