none
DataGridView.Value='xxx'時の変更前値を得る方法について。 RRS feed

  • 質問

  • 質問させて下さい。よろしくお願い致します。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
    2017年10月30日 9:26

回答

  • 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日 3:39

すべての返信

  • 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プロパティを安易に使うのを良しとしない場合はダメですが。。。

    2017年10月31日 0:46
  • 返信ありがとうございます。
    最初、そのような方法を考えておりました。
    しかしおっしゃる通り、Tagが使えなくなるという問題もあり、他の方法を模索した経緯があります。

    ご提示頂いき感謝致します。m(_ _)m
    やはり良い方法はないものですかね。。。。

    2017年10月31日 1:10
  • セル値を変更するきっかけが次のコードですがから、
    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/

    2017年10月31日 1:19
    モデレータ
  • モデレータ様、返信ありがとうございます。
    また、私の説明が足らず申し訳ありません。

    詳しく説明致しますと、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:34
  • 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日 3:39
  • gekkaさん、返信ありがとうこざいます!

    >CellValueChangedイベントで値を保持しておけば良いのでは?

    当初、画面表示時の初期表示処理完了後、
    変更のあったセルの変更前後の値のみ記録する。
    という小さな小さな管理で考えでいたのですが、
    今となっては画面表示時の初期表示処理も含めて
    CellValueChangedで全て記録するほうがいいような気がしています。。

    加えて、それによる本格的な行削除追加含めた変更記録のコードも
    提示して頂いて、こんな方法があるのかととても勉強になりました。
    gekkaさんのこのコード例を自分なりにしっかり理解して
    自身の知識と今後のコードに反映させて行きたいと思います。

    本当にありがとうございました!m(_ _)m
    2017年10月31日 4:23