Click here to Skip to main content
15,891,567 members
Articles / Desktop Programming / Windows Forms

Do You Really Want To Be Agile?

Rate me:
Please Sign up or sign in to vote.
4.91/5 (50 votes)
29 Dec 2011CPOL44 min read 98.6K   735   112  
A walk on the wild side using Relationship Oriented Programming.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Reflection;

namespace Clifton.Tools.Data
{
	/// <summary>
	/// Used as a key to index the ColumnBinder instance.
	/// The key is a composite of the destination object and property.
	/// This composite key maps to one and only one ColumnBinder, because
	/// an object's property can map to only one column in a table.
	/// </summary>
	public struct PropertyBinding
	{
		public object destObject;
		public string destProperty;

		public PropertyBinding(object destObject, string destProperty)
		{
			this.destObject = destObject;
			this.destProperty = destProperty;
		}
	}

	public class TableBindHelperException : ApplicationException
	{
		public TableBindHelperException(string msg)
			: base(msg)
		{
		}
	}

	/// <summary>
	/// Container for mapping a column bound to an object-property.
	/// </summary>
	public class ColumnBinder
	{
		protected string columnName;
		protected object destObject;
		protected string destProperty;
		protected PropertyInfo propInfo;
		protected TableBindHelper tableBindHelper;

		public string ColumnName
		{
			get {return columnName;}
		}

		public object Object
		{
			get {return destObject;}
		}

		public PropertyInfo PropertyInfo
		{
			get { return propInfo; }
		}

		public ColumnBinder(TableBindHelper tableBindHelper, string columnName, object destObject, string destProperty)
		{
			this.tableBindHelper = tableBindHelper;
			this.columnName = columnName;
			this.destObject = destObject;
			this.destProperty = destProperty;
			propInfo = destObject.GetType().GetProperty(destProperty);
		}

		public void CreatePropertyWatcher()
		{
			Type eventType = destObject.GetType();
			EventInfo eventInfo = eventType.GetEvent(destProperty+"Changed");
			eventInfo.AddEventHandler(destObject, new EventHandler(OnDestinationChanged));
		}

		protected void OnDestinationChanged(object sender, EventArgs e)
		{
			// Reformat the event and pass it on to the handler that works with INotifyPropertyChanged objects.
			tableBindHelper.OnDestinationChanged(sender, new PropertyChangedEventArgs(destProperty));
		}
	}

	/// <summary>
	/// Provides binding between a DataTable and another object, without requiring the System.Windows.Forms namespace.
	/// Also implements a row cursor for record navigation and automatic object updating.
	/// </summary>
	public class TableBindHelper
	{
		protected DataTable table;
		protected int rowIdx;
		protected Dictionary<string, ColumnBinder> columnBinders;
		protected Dictionary<PropertyBinding, ColumnBinder> propertyBinders;

		/// <summary>
		/// Get/set the current row being bound.
		/// </summary>
		public int RowIndex
		{
			get { return RowIndex; }
			set { SetRowIndex(value); }
		}

		public TableBindHelper(DataTable table)
		{
			this.table = table;
			columnBinders = new Dictionary<string, ColumnBinder>();
			propertyBinders = new Dictionary<PropertyBinding, ColumnBinder>();
			table.ColumnChanged += new DataColumnChangeEventHandler(OnColumnChanged);
			table.RowDeleted += new DataRowChangeEventHandler(OnRowDeleted);
			table.RowChanged += new DataRowChangeEventHandler(OnRowChanged);
		}

		public void AddColumnBinder(string columnName, object dest, string propertyName)
		{
			AddColumnBinder(columnName, dest, propertyName, false);
		}

		/// <summary>
		/// Add a binding to a column of the table, whose target is the supplied object
		/// and property.
		/// </summary>
		public void AddColumnBinder(string columnName, object dest, string propertyName, bool useLegacyChangeEvent)
		{
			ColumnBinder cb = new ColumnBinder(this, columnName, dest, propertyName);
			columnBinders[columnName] = cb;
			PropertyBinding db = new PropertyBinding(dest, propertyName);
			propertyBinders[db]=cb;

			if ( (dest is INotifyPropertyChanged) && (!useLegacyChangeEvent) )
			{
				// Create a generic property watcher.
				CreatePropertyWatcher(dest, propertyName);
			}
			else
			{
				// Create the event sink in the container that knows about the 
				// the property name.
				cb.CreatePropertyWatcher();
			}

			if (rowIdx < table.Rows.Count)
			{
				object val = table.Rows[rowIdx][cb.ColumnName];
				UpdateTargetWithValue(cb, val);
			}
		}

		/// <summary>
		/// Adjusts the row index if the row index now is greater than the number
		/// of available rows.  Updates all destination objects to reflect a possible
		/// change in the data that the row index is indexing.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected void OnRowDeleted(object sender, DataRowChangeEventArgs e)
		{
			if (rowIdx >= table.Rows.Count)
			{
				// Can result in rowIdx set to -1.
				rowIdx = table.Rows.Count - 1;
			}

			if (rowIdx >= 0)
			{
				UpdateAllDestinationObjects();
			}
		}

		/// <summary>
		/// Updates the row index to the row being added.
		/// </summary>
		protected void OnRowChanged(object sender, DataRowChangeEventArgs e)
		{
			if (e.Action == DataRowAction.Add)
			{
				RowIndex = FindRow(e.Row);
			}
		}

		protected int FindRow(DataRow row)
		{
			int idx=0;
			bool found = false;

			foreach (DataRow tableRow in table.Rows)
			{
				if (tableRow == row)
				{
					++idx;
					found = true;
					break;
				}
			}

			if (!found)
			{
				throw new TableBindHelperException("The added row can not be found in the table.");
			}

			return idx;
		}

		/// <summary>
		/// Called when the column value is changed, so any bound object can have its
		/// property updated.
		/// </summary>
		protected void OnColumnChanged(object sender, DataColumnChangeEventArgs e)
		{
			ColumnBinder cb = null;
			bool ret = columnBinders.TryGetValue(e.Column.ColumnName, out cb);

			if (ret)
			{
				UpdateTargetWithValue(cb, e.ProposedValue);
			}
		}

		/// <summary>
		/// Updates the target with the column value, handling DBNull.Value.
		/// </summary>
		protected void UpdateTargetWithValue(ColumnBinder cb, object val)
		{
			if ( (val == null) || (val==DBNull.Value) )
			{
				// TODO: We need a more sophisticated way of:
				// 1: does the target handle null/DBNull.Value itself?
				// 2: specifying the default value associated with a property.
				val = String.Empty;
			}

			cb.PropertyInfo.SetValue(cb.Object, val, null);
		}

		/// <summary>
		/// Using the INotifyPropertyChanged interface, which requires the implementation
		/// of the PropertyChanged event, this method wires up a generic handler.
		/// </summary>
		protected void CreatePropertyWatcher(object dest, string propertyName)
		{
			// string eventName = propertyName + "Changed";
			Type eventType = dest.GetType();
			EventInfo eventInfo = eventType.GetEvent("PropertyChanged");
			eventInfo.AddEventHandler(dest, new PropertyChangedEventHandler(OnDestinationChanged));
		}

		/// <summary>
		/// Called when the bound object's value changes, so that the change can be
		/// reflected in the associated table's row and the bound column.
		/// </summary>
		public void OnDestinationChanged(object sender, PropertyChangedEventArgs e)
		{
			PropertyBinding db = new PropertyBinding(sender, e.PropertyName);
			ColumnBinder cb;
			bool ret = propertyBinders.TryGetValue(db, out cb);

			if (ret)
			{
				UpdateTablePropertyValue(cb);
			}
		}

		/// <summary>
		/// Updates the destiniation property value with the associated column
		/// in the current indexed row.
		/// </summary>
		protected void UpdateTablePropertyValue(ColumnBinder cb)
		{
			if (rowIdx < table.Rows.Count)
			{
				object val = cb.PropertyInfo.GetValue(cb.Object, null);

				if (val == null)
				{
					val = DBNull.Value;
				}

				table.Rows[rowIdx][cb.ColumnName] = val;
			}
		}

		/// <summary>
		/// Sets the current row index, updating all bound target objects and their properties.
		/// </summary>
		protected void SetRowIndex(int val)
		{
			if (val < 0)
			{
				throw new ArgumentOutOfRangeException("RowIndex cannot be < 0");
			}

			if (val >= table.Rows.Count)
			{
				throw new ArgumentOutOfRangeException("RowIndex cannot be >= table.Rows.Count");
			}

			if (rowIdx != val)
			{
				rowIdx = val;
				UpdateAllDestinationObjects();
			}
		}

		/// <summary>
		/// Updates all bound target object and their properties to the current row index.
		/// </summary>
		protected void UpdateAllDestinationObjects()
		{
			foreach (ColumnBinder cb in columnBinders.Values)
			{
				UpdateTargetWithValue(cb, table.Rows[rowIdx][cb.ColumnName]);
			}
		}
	}
}

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