Click here to Skip to main content
15,884,975 members
Articles / Programming Languages / C# 4.0

Relationship Oriented Programming - The IDE plus More on Agile Project Management

Rate me:
Please Sign up or sign in to vote.
4.98/5 (25 votes)
12 Mar 2012CPOL81 min read 77.5K   1.2K   49  
An Integrated Development Environment (IDE) for the Relationship Oriented Programming Tool.
/*
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.

*/

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.Tools.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>
	/// A wrapper for a transaction row, encapsulating the type of transaction, the primary
	/// key associated with the transaction, old and new values (when a field changes) and 
	/// internally manages the entire field-value list when a row is deleted.
	/// </summary>
	public class DataTableTransactionRecord
	{
		/// <summary>
		/// Enumerates the supported transaction types: add row, delete row, and change field.
		/// </summary>
		public enum TransactionType
		{
			NewRow,
			DeleteRow,
			ChangeField
		}

		/// <summary>
		/// The row to which this transaction applies.
		/// </summary>
		protected DataRow row;

		/// <summary>
		/// The transaction number.
		/// </summary>
		protected int transNum;

		/// <summary>
		/// The transaction type.
		/// </summary>
		protected TransactionType transType;

		/// <summary>
		/// The column name, used only when a field changes.
		/// </summary>
		protected string columnName;

		/// <summary>
		/// The old field value, used only when a field changes.
		/// </summary>
		protected object oldValue;

		/// <summary>
		/// The new field value, used only when a field changes.
		/// </summary>
		protected object newValue;

		/// <summary>
		/// The collection of field-values populated only when a row is deleted,
		/// so that the row can be recovered.
		/// </summary>
		protected Dictionary<string, object> columnValues;

		/// <summary>
		/// Get the state of the field-value buffer, which indicates that a the collection of field-values
		/// has been populated, thus the row has been deleted at some point.  True if the collection is
		/// populated, false if the collection is empty.
		/// </summary>
		public bool WasDeleted
		{
			get { return columnValues.Count > 0; }
		}

		/// <summary>
		/// Get the transaction number associated with this transaction.
		/// </summary>
		public int TransNum
		{
			get { return transNum; }
		}

		/// <summary>
		/// Gets the new field value.  Throws DataTableTransactionException if the transaction type is not ChangeField.
		/// </summary>
		public object NewValue
		{
			get { return newValue; }
			set { newValue = value; }
		}

		/// <summary>
		/// Gets the old field value.  Throws DataTableTransactionException if the transaction type is not ChangeField.
		/// </summary>
		public object OldValue
		{
			get { return oldValue; }
		}

		/// <summary>
		/// Gets the name of the column associated with the the transaction.  
		/// Throws DataTableTransactionException if the transaction type is not ChangeField.
		/// </summary>
		public string ColumnName
		{
			get { return columnName; }
		}

		/// <summary>
		/// Gets the transaction type.
		/// </summary>
		public TransactionType TransType
		{
			get { return transType; }
		}

		public DataRow Row
		{
			get { return row; }
			set { row = value; }
		}

		/// <summary>
		/// Constructor used to instantiate a row change (add/delete) transaction.
		/// </summary>
		/// <param name="transNum">The associated transaction number.  Can be any integer value.</param>
		/// <param name="row">The row being added/deleted.</param>
		/// <param name="transType">The transaction type.  Throws DataTableTransactionException if the transaction is ChangeField.</param>
		public DataTableTransactionRecord(int transNum, DataRow row, TransactionType transType)
		{
			if (transType == TransactionType.ChangeField)
			{
				throw new DataTableTransactionException("ChangeField transactions cannot use this constructor.");
			}

			if (row == null)
			{
				throw new ArgumentNullException("DataRow cannot be null.");
			}

			this.transNum = transNum;
			this.row = row;
			this.transType = transType;
			Initialize();
		}

		/// <summary>
		/// Constructor used to instantiate a field change transaction.
		/// </summary>
		/// <param name="transNum">The transaction number.  This can be any integer value.</param>
		/// <param name="row">The row being added/deleted.</param>
		/// <param name="columnName">The column name of the field being changed.</param>
		/// <param name="oldValue">The old field's value.</param>
		/// <param name="newValue">The new field's value.</param>
		public DataTableTransactionRecord(int transNum, DataRow row, string columnName, object oldValue, object newValue)
		{
			if (columnName == null)
			{
				throw new ArgumentNullException("Column name cannot be null.");
			}

			if (row == null)
			{
				throw new ArgumentNullException("DataRow cannot be null.");
			}

			this.transNum = transNum;
			this.row = row;
			this.transType = TransactionType.ChangeField;						// implicit based on the constructor.
			this.columnName = columnName;
			this.oldValue = oldValue;
			this.newValue = newValue;
			Initialize();
		}

		/// <summary>
		/// Adds the value associated with a field to the internal field-value collection used when deleting a row.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="val">The associated value.</param>
		public void AddColumnNameValuePair(string columnName, object val)
		{
			if (columnName == null)
			{
				throw new ArgumentNullException("Column name cannot be null.");
			}

			columnValues.Add(columnName, val);
		}

		/// <summary>
		/// Gets the value from the field-value collection for the specified field.  Used to retrieve a field value
		/// for a row that has been deleted.
		/// </summary>
		/// <param name="columnName"></param>
		/// <returns></returns>
		public object GetValue(string columnName)
		{
			if (columnName == null)
			{
				throw new ArgumentNullException("Column name cannot be null.");
			}

			return columnValues[columnName];
		}

		/// <summary>
		/// Common initialization called by constructors.
		/// </summary>
		protected void Initialize()
		{
			columnValues = new Dictionary<string, object>();
		}
	}

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

		/// <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;
				Hook();
			}
		}

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

		/// <summary>
		/// Constructor.
		/// </summary>
		public DataTableTransactionLog()
		{
			transactions = new List<DataTableTransactionRecord>();
			uncomittedRows = new Dictionary<DataRow, List<int>>();
			waitingForChangedEventList = new List<DataTableTransactionRecord>();
			doLogging = true;
		}

		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 row = r.Row;

			switch (r.TransType)
			{
				// Delete the row we added.
				case DataTableTransactionRecord.TransactionType.NewRow:
					// Only save row fields if this row is first time deleted.
					if (!r.WasDeleted)
					{
						// Save all the field values for the row being deleted.
						SaveRowFields(r, row);
					}

					// Delete the row.
					row.Delete();
					break;

				// Add the row we deleted.
				case DataTableTransactionRecord.TransactionType.DeleteRow:
					DataRow newRow = sourceTable.NewRow();

					// Restore all the field values into the new row.
					RestoreRowFields(r, newRow);
					sourceTable.Rows.Add(newRow);

					// 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;
						}
					}

					break;

				// Undo the change to field.
				case DataTableTransactionRecord.TransactionType.ChangeField:
					row[r.ColumnName] = r.OldValue;
					break;
			}

			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 row = r.Row;

			switch (r.TransType)
			{
				// Add the row that was previously added but was deleted.
				case DataTableTransactionRecord.TransactionType.NewRow:
					DataRow newRow = sourceTable.NewRow();

					// If the record is a deleted record, then we set all the field values, otherwise, just set the pk field values.
					if (r.WasDeleted)
					{
						RestoreRowFields(r, newRow);
					}

					sourceTable.Rows.Add(newRow);

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

					break;

				case DataTableTransactionRecord.TransactionType.DeleteRow:
					if (!r.WasDeleted)
					{
						SaveRowFields(r, row);
					}

					row.Delete();
					break;

				case DataTableTransactionRecord.TransactionType.ChangeField:
					row[r.ColumnName] = r.NewValue;
					break;
			}

			ResumeLogging();
		}

		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.TransactionType.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);
				}
			}
		}

		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.TransactionType.DeleteRow);
				transactions.Add(record);
				SaveRowFields(record, e.Row);
				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);
			}
		}

		/// <summary>
		/// Restores all row fields saved in the transaction record's field-value collection.
		/// </summary>
		/// <param name="record"></param>
		/// <param name="row"></param>
		protected void RestoreRowFields(DataTableTransactionRecord record, DataRow row)
		{
			foreach (DataColumn dc in row.Table.Columns)
			{
				row[dc] = record.GetValue(dc.ColumnName);
			}

		}

		/// <summary>
		/// Saves all row fields to the transaction record's field-value collection.
		/// </summary>
		/// <param name="record"></param>
		/// <param name="row"></param>
		protected void SaveRowFields(DataTableTransactionRecord record, DataRow row)
		{
			foreach (DataColumn dc in row.Table.Columns)
			{
				record.AddColumnNameValuePair(dc.ColumnName, row[dc]);
			}
		}
	}
}

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