先說結論,以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
