トップ回答者
DataGridView.Value='xxx'時の変更前値を得る方法について。

質問
-
質問させて下さい。よろしくお願い致します。m(_ _)m
【開発環境】
Visual Studio Express 2013 for Windows Desktop(2.0.30723.00 Update3)
Framework(4.6.01055)
Visual Basic 2013(06157-004-0441005-02597)
【質問内容】
DataGridViewのItem(x,y).Valueでセル値を変更した時に、変更前の値を取得
する良い方法はあるのでしょうか?
入力エディタが起動していればCellValidating()イベントで拾えるのですが、
DataGridViewにはCellValueChanged()イベントはあっても
CellValueChanging()イベントが無くて困っています。。。
継承コントロール化してItemやValueプロパティをShadowしたりもしたのですが
なかなかうまくいかず、八方塞がり状態です。。。
どなたか分かる方おりましたら教えて頂けないでしょうか?
どうかよろしくお願い致します。m(_ _)m
回答
-
CellValueChangedイベントで値を保持しておけば良いのでは?
Public Class Form1 Private WithEvents DataGridView1 As New DataGridViewEx Private WithEvents button As New Button Private logTextBox As New TextBox Sub New() ' この呼び出しはデザイナーで必要です。 InitializeComponent() ' InitializeComponent() 呼び出しの後で初期化を追加します。 DataGridView1.Columns.Add(New DataGridViewTextBoxColumn()) DataGridView1.Columns.Add(New DataGridViewTextBoxColumn()) DataGridView1.Dock = DockStyle.Fill DataGridView1.Rows.Add() DataGridView1.Rows.Add() Me.Controls.Add(DataGridView1) button.Text = "Test" button.Dock = DockStyle.Bottom Me.Controls.Add(button) logTextBox.Dock = DockStyle.Bottom logTextBox.Multiline = True logTextBox.Height = 100 logTextBox.ScrollBars = ScrollBars.Both Me.Controls.Add(logTextBox) Me.DataGridView1.ReadOnly = True 'Readonlyやコンストラクタ内だとCellValidatingが発生しないことをテスト Me.DataGridView1.Item(1, 0).Value = "てすとかいし" End Sub Private Sub DataGridView1_CellValueChanged2(sender As Object, e As DataGridViewCellEventArgsEx) Handles DataGridView1.CellValueChanged2 logTextBox.AppendText(String.Format("{0},{1}" & vbTab & "{0}" & vbTab & "{1}", e.RowIndex, e.ColumnIndex, e.OldValue, e.NewValue) & vbCrLf) End Sub Private Sub button_Click(sender As Object, e As EventArgs) Handles button.Click Me.DataGridView1.Item(0, 0).Value = DateTime.Now.ToLongTimeString() End Sub End Class Class DataGridViewEx Inherits DataGridView Public ReadOnly Property Buffer As DataGidViewValueBuffer Get Return _Buffer End Get End Property Private _Buffer As DataGidViewValueBuffer Sub New() MyBase.New() _Buffer = New DataGidViewValueBuffer(Me) End Sub Public Custom Event CellValueChanged2 As EventHandler(Of DataGridViewCellEventArgsEx) AddHandler(ByVal value As EventHandler(Of DataGridViewCellEventArgsEx)) AddHandler _Buffer.CellValueChanged, value End AddHandler RemoveHandler(ByVal value As EventHandler(Of DataGridViewCellEventArgsEx)) RemoveHandler _Buffer.CellValueChanged, value End RemoveHandler RaiseEvent(ByVal sender As Object, ByVal e As DataGridViewCellEventArgsEx) End RaiseEvent End Event End Class Class DataGidViewValueBuffer Public Event CellValueChanged As EventHandler(Of DataGridViewCellEventArgsEx) Private _rowValueDicrionary As New Dictionary(Of DataGridViewRow, Dictionary(Of DataGridViewColumn, Object)) Private WithEvents _DataGridView As DataGridView Public Sub New(ByVal dgv As DataGridView) Me._DataGridView = dgv End Sub Public Property Item(ByVal clm As DataGridViewColumn, ByVal row As DataGridViewRow) Get Dim old As Object = Nothing Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing If (_rowValueDicrionary.TryGetValue(row, rd)) Then rd.TryGetValue(clm, old) End If Return old End Get Set(value) Dim old As Object = Nothing Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing If (Not _rowValueDicrionary.TryGetValue(row, rd)) Then rd = New Dictionary(Of DataGridViewColumn, Object)() _rowValueDicrionary.Add(row, rd) End If rd.Item(clm) = value End Set End Property Private Sub _DataGridView_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles _DataGridView.CellValueChanged Dim row As DataGridViewRow = Me._DataGridView.Rows.Item(e.RowIndex) Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing Dim clm As DataGridViewColumn = _DataGridView.Columns.Item(e.ColumnIndex) Dim old As Object = Nothing Dim [new] As Object = _DataGridView.Item(e.ColumnIndex, e.RowIndex).Value If (_rowValueDicrionary.TryGetValue(row, rd)) Then rd.TryGetValue(clm, old) Else rd = New Dictionary(Of DataGridViewColumn, Object)() _rowValueDicrionary.Add(row, rd) End If Dim e2 As New DataGridViewCellEventArgsEx(e, _DataGridView, old, [new]) RaiseEvent CellValueChanged(_DataGridView, e2) rd.Item(clm) = [new] End Sub Private Sub _DataGridView_RowsAdded(sender As Object, e As DataGridViewRowsAddedEventArgs) Handles _DataGridView.RowsAdded Dim rd As New Dictionary(Of DataGridViewColumn, Object) For i As Integer = e.RowIndex To e.RowIndex + e.RowCount - 1 Dim row As DataGridViewRow = _DataGridView.Rows.Item(i) For Each clm As DataGridViewColumn In _DataGridView.Columns Dim value As Object value = row.Cells.Item(clm.Name).Value If (value IsNot Nothing) Then rd.Add(clm, value) Dim e2 As New DataGridViewCellEventArgsEx(clm.Index, i, _DataGridView, Nothing, value) RaiseEvent CellValueChanged(_DataGridView, e2) End If Next _rowValueDicrionary.Add(row, rd) Next End Sub Private Sub _DataGridView_RowsRemoved(sender As Object, e As DataGridViewRowsRemovedEventArgs) Handles _DataGridView.RowsRemoved For Each row As DataGridViewRow In _rowValueDicrionary.Keys.ToArray().Where(Function(r) r.Index < 0) _rowValueDicrionary.Remove(row) Next End Sub Private Sub _DataGridView_ColumnRemoved(sender As Object, e As DataGridViewColumnEventArgs) Handles _DataGridView.ColumnRemoved For Each rd As Dictionary(Of DataGridViewColumn, Object) In _rowValueDicrionary.Values rd.Remove(e.Column) Next End Sub End Class Class DataGridViewCellEventArgsEx Inherits DataGridViewCellEventArgs Sub New(ByVal columnIndex As Integer, ByVal rowIndex As Integer, ByVal dataGridView As DataGridView, ByVal old As Object, ByVal newValue As Object) MyBase.New(columnIndex, rowIndex) Me.DataGridView = dataGridView Me.OldValue = old Me.NewValue = newValue End Sub Sub New(ByVal e As DataGridViewCellEventArgs, ByVal dataGridView As DataGridView, ByVal old As Object, ByVal newValue As Object) MyClass.New(e.ColumnIndex, e.RowIndex, dataGridView, old, newValue) End Sub Public ReadOnly DataGridView As DataGridView Public ReadOnly OldValue As Object Public ReadOnly NewValue As Object End Class
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答としてマーク 東京都_会社員 2017年10月31日 4:23
すべての返信
-
CellのTagプロパティに、各Cellの初期状態の値をセットしておくのが一番単純な方法な気がします。
※イメージとしては、値をセットした後に以下の処理をしておけば、
grid.Rows.Cast<DataGridViewRow>().ToList().ForEach(row => { row.Cells.Cast<DataGridViewCell>().ToList().ForEach(cell => { cell.Tag = cell.Value; }); });
Cellの値変更を判定したい時にValueとTagを比較すればOK
※既にTagプロパティを何かに使っていたり、Tagプロパティを安易に使うのを良しとしない場合はダメですが。。。
-
セル値を変更するきっかけが次のコードですがから、
DataGridViewのItem(x,y).Value
これからセル値が変わるというタイミングはわかります。
よって、イベントで通知してもらう必要はなく、自分で制御すれば良いだけのような気がします。
例えば、
DataGridViewのItem(x,y).Value = "aaa";
と、直接Valueプロパティに代入するのではなく、適当なメソッドを作成し、そのメソッドの中で変更前のセル値を取り出しつつ、
DataGridViewのItem(x,y).Valueに値を入力して、新しいセル値を設定するということが考えられます。十分な仕様がわかっていませんので、上記ではダメというのであれば、もう少し詳しい事情をお知らせ下さい。
少し話が変わりますが、DataGridViewのセルに値を直接代入するのではなく、データソースを使うようにし、データソース側で値を変更するようにすると、より柔軟に設計できます。この場合、データはデータソースにあり、DataGridViewは単なるデータ操作のUIになります。データソースは、とりあえずDataTable、Listなどでも良いですが、DTOを使うのがお勧めです。ただ、その分、面倒にはなりますが。
(参考)
Part 2. スマートクライアントにおける単体入力データ検証
[DTO と UI バインドオブジェクトの違い]
https://blogs.msdn.microsoft.com/nakama/2009/02/26/part-2-2/★良い回答には回答済みマークを付けよう! MVP - .NET http://d.hatena.ne.jp/trapemiya/
- 回答の候補に設定 立花楓Microsoft employee, Moderator 2017年10月31日 7:16
-
モデレータ様、返信ありがとうございます。
また、私の説明が足らず申し訳ありません。
詳しく説明致しますと、DataGridViewに対してCtl+ZによるUndo機能を実装したいと思い、
DataGridViewを継承したコントロールCstmGridViewを作成しました。
そして出来るだけプロパティやメソッドなどは標準のまま使いたいと考えています。
そしてUndo機能を実現する為、CstmGridViewの以下のイベントの中で変更前と変更後の
値を変数に退避しています。
・CellValidating
・RowAdded
・RowRemoved
・ColAdded
・ColRemoved
そして、画面側からItem(x,y).Valueに対して値を設定した場合にはCellValidatingが
発生しない為、Valueプロパティを捕捉する必要があると思い、Valueプロパティへの呼び出し
を拾う為に、DataGridViewTextBoxCellを継承したCstmTextBoxCellを作成してこれを、
Column(n).TemplateCellに設定しました。
Public Class CstmTextBoxCell
Inherits DataGridViewTextBoxCell
Public Overridable Shadows Property Value() As Object
Get
Return MyBase.Value
End Get
Set(value As Object)
----アンドゥ用コード----
MyBase.Value = value
End Set
End Property
End Class
そして画面側のコードで、
Ctype(CstmGridView.Item(x,y),CstmTextBoxCell).Valueと書けば上記のValueプロパティを
通るので-アンドゥ用コードも実行出来るのですが、
CstmGridView.Item(x,y).Valueと書かれてしまうと、Item(x,y)プロパティは標準のままなので
返す型がDataGridViewCellになってしまい上記のValueプロパティを通ってくれない事に気づきました。
じゃあ仕方ないのでItemもShadowsしてDataGridViewCellを継承したCstmCell型を返すようにして
CstmCell.Valueの中で捕捉しようと思ったのですが、
Default Public Overridable Shadows ReadOnly Property Item(x As Integer, y As Integer) As CstmCell
Get
Return CType(MyBase.Item(x, y), CstmCell) ★
End Get
End Property
★の部分で「'CstmCell'に変換出来ません。」とエラーになってしまいました。
MyBase.Item(x, y).GetTypeはCstmTextBoxCellであり
MyBase.Item(x, y).GetType.BaseTypeはDataGridViewTextBoxCellであり
MyBase.Item(x, y).GetType.BaseType.BaseTypeはDataGridViewCellなので、
このDataGridViewCellを継承したCstmCellに変換出来るものと思っていたのですが...
そもそもこんな面倒なやり方はどうなのだろうかと悩んでいる状態です。。
- 編集済み 東京都_会社員 2017年10月31日 2:37
-
CellValueChangedイベントで値を保持しておけば良いのでは?
Public Class Form1 Private WithEvents DataGridView1 As New DataGridViewEx Private WithEvents button As New Button Private logTextBox As New TextBox Sub New() ' この呼び出しはデザイナーで必要です。 InitializeComponent() ' InitializeComponent() 呼び出しの後で初期化を追加します。 DataGridView1.Columns.Add(New DataGridViewTextBoxColumn()) DataGridView1.Columns.Add(New DataGridViewTextBoxColumn()) DataGridView1.Dock = DockStyle.Fill DataGridView1.Rows.Add() DataGridView1.Rows.Add() Me.Controls.Add(DataGridView1) button.Text = "Test" button.Dock = DockStyle.Bottom Me.Controls.Add(button) logTextBox.Dock = DockStyle.Bottom logTextBox.Multiline = True logTextBox.Height = 100 logTextBox.ScrollBars = ScrollBars.Both Me.Controls.Add(logTextBox) Me.DataGridView1.ReadOnly = True 'Readonlyやコンストラクタ内だとCellValidatingが発生しないことをテスト Me.DataGridView1.Item(1, 0).Value = "てすとかいし" End Sub Private Sub DataGridView1_CellValueChanged2(sender As Object, e As DataGridViewCellEventArgsEx) Handles DataGridView1.CellValueChanged2 logTextBox.AppendText(String.Format("{0},{1}" & vbTab & "{0}" & vbTab & "{1}", e.RowIndex, e.ColumnIndex, e.OldValue, e.NewValue) & vbCrLf) End Sub Private Sub button_Click(sender As Object, e As EventArgs) Handles button.Click Me.DataGridView1.Item(0, 0).Value = DateTime.Now.ToLongTimeString() End Sub End Class Class DataGridViewEx Inherits DataGridView Public ReadOnly Property Buffer As DataGidViewValueBuffer Get Return _Buffer End Get End Property Private _Buffer As DataGidViewValueBuffer Sub New() MyBase.New() _Buffer = New DataGidViewValueBuffer(Me) End Sub Public Custom Event CellValueChanged2 As EventHandler(Of DataGridViewCellEventArgsEx) AddHandler(ByVal value As EventHandler(Of DataGridViewCellEventArgsEx)) AddHandler _Buffer.CellValueChanged, value End AddHandler RemoveHandler(ByVal value As EventHandler(Of DataGridViewCellEventArgsEx)) RemoveHandler _Buffer.CellValueChanged, value End RemoveHandler RaiseEvent(ByVal sender As Object, ByVal e As DataGridViewCellEventArgsEx) End RaiseEvent End Event End Class Class DataGidViewValueBuffer Public Event CellValueChanged As EventHandler(Of DataGridViewCellEventArgsEx) Private _rowValueDicrionary As New Dictionary(Of DataGridViewRow, Dictionary(Of DataGridViewColumn, Object)) Private WithEvents _DataGridView As DataGridView Public Sub New(ByVal dgv As DataGridView) Me._DataGridView = dgv End Sub Public Property Item(ByVal clm As DataGridViewColumn, ByVal row As DataGridViewRow) Get Dim old As Object = Nothing Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing If (_rowValueDicrionary.TryGetValue(row, rd)) Then rd.TryGetValue(clm, old) End If Return old End Get Set(value) Dim old As Object = Nothing Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing If (Not _rowValueDicrionary.TryGetValue(row, rd)) Then rd = New Dictionary(Of DataGridViewColumn, Object)() _rowValueDicrionary.Add(row, rd) End If rd.Item(clm) = value End Set End Property Private Sub _DataGridView_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles _DataGridView.CellValueChanged Dim row As DataGridViewRow = Me._DataGridView.Rows.Item(e.RowIndex) Dim rd As Dictionary(Of DataGridViewColumn, Object) = Nothing Dim clm As DataGridViewColumn = _DataGridView.Columns.Item(e.ColumnIndex) Dim old As Object = Nothing Dim [new] As Object = _DataGridView.Item(e.ColumnIndex, e.RowIndex).Value If (_rowValueDicrionary.TryGetValue(row, rd)) Then rd.TryGetValue(clm, old) Else rd = New Dictionary(Of DataGridViewColumn, Object)() _rowValueDicrionary.Add(row, rd) End If Dim e2 As New DataGridViewCellEventArgsEx(e, _DataGridView, old, [new]) RaiseEvent CellValueChanged(_DataGridView, e2) rd.Item(clm) = [new] End Sub Private Sub _DataGridView_RowsAdded(sender As Object, e As DataGridViewRowsAddedEventArgs) Handles _DataGridView.RowsAdded Dim rd As New Dictionary(Of DataGridViewColumn, Object) For i As Integer = e.RowIndex To e.RowIndex + e.RowCount - 1 Dim row As DataGridViewRow = _DataGridView.Rows.Item(i) For Each clm As DataGridViewColumn In _DataGridView.Columns Dim value As Object value = row.Cells.Item(clm.Name).Value If (value IsNot Nothing) Then rd.Add(clm, value) Dim e2 As New DataGridViewCellEventArgsEx(clm.Index, i, _DataGridView, Nothing, value) RaiseEvent CellValueChanged(_DataGridView, e2) End If Next _rowValueDicrionary.Add(row, rd) Next End Sub Private Sub _DataGridView_RowsRemoved(sender As Object, e As DataGridViewRowsRemovedEventArgs) Handles _DataGridView.RowsRemoved For Each row As DataGridViewRow In _rowValueDicrionary.Keys.ToArray().Where(Function(r) r.Index < 0) _rowValueDicrionary.Remove(row) Next End Sub Private Sub _DataGridView_ColumnRemoved(sender As Object, e As DataGridViewColumnEventArgs) Handles _DataGridView.ColumnRemoved For Each rd As Dictionary(Of DataGridViewColumn, Object) In _rowValueDicrionary.Values rd.Remove(e.Column) Next End Sub End Class Class DataGridViewCellEventArgsEx Inherits DataGridViewCellEventArgs Sub New(ByVal columnIndex As Integer, ByVal rowIndex As Integer, ByVal dataGridView As DataGridView, ByVal old As Object, ByVal newValue As Object) MyBase.New(columnIndex, rowIndex) Me.DataGridView = dataGridView Me.OldValue = old Me.NewValue = newValue End Sub Sub New(ByVal e As DataGridViewCellEventArgs, ByVal dataGridView As DataGridView, ByVal old As Object, ByVal newValue As Object) MyClass.New(e.ColumnIndex, e.RowIndex, dataGridView, old, newValue) End Sub Public ReadOnly DataGridView As DataGridView Public ReadOnly OldValue As Object Public ReadOnly NewValue As Object End Class
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
- 回答としてマーク 東京都_会社員 2017年10月31日 4:23
-
gekkaさん、返信ありがとうこざいます!
>CellValueChangedイベントで値を保持しておけば良いのでは?
当初、画面表示時の初期表示処理完了後、
変更のあったセルの変更前後の値のみ記録する。
という小さな小さな管理で考えでいたのですが、
今となっては画面表示時の初期表示処理も含めて
CellValueChangedで全て記録するほうがいいような気がしています。。
加えて、それによる本格的な行削除追加含めた変更記録のコードも
提示して頂いて、こんな方法があるのかととても勉強になりました。
gekkaさんのこのコード例を自分なりにしっかり理解して
自身の知識と今後のコードに反映させて行きたいと思います。
本当にありがとうございました!m(_ _)m