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