先說結論,以Dictionary來當作DGV的DataSource只能做到值的改變同步,以BindingList可以做到值的改變以及增加刪除的同步顯示。
目標就是DGV綁訂了某個DataSource,可能是List家族或者Dictionary家族的Values,當值改變時可以同步更新UI畫面。
找了好多方法後才知道原來同步的重點在於INotifyPropertyChanged這個Interface的實作,之前看人家說只要綁了BindingList就可以無敵了,自己做還是啥都沒有。
在還沒使用INotifyPropertyChanged實作前,我的DGV是這樣的:
我不論使用Dictionary還是List、IList、BindingList、IDictionary等等,
值改變後DGV不會跟著改變,但如果滑鼠點一下那一個儲存格,值就會改變,簡單說,就是不動DGV就視同不會變。
如果對著DGV使用Refresh()也是可以改變,因此在使用INotifyPropertyChanged前,我是寫個Timer去讓我的畫面2秒Refresh()一次。
以下就介紹一下我的寫法:
我最終還是選擇使用Dictionary,因為有KeyValuePair的性質可以ContainsKey,這點對我很重要,
因此放棄了新增刪除同步的實現,這部分會使用重綁DataSource來實現。
1.創建一個字典
private Dictionary<string, CommodityInfo> CommodityDict = new Dictionary<string, CommodityInfo>();
2.裡面的CommodityInfo是自訂的Class
一開始長這樣:
/// <summary> /// 初始的商品資訊Class /// </summary> public class CommodityInfo { /// <summary> /// 商品代碼 /// </summary> public string Symbol { set; get; } /// <summary> /// 商品名稱 /// </summary> public string SymbolName { set; get; } /// <summary> /// 收盤價 /// </summary> public decimal ClosePrice { set; get; } }
然後增加INotifyPropertyChanged的實作:
/// <summary> /// 繼承INotifyPropertyChanged /// </summary> public class CommodityInfo : INotifyPropertyChanged { /// <summary> /// 繼承INotifyPropertyChanged必須實作的事件 /// </summary> public event PropertyChangedEventHandler PropertyChanged; private void ValueChanged(string Property) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Property)); } #region Fields private string _Symbol; private string _SymbolName; private decimal _ClosePrice; #endregion #region Properties /// <summary> /// 商品代碼 /// </summary> public string Symbol { set { _Symbol = value; ValueChanged("Symbol"); } get { return _Symbol; } } /// <summary> /// 商品名稱 /// </summary> public string SymbolName { set { _SymbolName = value; ValueChanged("SymbolName"); } get { return _SymbolName; } } /// <summary> /// 收盤價 /// </summary> public decimal ClosePrice { set { _ClosePrice = value; ValueChanged("ClosePrice"); } get { return _ClosePrice; } } #endregion }
可以看到,被夾在Properties的region裡的變數都修改了set; 和get;
原因在於如果有{get;}屬性存在,就可以被DGV抓住當作Column。
而我們的重點在於把值重新賦予時要能觸發DGV更新,因此要把INotifyPropertyChanged的事件寫在set{}裡。
3.這裡有點小小的自我要求,如果哪一天我不喜歡SymbolName這個變數名稱了,把它改成CommodityName,
然後又剛好忘了修改後面的ValueChanged("SymbolName"),至此Compile都會過,但程式運行了就會錯。
因此如果能把"Property"變成系統指定的值,就可以在Compile甚至是編輯時就能發現錯誤。
故用了一個外部的靜態反映方法,自己的小筆記寫在側邊: http://wings890109.pixnet.net/blog/post/67862910-c%23-%E5%8F%8D%E5%B0%84%E5%8F%96%E5%BE%97class%E8%A3%A1property%E7%9A%84%E5%90%8D%E5%AD%97
學習來源: https://handcraftsman.wordpress.com/2008/11/11/how-to-get-c-property-names-without-magic-strings/
外部靜態方法:
public static class ReflectionUtility { public static string GetPropertyName<T>(Expression<Func<T>> expression) { MemberExpression body = (MemberExpression)expression.Body; return body.Member.Name; } }
Commodity的Properties部分便會修改如下:
#region Properties /// <summary> /// 商品代碼 /// </summary> public string Symbol { set { _Symbol = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.Symbol)); } get { return _Symbol; } } /// <summary> /// 商品名稱 /// </summary> public string SymbolName { set { _SymbolName = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.SymbolName)); } get { return _SymbolName; } } /// <summary> /// 收盤價 /// </summary> public decimal ClosePrice { set { _ClosePrice = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.ClosePrice)); } get { return _ClosePrice; } } #endregion
4.經常我創作的Class進入字典的Values都是在Other Thread,所以若經過PropertyChanged的事件更新DGV,容易發生跨執行緒問題。
在這個問題上,有兩種寫法,一種是讓CommodityClass多重繼承ISynchronizeInvoke,
第二種就是直接在Class裡多一個ISynchronizeInvoke的Field。
學習來源: https://stackoverflow.com/questions/1351138/bindinglist-listchanged-event
我自己時做是用第二種方法,Class內部變成如下:
private ISynchronizeInvoke _SyncObj; private Action<string> _FireValueChanged; /// <summary> /// 無作用 /// </summary> private CommodityInfo() : this(null) { } /// <summary> /// 創建式 /// </summary> /// <param name="syncObj">通常是放預備要連結的DGV的Form實作物件</param> public CommodityInfo(ISynchronizeInvoke syncObj) { //這兩行一定要放在所有Other Initialize之前, //因為一旦做了初始化,就會啟動ValueChanged,然後就會報錯 //_SyncObj尚未初始化,仍是null,因此要寫在最前面 _SyncObj = syncObj; _FireValueChanged = ValueChanged; //Other Initialize ClosePrice = 0; } /// <summary> /// 繼承INotifyPropertyChanged必須實作的事件 /// </summary> public event PropertyChangedEventHandler PropertyChanged; private void ValueChanged(string Property) { if (_SyncObj.InvokeRequired) { _FireValueChanged(Property); } else { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Property)); } }
在Form裡的呼叫方法變為:
public partial class Form1 : Form { private void Create_CommodityInfo() { CommodityInfo ci = new CommodityInfo(this); } }
因此最後整個Class變為:
/// <summary> /// 繼承INotifyPropertyChange與內部使用ISynchronizeInvoke /// </summary> public class CommodityInfo : INotifyPropertyChanged { private ISynchronizeInvoke _SyncObj; private Action<string> _FireValueChanged; /// <summary> /// 無作用 /// </summary> private CommodityInfo() : this(null) { } /// <summary> /// 創建式 /// </summary> /// <param name="syncObj">通常是放預備要連結的DGV的Form實作物件</param> public CommodityInfo(ISynchronizeInvoke syncObj) { //這兩行一定要放在所有Other Initialize之前, //因為一旦做了初始化,就會啟動ValueChanged,然後就會報錯 //_SyncObj尚未初始化,仍是null,因此要寫在最前面 _SyncObj = syncObj; _FireValueChanged = ValueChanged; //Other Initialize ClosePrice = 0; } /// <summary> /// 繼承INotifyPropertyChanged必須實作的事件 /// </summary> public event PropertyChangedEventHandler PropertyChanged; private void ValueChanged(string Property) { if (_SyncObj.InvokeRequired) { _FireValueChanged(Property); } else { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Property)); } } #region Fields private string _Symbol; private string _SymbolName; private decimal _ClosePrice; #endregion #region Properties /// <summary> /// 商品代碼 /// </summary> public string Symbol { set { _Symbol = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.Symbol)); } get { return _Symbol; } } /// <summary> /// 商品名稱 /// </summary> public string SymbolName { set { _SymbolName = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.SymbolName)); } get { return _SymbolName; } } /// <summary> /// 收盤價 /// </summary> public decimal ClosePrice { set { _ClosePrice = value; ValueChanged(ReflectionUtility.GetPropertyName(() => this.ClosePrice)); } get { return _ClosePrice; } } #endregion }
5.建立字典:
private void Create_Dict() { for (int i = 0; i < 3; i++) { CommodityDict.Add(i.ToString(), new CommodityInfo(this)); } }
6.綁定到DGV:
private void Binding_to_DGV() { dataGridView1.DataSource = new BindingSource(new BindingList<CommodityInfo>(CommodityDict.Values.ToList()), null); }
7.寫個變更隨機值的方法綁到Btn1的Click之下即可觀察結果了
private void RandomChange() { Random rnd = new Random(); CommodityDict["0"].ClosePrice = rnd.Next(0, 100); }
後記:
找到一個似乎更厲害的東西,還沒研究透,不知道對於新增刪除有沒有辦法做到一體適用
bindabledictionary
http://enlighten-media.net/blog/csharp-development/csharp-bindable-dictionary
https://stackoverflow.com/questions/3371821/serializing-a-custom-bindabledictionarytkey-tvalue
留言列表