先說結論,以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的實作:

學習來源: https://stackoverflow.com/questions/3340192/datagridview-bound-to-bindinglist-does-not-refresh-when-value-changed

    /// <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://referencesource.microsoft.com/#system/compmod/system/componentmodel/BindingList.cs,e63bae806724bf91

https://stackoverflow.com/questions/3371821/serializing-a-custom-bindabledictionarytkey-tvalue

https://social.msdn.microsoft.com/Forums/windows/en-US/6b0a83a3-76bb-4c7b-ac91-5467e5776850/problem-with-display-and-valuemember-when-binding-a-custom-dictionary-to-a-combobox?forum=winformsdatacontrols

arrow
arrow

    wings890109 發表在 痞客邦 留言(0) 人氣()