[Windows Forms] : BindingSource使用模式 - 加強版BindingList<T>
前言 :
一般使用 BindingSource做 Data Binding的工作,不管是用 ADO.NET物件或是自訂資料物件當作資料來源。
運作流程大多類似
1.讀取資料並將資料填寫進 DataSet(or BindingList)
2.將DataSet(or BindingList)繫結至BindingSource
3.畫面Control觸發事件時,操作資料庫(or 集合)變更資料,並且操作BindingSource顯示資料。
這樣的運作流程,因為靠畫面Control觸發的事件,來當作操作函式的進入點。
把這樣的軟體架構,會顯得各層之間的職責略顯模糊。
職責模糊範例程式 : 按此下載
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data;
using System.Data.SqlClient;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Fill
BindingList<County> bindingList = new BindingList<County>();
foreach (County county in this.GetList())
{
bindingList.Add(county);
}
// Binding
countyBindingSource.DataSource = bindingList;
}
private void bindingNavigatorAddNewItem_Click(object sender, EventArgs e)
{
// Operate
County item = countyBindingSource.Current as County;
if (item != null)
{
this.Add(item);
MessageBox.Show("Database Added");
}
}
private const string _connectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf;Integrated Security=True;User Instance=True";
public void Add(County item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
SqlCommand command;
using (SqlConnection connection = new SqlConnection(_connectionString))
{
// Connection
connection.Open();
// Insert County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "INSERT INTO [CountyTable](CountyID, CountyName, CountyDescription) VALUES (@CountyID, @CountyName, @CountyDescription)";
command.Parameters.AddWithValue("@CountyID", item.CountyID);
command.Parameters.AddWithValue("@CountyName", item.CountyName);
command.Parameters.AddWithValue("@CountyDescription", item.CountyDescription);
command.ExecuteNonQuery();
}
}
}
public IEnumerable<County> GetList()
{
SqlCommand command;
using (SqlConnection connection = new SqlConnection(_connectionString))
{
// Connection
connection.Open();
// Result
List<County> itemCollection = new List<County>();
// Select County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "SELECT * FROM [CountyTable]";
using (SqlDataReader dataReader = command.ExecuteReader())
{
while (dataReader.Read())
{
County item = new County();
item.CountyID = Convert.ToInt32(dataReader["CountyID"]);
item.CountyName = Convert.ToString(dataReader["CountyName"]);
item.CountyDescription = Convert.ToString(dataReader["CountyDescription"]);
itemCollection.Add(item);
}
}
}
// Return
return itemCollection;
}
}
}
}
本篇文章介紹如何開發加強版BindingList<T>,用來將 Data Binding的運作流程作封裝。
將原本各層之間模糊不清的職責,做一定程度的分派。
讓開發人員在設計 Data Binding相關程式碼時,能將焦點集中在資料物件的操作工作上。
相關資料 :
[.NET] : BindingSource使用模式 - Data Binding基礎知識 (一)
[.NET] : BindingSource使用模式 - Data Binding基礎知識 (二)
實作 :
首先看看開發人員如何使用加強版BindingList<T>完成工作。
主要使用的介面及物件為
•StandardBindingList<T>類別,將Data Binding的運作流程封裝在內,用來取代 .NET內建提供的 System.ComponentModel.BindingList<T>。
•IStandardBindingListStrategy<T>介面,開發人員實作 IStandardBindingListStrategy<T>並且注入後,就完成 Data Binding的資料來源的開發工作。
加強版BindingList<T>範例程式 : 按此下載
先展示實際使用的程式碼及成果。
可以看到開發人員,只需要建立跟資料庫溝通的物件,就可以完成畫面到資料庫一連串的開發工作。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CLK.ComponentModel;
namespace StandardBindingListSample
{
public partial class Form1 : Form
{
// Properties
private const string _connectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\CountyBindingListStrategyDatabase.mdf;Integrated Security=True;User Instance=True";
// Constructor
public Form1()
{
InitializeComponent();
this.countyBindingSource.DataSource = new StandardBindingList<County>(new SqlCountyBindingListStrategy(_connectionString));
}
}
}
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using CLK.ComponentModel;
namespace StandardBindingListSample
{
public class SqlCountyBindingListStrategy : IStandardBindingListStrategy<County>
{
// Properties
private readonly string _connectionString = string.Empty;
// Constructor
public SqlCountyBindingListStrategy(string connectionString)
{
#region Require
if (string.IsNullOrEmpty(connectionString) == true) throw new ArgumentNullException();
#endregion
_connectionString = connectionString;
}
// Methods
private SqlConnection CreateConnection()
{
return new SqlConnection(_connectionString);
}
public void Add(County item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
SqlCommand command;
using (SqlConnection connection = this.CreateConnection())
{
// Connection
connection.Open();
// Insert County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "INSERT INTO [CountyTable](CountyID, CountyName, CountyDescription) VALUES (@CountyID, @CountyName, @CountyDescription)";
command.Parameters.AddWithValue("@CountyID", item.CountyID);
command.Parameters.AddWithValue("@CountyName", item.CountyName);
command.Parameters.AddWithValue("@CountyDescription", item.CountyDescription);
command.ExecuteNonQuery();
}
}
}
public void Modify(County item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
SqlCommand command;
using (SqlConnection connection = this.CreateConnection())
{
// Connection
connection.Open();
// Update County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "UPDATE [CountyTable] SET CountyName=@CountyName, CountyDescription=@CountyDescription WHERE CountyID=@CountyID";
command.Parameters.AddWithValue("@CountyID", item.CountyID);
command.Parameters.AddWithValue("@CountyName", item.CountyName);
command.Parameters.AddWithValue("@CountyDescription", item.CountyDescription);
command.ExecuteNonQuery();
}
}
}
public void Remove(County item)
{
#region Require
if (item == null) throw new ArgumentNullException();
#endregion
SqlCommand command;
using (SqlConnection connection = this.CreateConnection())
{
// Connection
connection.Open();
// Delete County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "DELETE FROM [CountyTable] WHERE CountyID=@CountyID";
command.Parameters.AddWithValue("@CountyID", item.CountyID);
command.ExecuteNonQuery();
}
}
}
public IEnumerable<County> GetList()
{
SqlCommand command;
using (SqlConnection connection = this.CreateConnection())
{
// Connection
connection.Open();
// Result
List<County> itemCollection = new List<County>();
// Select County
using (command = connection.CreateCommand())
{
command.CommandType = CommandType.Text;
command.CommandText = "SELECT * FROM [CountyTable]";
using (SqlDataReader dataReader = command.ExecuteReader())
{
while (dataReader.Read())
{
County item = new County();
item.CountyID = Convert.ToInt32(dataReader["CountyID"]);
item.CountyName = Convert.ToString(dataReader["CountyName"]);
item.CountyDescription = Convert.ToString(dataReader["CountyDescription"]);
itemCollection.Add(item);
}
}
}
// Return
return itemCollection;
}
}
}
}
再來展開StandardBindingList
首先是 StandardBindingObject<T>類別。
-StandardBindingObject<T>封裝實際要做 Data Binding的資料物件 T,讓後續的程式碼能夠取得資料物件 T。
-StandardBindingObject<T>提供多個屬性,讓 StandardBindingList<T>將發生過的 Data Binding流程做紀錄。
-StandardBindingObject<T>也聆聽,有實做INotifyPropertyChanged的資料物件 T,用來記錄資料變更的流程。
public class StandardBindingObject<T>
where T : class, new()
{
// Properties
private PropertyChangedEventHandler PropertyChangedDelegate { get; set; }
public T NativeBindingObject { get; private set; }
public bool IsEmptyTrack
{
get
{
if (this.IsDirty == true) return false;
if (this.IsInsertItem == true) return false;
if (this.IsRemoveItem == true) return false;
if (this.IsSetItem == true) return false;
if (this.IsClearItems == true) return false;
if (this.IsCancelNew == true) return false;
if (this.IsEndNew == true) return false;
return true;
}
}
public bool IsDirty { get; set; }
public bool IsInsertItem { get; set; }
public bool IsRemoveItem { get; set; }
public bool IsSetItem { get; set; }
public bool IsClearItems { get; set; }
public bool IsCancelNew { get; set; }
public bool IsEndNew { get; set; }
// Constructor
public StandardBindingObject() : this(new T()) { }
public StandardBindingObject(T nativeBindingObject)
{
#region Require
if (nativeBindingObject == null) throw new ArgumentNullException("nativeBindingObject");
#endregion
// Properties
this.PropertyChangedDelegate = delegate(object sender, PropertyChangedEventArgs e) { this.IsDirty = true; };
this.NativeBindingObject = nativeBindingObject;
this.ResetTrack();
}
// Methods
public void ResetTrack()
{
this.IsDirty = false;
this.IsInsertItem = false;
this.IsRemoveItem = false;
this.IsSetItem = false;
this.IsClearItems = false;
this.IsCancelNew = false;
this.IsEndNew = false;
}
public void HookPropertyChanged()
{
// INotifyPropertyChanged
INotifyPropertyChanged notifyPropertyChanged = this.NativeBindingObject as INotifyPropertyChanged;
if (notifyPropertyChanged != null) notifyPropertyChanged.PropertyChanged += this.PropertyChangedDelegate;
}
public void UnhookPropertyChanged()
{
// INotifyPropertyChanged
INotifyPropertyChanged notifyPropertyChanged = this.NativeBindingObject as INotifyPropertyChanged;
if (notifyPropertyChanged != null) notifyPropertyChanged.PropertyChanged -= this.PropertyChangedDelegate;
}
}
再來是 StandardBindingPropertyDescriptor<T>類別。
-StandardBindingPropertyDescriptor<T>封裝實際要做 Data Binding的資料物件 T的屬性物件 PropertyDescriptor,讓自己對外表現的就跟被封裝的物件一樣。
-StandardBindingPropertyDescriptor<T>內部存取資料物件的屬性時,是讀取 StandardBindingObject<T>封裝的資料物件 T的屬性。
-StandardBindingPropertyDescriptor<T>透過 PropertyDescriptor的機制,聆聽沒有實做INotifyPropertyChanged的資料物件 T的屬性資料變更,並用StandardBindingObject<T>來記錄變化。
public sealed class StandardBindingPropertyDescriptor<T> : PropertyDescriptor
where T : class, new()
{
// Properties
private readonly PropertyDescriptor _nativeBindingPropertyDescriptor = null;
private readonly bool _raiseStandardBindingObjectSetDirty = false;
// Constructor
public StandardBindingPropertyDescriptor(PropertyDescriptor nativeBindingPropertyDescriptor)
: base(nativeBindingPropertyDescriptor)
{
#region Require
if (nativeBindingPropertyDescriptor == null) throw new ArgumentNullException("component");
#endregion
_nativeBindingPropertyDescriptor = nativeBindingPropertyDescriptor;
if (typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(T)) == false) _raiseStandardBindingObjectSetDirty = true;
}
// Properties
public override Type ComponentType
{
get
{
return _nativeBindingPropertyDescriptor.ComponentType;
}
}
public override TypeConverter Converter
{
get
{
return _nativeBindingPropertyDescriptor.Converter;
}
}
public override bool IsLocalizable
{
get
{
return _nativeBindingPropertyDescriptor.IsLocalizable;
}
}
public override bool IsReadOnly
{
get
{
return _nativeBindingPropertyDescriptor.IsReadOnly;
}
}
public override Type PropertyType
{
get
{
return _nativeBindingPropertyDescriptor.PropertyType;
}
}
// Methods
private void GetBindingObject(object component, out StandardBindingObject<T> standardBindingObject, out T nativeBindingObject)
{
#region Require
if (component == null) throw new ArgumentNullException("component");
#endregion
// StandardBindingObject
standardBindingObject = component as StandardBindingObject<T>;
if (standardBindingObject == null) throw new ArgumentNullException("standardBindingObject");
// NativeBindingObject
nativeBindingObject = standardBindingObject.NativeBindingObject;
if (nativeBindingObject == null) throw new ArgumentNullException("nativeBindingObject");
}
private StandardBindingObject<T> GetStandardBindingObject(object component)
{
#region Require
if (component == null) throw new ArgumentNullException("component");
#endregion
// GetBindingObject
StandardBindingObject<T> standardBindingObject = null;
T nativeBindingObject = null;
this.GetBindingObject(component, out standardBindingObject, out nativeBindingObject);
// Return
return standardBindingObject;
}
private T GetNativeBindingObject(object component)
{
#region Require
if (component == null) throw new ArgumentNullException("component");
#endregion
// GetBindingObject
StandardBindingObject<T> standardBindingObject = null;
T nativeBindingObject = null;
this.GetBindingObject(component, out standardBindingObject, out nativeBindingObject);
// Return
return nativeBindingObject;
}
public override void SetValue(object component, object value)
{
// GetBindingObject
StandardBindingObject<T> standardBindingObject = null;
T nativeBindingObject = null;
this.GetBindingObject(component, out standardBindingObject, out nativeBindingObject);
// RaiseStandardBindingObjectSetDirty
if (_raiseStandardBindingObjectSetDirty == false)
{
// SetValue
_nativeBindingPropertyDescriptor.SetValue(nativeBindingObject, value);
}
else
{
// SetDirty
EventHandler setDirtyDelegate = delegate(object sender, EventArgs e)
{
standardBindingObject.IsDirty = true;
};
// SetValue
_nativeBindingPropertyDescriptor.AddValueChanged(nativeBindingObject, setDirtyDelegate);
_nativeBindingPropertyDescriptor.SetValue(nativeBindingObject, value);
_nativeBindingPropertyDescriptor.RemoveValueChanged(nativeBindingObject, setDirtyDelegate);
}
}
public override object GetValue(object component)
{
return _nativeBindingPropertyDescriptor.GetValue(this.GetNativeBindingObject(component));
}
public override void ResetValue(object component)
{
_nativeBindingPropertyDescriptor.ResetValue(this.GetNativeBindingObject(component));
}
public override bool CanResetValue(object component)
{
return _nativeBindingPropertyDescriptor.CanResetValue(this.GetNativeBindingObject(component));
}
public override bool ShouldSerializeValue(object component)
{
return _nativeBindingPropertyDescriptor.ShouldSerializeValue(this.GetNativeBindingObject(component));
}
public override object GetEditor(Type editorBaseType)
{
return _nativeBindingPropertyDescriptor.GetEditor(editorBaseType);
}
public override PropertyDescriptorCollection GetChildProperties(object instance, Attribute[] filter)
{
return _nativeBindingPropertyDescriptor.GetChildProperties(this.GetNativeBindingObject(instance), filter);
}
public override void AddValueChanged(object component, EventHandler handler)
{
_nativeBindingPropertyDescriptor.AddValueChanged(this.GetNativeBindingObject(component), handler);
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
_nativeBindingPropertyDescriptor.RemoveValueChanged(this.GetNativeBindingObject(component), handler);
}
}
再來定義 IStandardBindingListStrategy<T>介面。
-IStandardBindingListStrategy<T>定義,要做 Data Binding的資料物件 T,進出系統邊界應該要實作的功能。
public interface IStandardBindingListStrategy<T>
where T : class, new()
{
void Add(T item);
void Modify(T item);
void Remove(T item);
IEnumerable<T> GetList();
}
最後是StandardBindingList<T>類別。
-StandardBindingList<T>將Data Binding的運作流程封裝在內,用來取代 .NET內建提供的 System.ComponentModel.BindingList<T>。
-StandardBindingList<T>透過繼承 Override的方式來聆聽發生過的 Data Binding流程,使用 StandardBindingObject<T>來記錄變化。
-StandardBindingList<T>實作了ITypedList介面,取代原本 Data Binding流程裡取得PropertyDescriptor的流程。將原本應該取得資料物件T的屬性物件,改為取得StandardBindingPropertyDescriptor<T>。
-StandardBindingList<T>開放了Refresh()函式,執行這個函式StandardBindingList<T>就會透過IStandardBindingListStrategy<T>取得資料物件 T的資料做 Data Binding的動作。
-StandardBindingList<T>會在每個 Data Binding流程裡,檢查StandardBindingObject<T>發生過的紀錄。當記錄滿足條件,就會呼叫IStandardBindingListStrategy<T>的函式處理資料物件 T。
public class StandardBindingList<T> : BindingList<StandardBindingObject<T>>, ITypedList
where T : class, new()
{
// Properties
private readonly IStandardBindingListStrategy<T> _strategy = null;
private readonly PropertyDescriptorCollection _propertyDescriptorCollection = null;
private bool _isRefreshing = false;
// Constructor
public StandardBindingList(IStandardBindingListStrategy<T> strategy) : this(strategy, true) { }
public StandardBindingList(IStandardBindingListStrategy<T> strategy, bool runRefresh)
{
#region Require
if (strategy == null) throw new ArgumentNullException();
#endregion
// Properties
_strategy = strategy;
_propertyDescriptorCollection = this.CreateStandardBindingPropertyDescriptorCollection();
// Refresh
if (runRefresh == true)
{
this.Refresh();
}
}
// Methods
private PropertyDescriptorCollection CreateStandardBindingPropertyDescriptorCollection()
{
// Result
List<PropertyDescriptor> standardBindingPropertyDescriptorCollection = new List<PropertyDescriptor>();
// Create
foreach (PropertyDescriptor nativePropertyDescriptor in TypeDescriptor.GetProperties(typeof(T)))
{
standardBindingPropertyDescriptorCollection.Add(new StandardBindingPropertyDescriptor<T>(nativePropertyDescriptor));
}
// Return
return new PropertyDescriptorCollection(standardBindingPropertyDescriptorCollection.ToArray());
}
private void CommitTrack(StandardBindingObject<T> standardBindingObject)
{
#region Require
if (standardBindingObject == null) throw new ArgumentNullException("standardBindingObject");
#endregion
if (_isRefreshing == false)
{
if (standardBindingObject.IsEmptyTrack == false)
{
if (this.CommitTrack(standardBindingObject, _strategy) == true)
{
standardBindingObject.ResetTrack();
}
}
}
}
protected virtual bool CommitTrack(StandardBindingObject<T> standardBindingObject, IStandardBindingListStrategy<T> strategy)
{
#region Require
if (standardBindingObject == null) throw new ArgumentNullException("standardBindingObject");
if (strategy == null) throw new ArgumentNullException("strategy");
#endregion
// Add
if (standardBindingObject.IsInsertItem == true)
{
if (standardBindingObject.IsRemoveItem == false)
{
if (standardBindingObject.IsCancelNew == false)
{
if (standardBindingObject.IsEndNew == true)
{
strategy.Add(standardBindingObject.NativeBindingObject);
return true;
}
}
}
}
// Remove
if (standardBindingObject.IsInsertItem == false)
{
if (standardBindingObject.IsCancelNew == false)
{
if (standardBindingObject.IsRemoveItem == true)
{
strategy.Remove(standardBindingObject.NativeBindingObject);
return true;
}
}
}
// Modify
if (standardBindingObject.IsInsertItem == false)
{
if (standardBindingObject.IsRemoveItem == false)
{
if (standardBindingObject.IsCancelNew == false)
{
if (standardBindingObject.IsEndNew == true)
{
if (standardBindingObject.IsDirty == true)
{
strategy.Modify(standardBindingObject.NativeBindingObject);
return true;
}
}
}
}
}
// Return
return false;
}
public void Refresh()
{
try
{
// BeginRefresh
_isRefreshing = true;
// Clear
this.Clear();
// Add
foreach (T item in _strategy.GetList())
{
StandardBindingObject<T> standardBindingObject = new StandardBindingObject<T>(item);
this.Add(standardBindingObject);
standardBindingObject.ResetTrack();
}
}
finally
{
// EndRefresh
_isRefreshing = false;
}
// ResetBindings
this.ResetBindings();
}
#region BindingList<T>
protected override void OnListChanged(ListChangedEventArgs e)
{
#region Require
if (e == null) throw new ArgumentNullException("e");
#endregion
if (_isRefreshing == false)
{
base.OnListChanged(e);
}
}
#endregion
#region Collection<T>
protected override void InsertItem(int index, StandardBindingObject<T> item)
{
#region Require
if (item == null) throw new ArgumentNullException("item");
#endregion
// Base
base.InsertItem(index, item);
// NewItem
item.HookPropertyChanged();
item.IsInsertItem = true;
this.CommitTrack(item);
}
protected override void SetItem(int index, StandardBindingObject<T> item)
{
#region Require
if (item == null) throw new ArgumentNullException("item");
#endregion
// OldItem
StandardBindingObject<T> oldItem = this[index];
oldItem.UnhookPropertyChanged();
// Base
base.SetItem(index, item);
// NewItem
item.HookPropertyChanged();
item.IsSetItem = true;
this.CommitTrack(item);
}
protected override void RemoveItem(int index)
{
// OldItem
StandardBindingObject<T> oldItem = this[index];
oldItem.UnhookPropertyChanged();
oldItem.IsRemoveItem = true;
this.CommitTrack(oldItem);
// Base
base.RemoveItem(index);
}
protected override void ClearItems()
{
// OldItem
foreach (StandardBindingObject<T> oldItem in this.Items.ToArray())
{
oldItem.UnhookPropertyChanged();
oldItem.IsClearItems = true;
this.CommitTrack(oldItem);
}
// Base
base.ClearItems();
}
#endregion
#region ICancelAddNew
public override void CancelNew(int itemIndex)
{
// StandardBindingObject
if (0 <= itemIndex && itemIndex < this.Count)
{
StandardBindingObject<T> standardBindingObject = this[itemIndex];
standardBindingObject.IsCancelNew = true;
this.CommitTrack(standardBindingObject);
}
// Base
base.CancelNew(itemIndex);
}
public override void EndNew(int itemIndex)
{
// StandardBindingObject
if (0 <= itemIndex && itemIndex < this.Count)
{
StandardBindingObject<T> standardBindingObject = this[itemIndex];
standardBindingObject.IsEndNew = true;
this.CommitTrack(standardBindingObject);
}
// Base
base.EndNew(itemIndex);
}
#endregion
#region ITypedList
public string GetListName(PropertyDescriptor[] listAccessors)
{
return typeof(StandardBindingObject<T>).Name;
}
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
if (listAccessors != null && listAccessors.Length > 0)
{
throw new InvalidOperationException();
// return this.CreateStandardBindingPropertyDescriptorCollection(ListBindingHelper.GetListItemProperties(listAccessors[0].PropertyType));
}
else
{
return _propertyDescriptorCollection;
}
}
#endregion
}
後記 :
StandardBindingList
這些功能將會在後續的文章內一一實作,不過都還是以本章節的思路來做擴充。
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。