トップ回答者
BindingSourceにINotifyPropertyChangedを実装したオブジェクトを当てた場合の挙動に関して

質問
-
環境:Windows7 pro / Visual Studio 2010 pro / C# .NET4.0 Windowsフォーム
いつもお世話になります。
後学のために教えていただければと思います。
「INotifyPropertyChanged Interface」
https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged(v=vs.85)
表題に関して、上記のMSDNのサイトのCommunity Additionsに次のことが書かれています。
「INotifyPropertyChangedを実装し、それをBindingSourceにバインドするオブジェクトを使用する場合、細かいプロパティの通知は適切にサポートされていないことに注意してください。 つまり、変更された単一のプロパティーを指定することはできますが、すべてのプロパティーを読み取ってバインディングをリフレッシュします。」 (by google翻訳)
確かに、PropertyChangedEventHandlerの引数PropertyChangedEventArgsに関係なく、すべてのプロパティのGetが呼ばれます。
これを回避する(変更されたプロパティのみ読み取られるようにする)にはどのような方法があるのでしょうか。
(INotifyPropertyChangedを実装したオブジェクトは使用し、双方向データバインドを実現する)
よろしくお願いいたします。
回答
-
BindingSourceはDataSourceにセットしたリストのアイテムの変化に対してListChangedイベントを出して、DataGridViewはその変化したアイテムに対応する行の表示を更新します。
行の表示を更新するためには、その行に含まれる列に対応する元のプロパティを読み取ります。
そのため変更していないプロパティ以外が読み取られているように見えますが、そう見えるだけです。それが気に入らないというのであれば、BindingSourceのListChagnedイベントの発生を抑制し、行単位で無く、対応するセルのみが表示更新されるようにすれば良いという事です。
#そこまでやってもウィンドウが隠れたりして再描画が発生すれば読み取りが発生するので意味は無いと思いますがあるいはWPFのようにINotifyPropertyChangedを強力に処理してくれるフレームワークを使えば回避できるでしょう。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : System.Windows.Forms.Form { private Button changeItemBtn = new Button(); private DataGridView customersDataGridView = new DataGridView(); private BindingSourceEx customersBindingSource = new BindingSourceEx(); public Form1() { this.InitializeComponent(); this.changeItemBtn.Text = "Change Item"; this.changeItemBtn.Dock = DockStyle.Bottom; this.changeItemBtn.Click += new EventHandler(changeItemBtn_Click); this.Controls.Add(this.changeItemBtn); customersDataGridView.Dock = DockStyle.Top; this.Controls.Add(customersDataGridView); this.Size = new Size(800, 200); this.Load += new EventHandler(Form1_Load); } List<DemoCustomer> customerList = new List<DemoCustomer>(); private void Form1_Load(System.Object sender, System.EventArgs e) { customerList.Add(DemoCustomer.CreateNewCustomer()); customerList.Add(DemoCustomer.CreateNewCustomer()); customerList.Add(DemoCustomer.CreateNewCustomer()); this.customersBindingSource.DataSource = customerList; this.customersDataGridView.DataSource = this.customersBindingSource; //ソースの各要素内のプロパティが変化したことを検出するイベントを登録 this.customersBindingSource.ListItemPropertyChanged += OnListItemPropertyChanged; } private void OnListItemPropertyChanged(object sender, ListItemPropertyChangedEventArgs e) { foreach (DataGridViewRow row in this.customersDataGridView.Rows) { if (row.DataBoundItem == e.Item) { foreach (DataGridViewColumn clm in this.customersDataGridView.Columns) { if (clm.DataPropertyName == e.PropertyName) { //変化したセルだけ描画を更新させる var cell = row.Cells[clm.Name]; this.customersDataGridView.InvalidateCell(cell); } } break; } } } void changeItemBtn_Click(object sender, EventArgs e) { List<DemoCustomer> customerList = this.customersBindingSource.DataSource as List<DemoCustomer>; customerList[0].CompanyName = "Tailspin Toys"; } //[STAThread] //static void Main() //{ // Application.EnableVisualStyles(); // Application.Run(new Form1()); //} } public class DemoCustomer : INotifyPropertyChanged { private Guid idValue = Guid.NewGuid(); private string customerName = String.Empty; private string companyNameValue = String.Empty; private string phoneNumberValue = String.Empty; public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } private DemoCustomer() { customerName = "no data"; companyNameValue = "no data"; phoneNumberValue = "no data"; } public static DemoCustomer CreateNewCustomer() { return new DemoCustomer(); } public Guid ID { get { System.Diagnostics.Debug.WriteLine("Get ID"); return this.idValue; } } public string CompanyName { get { System.Diagnostics.Debug.WriteLine("Get CompanyName"); return this.companyNameValue; } set { if (value != this.companyNameValue) { this.companyNameValue = value; NotifyPropertyChanged("CompanyName"); } } } public string PhoneNumber { get { System.Diagnostics.Debug.WriteLine("Get PhoneNumber"); return this.phoneNumberValue; } set { if (value != this.phoneNumberValue) { this.phoneNumberValue = value; NotifyPropertyChanged("PhoneNumber"); } } } } class BindingSourceEx : BindingSource, IListItemPropertyChanged { /// <summary>ソースの要素にあるプロパティの一覧</summary> private PropertyDescriptorCollection properties; /// <summary>ソースの要素一覧保持用</summary> private object[] items; /// <summary></summary> protected override void OnDataSourceChanged(EventArgs e) { Disconnect(); base.OnDataSourceChanged(e); Connect(); } /// <summary>ソースの要素にあるプロパティの一覧取得</summary> public override PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { this.properties = base.GetItemProperties(listAccessors); return properties; } /// <summary>ソースリストが何か変化した</summary> protected override void OnListChanged(ListChangedEventArgs e) { switch (e.ListChangedType) { case ListChangedType.ItemChanged: //要素のプロパティが変化したことを通知しないことでDataGridViewの行単位での表示更新を抑制 break; default: Disconnect(); Connect(); base.OnListChanged(e); break; } } /// <summary>要素のNotifyPropertyChangedイベントを捕捉させる</summary> private void Connect() { items = base.List.OfType<object>().ToArray(); if (this.properties == null || this.properties.Count == 0) { this.properties = GetItemProperties(null); } if (this.properties != null && items != null) { foreach (PropertyDescriptor pd in this.properties) { foreach (object o in items) { pd.AddValueChanged(o, OnItemPropertyChanged); } } } } /// <summary>要素のNotifyPropertyChangedイベントを捕捉解除</summary> private void Disconnect() { if (this.properties != null && items != null) { foreach (PropertyDescriptor pd in this.properties) { foreach (object o in items) { pd.RemoveValueChanged(o, OnItemPropertyChanged); } } items = null; } } /// <summary>要素のプロパティが変化した</summary> private void OnItemPropertyChanged(object sender, EventArgs e) { var lipc = ListItemPropertyChanged; if (lipc != null) { System.ComponentModel.PropertyChangedEventArgs pce = e as System.ComponentModel.PropertyChangedEventArgs; if (pce != null) { //要素のプロパティ変化のイベントを発生させる var lipce = new ListItemPropertyChangedEventArgs(pce.PropertyName); lipce.Item = sender; lipc(this, lipce); } } } public event EventHandler<ListItemPropertyChangedEventArgs> ListItemPropertyChanged; protected override void Dispose(bool disposing) { Disconnect(); base.Dispose(disposing); } } public interface IListItemPropertyChanged { event EventHandler<ListItemPropertyChangedEventArgs> ListItemPropertyChanged; } public class ListItemPropertyChangedEventArgs : PropertyChangedEventArgs { public ListItemPropertyChangedEventArgs(string propertyName) : base(propertyName) { } public object Item { get; set; } } }
#NotifyPropertyChangedイベントの捕捉は適当に書いたのでたぶんリークしてます
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年9月27日 0:17
- 回答としてマーク Ludnes 2017年9月29日 15:30
すべての返信
-
BindingSourceはDataSourceにセットしたリストのアイテムの変化に対してListChangedイベントを出して、DataGridViewはその変化したアイテムに対応する行の表示を更新します。
行の表示を更新するためには、その行に含まれる列に対応する元のプロパティを読み取ります。
そのため変更していないプロパティ以外が読み取られているように見えますが、そう見えるだけです。それが気に入らないというのであれば、BindingSourceのListChagnedイベントの発生を抑制し、行単位で無く、対応するセルのみが表示更新されるようにすれば良いという事です。
#そこまでやってもウィンドウが隠れたりして再描画が発生すれば読み取りが発生するので意味は無いと思いますがあるいはWPFのようにINotifyPropertyChangedを強力に処理してくれるフレームワークを使えば回避できるでしょう。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : System.Windows.Forms.Form { private Button changeItemBtn = new Button(); private DataGridView customersDataGridView = new DataGridView(); private BindingSourceEx customersBindingSource = new BindingSourceEx(); public Form1() { this.InitializeComponent(); this.changeItemBtn.Text = "Change Item"; this.changeItemBtn.Dock = DockStyle.Bottom; this.changeItemBtn.Click += new EventHandler(changeItemBtn_Click); this.Controls.Add(this.changeItemBtn); customersDataGridView.Dock = DockStyle.Top; this.Controls.Add(customersDataGridView); this.Size = new Size(800, 200); this.Load += new EventHandler(Form1_Load); } List<DemoCustomer> customerList = new List<DemoCustomer>(); private void Form1_Load(System.Object sender, System.EventArgs e) { customerList.Add(DemoCustomer.CreateNewCustomer()); customerList.Add(DemoCustomer.CreateNewCustomer()); customerList.Add(DemoCustomer.CreateNewCustomer()); this.customersBindingSource.DataSource = customerList; this.customersDataGridView.DataSource = this.customersBindingSource; //ソースの各要素内のプロパティが変化したことを検出するイベントを登録 this.customersBindingSource.ListItemPropertyChanged += OnListItemPropertyChanged; } private void OnListItemPropertyChanged(object sender, ListItemPropertyChangedEventArgs e) { foreach (DataGridViewRow row in this.customersDataGridView.Rows) { if (row.DataBoundItem == e.Item) { foreach (DataGridViewColumn clm in this.customersDataGridView.Columns) { if (clm.DataPropertyName == e.PropertyName) { //変化したセルだけ描画を更新させる var cell = row.Cells[clm.Name]; this.customersDataGridView.InvalidateCell(cell); } } break; } } } void changeItemBtn_Click(object sender, EventArgs e) { List<DemoCustomer> customerList = this.customersBindingSource.DataSource as List<DemoCustomer>; customerList[0].CompanyName = "Tailspin Toys"; } //[STAThread] //static void Main() //{ // Application.EnableVisualStyles(); // Application.Run(new Form1()); //} } public class DemoCustomer : INotifyPropertyChanged { private Guid idValue = Guid.NewGuid(); private string customerName = String.Empty; private string companyNameValue = String.Empty; private string phoneNumberValue = String.Empty; public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } private DemoCustomer() { customerName = "no data"; companyNameValue = "no data"; phoneNumberValue = "no data"; } public static DemoCustomer CreateNewCustomer() { return new DemoCustomer(); } public Guid ID { get { System.Diagnostics.Debug.WriteLine("Get ID"); return this.idValue; } } public string CompanyName { get { System.Diagnostics.Debug.WriteLine("Get CompanyName"); return this.companyNameValue; } set { if (value != this.companyNameValue) { this.companyNameValue = value; NotifyPropertyChanged("CompanyName"); } } } public string PhoneNumber { get { System.Diagnostics.Debug.WriteLine("Get PhoneNumber"); return this.phoneNumberValue; } set { if (value != this.phoneNumberValue) { this.phoneNumberValue = value; NotifyPropertyChanged("PhoneNumber"); } } } } class BindingSourceEx : BindingSource, IListItemPropertyChanged { /// <summary>ソースの要素にあるプロパティの一覧</summary> private PropertyDescriptorCollection properties; /// <summary>ソースの要素一覧保持用</summary> private object[] items; /// <summary></summary> protected override void OnDataSourceChanged(EventArgs e) { Disconnect(); base.OnDataSourceChanged(e); Connect(); } /// <summary>ソースの要素にあるプロパティの一覧取得</summary> public override PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors) { this.properties = base.GetItemProperties(listAccessors); return properties; } /// <summary>ソースリストが何か変化した</summary> protected override void OnListChanged(ListChangedEventArgs e) { switch (e.ListChangedType) { case ListChangedType.ItemChanged: //要素のプロパティが変化したことを通知しないことでDataGridViewの行単位での表示更新を抑制 break; default: Disconnect(); Connect(); base.OnListChanged(e); break; } } /// <summary>要素のNotifyPropertyChangedイベントを捕捉させる</summary> private void Connect() { items = base.List.OfType<object>().ToArray(); if (this.properties == null || this.properties.Count == 0) { this.properties = GetItemProperties(null); } if (this.properties != null && items != null) { foreach (PropertyDescriptor pd in this.properties) { foreach (object o in items) { pd.AddValueChanged(o, OnItemPropertyChanged); } } } } /// <summary>要素のNotifyPropertyChangedイベントを捕捉解除</summary> private void Disconnect() { if (this.properties != null && items != null) { foreach (PropertyDescriptor pd in this.properties) { foreach (object o in items) { pd.RemoveValueChanged(o, OnItemPropertyChanged); } } items = null; } } /// <summary>要素のプロパティが変化した</summary> private void OnItemPropertyChanged(object sender, EventArgs e) { var lipc = ListItemPropertyChanged; if (lipc != null) { System.ComponentModel.PropertyChangedEventArgs pce = e as System.ComponentModel.PropertyChangedEventArgs; if (pce != null) { //要素のプロパティ変化のイベントを発生させる var lipce = new ListItemPropertyChangedEventArgs(pce.PropertyName); lipce.Item = sender; lipc(this, lipce); } } } public event EventHandler<ListItemPropertyChangedEventArgs> ListItemPropertyChanged; protected override void Dispose(bool disposing) { Disconnect(); base.Dispose(disposing); } } public interface IListItemPropertyChanged { event EventHandler<ListItemPropertyChangedEventArgs> ListItemPropertyChanged; } public class ListItemPropertyChangedEventArgs : PropertyChangedEventArgs { public ListItemPropertyChangedEventArgs(string propertyName) : base(propertyName) { } public object Item { get; set; } } }
#NotifyPropertyChangedイベントの捕捉は適当に書いたのでたぶんリークしてます
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年9月27日 0:17
- 回答としてマーク Ludnes 2017年9月29日 15:30