Lesson 31 · Editing Data

Editing Data

Master the complete data-editing lifecycle — validated INSERT/UPDATE/DELETE, the CommandBuilder shortcut, SQL transactions for atomic multi-row operations, inline DataGridView editing, and a simple undo stack.

Key Takeaway: Good data-editing code has three layers: validation (check input before writing), persistence (parameterised INSERT/UPDATE/DELETE or CommandBuilder), and safety (transactions for multi-step operations, Try/Catch for failures). Validate in the UI so the database never receives bad data. Use a transaction whenever you need two or more SQL statements to succeed or fail together — conn.BeginTransaction() → execute commands → tx.Commit(), or tx.Rollback() in the Catch block. The CommandBuilder auto-generates INSERT/UPDATE/DELETE commands from a SELECT DataAdapter in one line — ideal for simple tables without business rules.
Validate()
Pattern
Check every input before writing. Return a list of error strings; block save if non-empty.
CommandBuilder
ADO.NET
SqliteCommandBuilder(da) — auto-generates INSERT/UPDATE/DELETE from the SELECT. Call da.Update(dt) to push changes.
da.Update(dt)
DataAdapter
Pushes all new/modified/deleted DataRows back to the database in a single call.
BeginTransaction
Connection method
conn.BeginTransaction() — starts a transaction. Assign to every command's Transaction property.
tx.Commit()
Transaction method
Writes all changes permanently. Call only after all statements succeed.
tx.Rollback()
Transaction method
Undoes everything since BeginTransaction. Call in the Catch block on failure.
RowState
DataRow property
Added / Modified / Deleted / Unchanged. CommandBuilder uses this to decide which SQL to run.
Undo stack
Pattern
Stack(Of UndoAction). Push before every change. Pop and reverse on Ctrl+Z.

31.1 Input Validation Before Saving

Never write unchecked user input to the database. Validate in the UI first — collect all errors into a list and show them together so the user can fix everything at once, rather than one error at a time.

Validation.vb — Visual Basic 2026
' --- Validate inputs, return list of error messages ---
Private Function ValidateStudent() As List(Of String)
    Dim errors As New List(Of String)

    ' Name: required, 2–80 chars, letters/spaces only
    If txtName.Text.Trim().Length < 2 Then
        errors.Add("Name must be at least 2 characters.")
    ElseIf txtName.Text.Trim().Length > 80 Then
        errors.Add("Name must be 80 characters or fewer.")
    End If

    ' Grade: numeric, 0–100
    Dim grade As Double
    If Not Double.TryParse(txtGrade.Text, grade) Then
        errors.Add("Grade must be a number.")
    ElseIf grade < 0 Or grade > 100 Then
        errors.Add("Grade must be between 0 and 100.")
    End If

    ' Class: required selection
    If cboClass.SelectedIndex < 0 Then
        errors.Add("Please select a class.")
    End If

    Return errors
End Function

' --- Use in Save button ---
Private Sub btnSave_Click(...) Handles btnSave.Click
    Dim errors = ValidateStudent()
    If errors.Count > 0 Then
        MessageBox.Show(String.Join(Environment.NewLine, errors),
                        "Validation Errors", MessageBoxButtons.OK, MessageBoxIcon.Warning)
        Return   ' abort save
    End If
    SaveStudent()   ' validation passed — write to database
End Sub

' --- Live validation feedback (TextChanged) ---
Private Sub txtGrade_TextChanged(...) Handles txtGrade.TextChanged
    Dim g As Double
    If Double.TryParse(txtGrade.Text, g) And g >= 0 And g <= 100 Then
        txtGrade.BackColor = Color.Honeydew    ' green tint = valid
        lblGradeError.Text = ""
    Else
        txtGrade.BackColor = Color.MistyRose   ' red tint = invalid
        lblGradeError.Text = "⚠ Must be 0–100"
    End If
End Sub

' --- Duplicate name check (before INSERT) ---
Function NameExists(name As String) As Boolean
    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Using cmd As New SqliteCommand(
            "SELECT COUNT(*) FROM Students WHERE Name = @Name", conn)
            cmd.Parameters.AddWithValue("@Name", name.Trim())
            Return CInt(cmd.ExecuteScalar()) > 0
        End Using
    End Using
End Function
Try It — Simulation 31.1: Input Validation

Try submitting with invalid data — empty name, out-of-range grade, missing class. Each field shows live feedback as you type. Clicking Save shows all collected errors at once.

ValidateStudent() — Live & On-Save Validation
Name: (2–80 chars)
Grade: (0–100)
Class:
Email: (optional, valid format)

31.2 The CommandBuilder — Auto-Generated SQL

The SqliteCommandBuilder (or OleDbCommandBuilder for Access) inspects your SELECT DataAdapter and automatically writes the matching INSERT, UPDATE, and DELETE commands. You then edit the in-memory DataTable and call da.Update(dt) — the adapter executes the right command for each changed row based on its RowState.

CommandBuilder.vb — Visual Basic 2026
' --- CommandBuilder setup (in LoadData) ---
Private _da As SqliteDataAdapter
Private _dt As New DataTable()
Private _cb As SqliteCommandBuilder

Private Sub LoadData()
    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        _da = New SqliteDataAdapter("SELECT * FROM Students ORDER BY Name", conn)
        _cb = New SqliteCommandBuilder(_da)   ' auto-generates INSERT/UPDATE/DELETE
        _da.Fill(_dt)
    End Using
    dgv.DataSource = _dt
End Sub

' --- Add a new row to the DataTable ---
Private Sub AddRow()
    Dim row = _dt.NewRow()
    row("Name")  = txtName.Text
    row("Grade") = CDbl(txtGrade.Text)
    row("Class") = cboClass.Text
    _dt.Rows.Add(row)   ' RowState = DataRowState.Added
End Sub

' --- Modify an existing row ---
Private Sub EditRow(rowIndex As Integer)
    _dt.Rows(rowIndex)("Grade") = CDbl(txtGrade.Text)
    ' RowState changes to DataRowState.Modified automatically
End Sub

' --- Mark a row for deletion ---
Private Sub DeleteRow(rowIndex As Integer)
    _dt.Rows(rowIndex).Delete()   ' RowState = DataRowState.Deleted
    ' Row not physically removed until da.Update() is called
End Sub

' --- Commit ALL pending changes to the database ---
Private Sub SaveAllChanges()
    Try
        Using conn As New SqliteConnection(CONN_STR)
            conn.Open()
            _da.SelectCommand.Connection = conn
            _da.Update(_dt)     ' pushes Added/Modified/Deleted rows
        End Using
        _dt.AcceptChanges()     ' reset all RowStates to Unchanged
        MessageBox.Show("All changes saved.")
    Catch ex As Exception
        MessageBox.Show(ex.Message, "Save Error")
    End Try
End Sub

' --- Discard all pending changes ---
Private Sub DiscardChanges()
    _dt.RejectChanges()   ' rolls back all Added/Modified/Deleted rows
End Sub

' --- Check if there are unsaved changes ---
Dim hasChanges = _dt.GetChanges() IsNot Nothing
CommandBuilder Limitations

The CommandBuilder only works for single-table SELECT statements with a primary key. It cannot handle JOINs, computed columns, or stored procedures. For complex business logic (cascading deletes, audit trails, triggers), write your own parameterised INSERT/UPDATE/DELETE commands.

Try It — Simulation 31.2: CommandBuilder — RowState Tracking

Add, edit, and mark rows for deletion — each shows a colour-coded RowState (green = Added, yellow = Modified, red = Deleted). Click Save All to simulate da.Update(dt) generating the right SQL. Discard calls RejectChanges().

SqliteCommandBuilder — da.Update(dt)
Name:
Grade:
Class:
■ Added ■ Modified ■ Deleted ■ Unchanged

31.3 SQL Transactions

A transaction ensures that a group of SQL statements all succeed or all fail together — the database never ends up in a half-changed state. Use conn.BeginTransaction(), assign the transaction to every command, call Commit() on success, and Rollback() in the Catch block.

Transactions.vb — Visual Basic 2026
' --- Transfer 10 marks from one student to another (atomic) ---
Private Sub TransferMarks(fromId As Integer, toId As Integer, amount As Integer)
    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Dim tx = conn.BeginTransaction()   ' START TRANSACTION
        Try
            ' Step 1 — deduct from source student
            Using cmd1 As New SqliteCommand(
                "UPDATE Students SET Grade = Grade - @Amt WHERE StudentID = @ID", conn, tx)
                cmd1.Parameters.AddWithValue("@Amt", amount)
                cmd1.Parameters.AddWithValue("@ID",  fromId)
                cmd1.ExecuteNonQuery()
            End Using

            ' Step 2 — add to destination student
            Using cmd2 As New SqliteCommand(
                "UPDATE Students SET Grade = Grade + @Amt WHERE StudentID = @ID", conn, tx)
                cmd2.Parameters.AddWithValue("@Amt", amount)
                cmd2.Parameters.AddWithValue("@ID",  toId)
                cmd2.ExecuteNonQuery()
            End Using

            ' Both steps succeeded — commit
            tx.Commit()
            MessageBox.Show("Transfer complete.")
        Catch ex As Exception
            tx.Rollback()   ' undo BOTH steps if either failed
            MessageBox.Show($"Transfer failed — rolled back: {ex.Message}")
        End Try
    End Using
End Sub

' --- Batch INSERT with transaction (much faster than individual inserts) ---
Private Sub BulkInsert(students As List(Of Student))
    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Dim tx = conn.BeginTransaction()
        Try
            Using cmd As New SqliteCommand(
                "INSERT INTO Students (Name, Grade, Class) VALUES (@N, @G, @C)", conn, tx)
                For Each s In students
                    cmd.Parameters.Clear()
                    cmd.Parameters.AddWithValue("@N", s.Name)
                    cmd.Parameters.AddWithValue("@G", s.Grade)
                    cmd.Parameters.AddWithValue("@C", s.Class)
                    cmd.ExecuteNonQuery()   ' reuse same command in loop
                Next
            End Using
            tx.Commit()
            MessageBox.Show($"Inserted {students.Count} records.")
        Catch ex As Exception
            tx.Rollback()
        End Try
    End Using
End Sub
Performance: Transactions Speed Up Bulk Inserts

SQLite wraps every ExecuteNonQuery() without an explicit transaction in its own auto-transaction, which flushes to disk. Inserting 1 000 rows individually can take 10+ seconds. Wrapping the loop in a single BeginTransaction()/Commit() reduces that to under 100 ms — a 100× speedup.

Try It — Simulation 31.3: SQL Transaction Simulator

Step through a multi-statement transaction manually, or trigger a deliberate failure to see the rollback. The transaction log shows each SQL statement and the commit/rollback outcome.

Transaction Demo — BEGIN / COMMIT / ROLLBACK
Scenario:

31.4 Inline DataGridView Editing

When dgv.ReadOnly = False, the user can double-click any cell to edit it directly. Handle the CellEndEdit event to validate the new value immediately and save (or reject) the change.

InlineEdit.vb — Visual Basic 2026
' --- Allow inline editing ---
dgv.ReadOnly  = False
dgv.EditMode  = DataGridViewEditMode.EditOnDoubleClick  ' or EditOnKeystroke

' --- Validate immediately after cell edit ---
Private Sub dgv_CellValidating(...) Handles dgv.CellValidating
    If dgv.Columns(e.ColumnIndex).Name = "Grade" Then
        Dim g As Double
        If Not Double.TryParse(e.FormattedValue.ToString(), g) _
            Or g < 0 Or g > 100 Then
            e.Cancel = True   ' keep cell in edit mode
            MessageBox.Show("Grade must be 0–100.")
        End If
    End If
End Sub

' --- Save the cell edit to the database ---
Private Sub dgv_CellEndEdit(...) Handles dgv.CellEndEdit
    Dim col = dgv.Columns(e.ColumnIndex).Name
    Dim val = dgv.Rows(e.RowIndex).Cells(e.ColumnIndex).Value
    Dim id  = CInt(dgv.Rows(e.RowIndex).Cells("StudentID").Value)

    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Dim sql = $"UPDATE Students SET {col} = @Val WHERE StudentID = @ID"
        Using cmd As New SqliteCommand(sql, conn)
            cmd.Parameters.AddWithValue("@Val", val)
            cmd.Parameters.AddWithValue("@ID",  id)
            cmd.ExecuteNonQuery()
        End Using
    End Using
End Sub

' --- Prevent editing of primary-key column ---
Private Sub dgv_CellBeginEdit(...) Handles dgv.CellBeginEdit
    If dgv.Columns(e.ColumnIndex).Name = "StudentID" Then
        e.Cancel = True   ' block edit of primary key
    End If
End Sub

' --- Undo last cell edit (Ctrl+Z) ---
Private Sub Form_KeyDown(...) Handles MyBase.KeyDown
    If e.Control And e.KeyCode = Keys.Z Then UndoLastEdit()
End Sub
Try It — Simulation 31.4: Inline DataGridView Editing

Double-click any cell (except ID) to edit it in place. Grade cells validate immediately — out-of-range values are rejected and the cell stays in edit mode. Each accepted change shows the equivalent UPDATE SQL.

Inline Edit — dgv.ReadOnly = False, CellEndEdit
Double-click a cell to edit it. Grade must be 0–100. ID column is protected. Press Enter to confirm, Escape to cancel.

31.5 Simple Undo Stack

A lightweight undo system stores a snapshot of each record before it is changed. Push a copy onto a Stack before every INSERT/UPDATE/DELETE. On Ctrl+Z, pop the top action and reverse it.

UndoStack.vb — Visual Basic 2026
' --- Undo action record ---
Enum UndoType : Added : Edited : Deleted : End Enum

Structure UndoAction
    Dim Kind        As UndoType
    Dim StudentID   As Integer
    Dim OldName     As String
    Dim OldGrade    As Double
    Dim OldClass    As String
End Structure

Private _undo As New Stack(Of UndoAction)()

' --- Push BEFORE making any change ---
Private Sub PushUndo(kind As UndoType, row As DataRow)
    _undo.Push(New UndoAction With {
        .Kind      = kind,
        .StudentID = CInt(row("StudentID")),
        .OldName   = row("Name").ToString(),
        .OldGrade  = CDbl(row("Grade")),
        .OldClass  = row("Class").ToString()
    })
    btnUndo.Enabled = True
End Sub

' --- Undo last action ---
Private Sub UndoLastEdit()
    If _undo.Count = 0 Then Return
    Dim action = _undo.Pop()

    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Select Case action.Kind
            Case UndoType.Added
                ' Undo add = delete the inserted row
                Using cmd As New SqliteCommand(
                    "DELETE FROM Students WHERE StudentID = @ID", conn)
                    cmd.Parameters.AddWithValue("@ID", action.StudentID)
                    cmd.ExecuteNonQuery()
                End Using
            Case UndoType.Edited
                ' Undo edit = restore old values
                Using cmd As New SqliteCommand(
                    "UPDATE Students SET Name=@N, Grade=@G, Class=@C WHERE StudentID=@ID", conn)
                    cmd.Parameters.AddWithValue("@N",  action.OldName)
                    cmd.Parameters.AddWithValue("@G",  action.OldGrade)
                    cmd.Parameters.AddWithValue("@C",  action.OldClass)
                    cmd.Parameters.AddWithValue("@ID", action.StudentID)
                    cmd.ExecuteNonQuery()
                End Using
            Case UndoType.Deleted
                ' Undo delete = re-insert the row
                Using cmd As New SqliteCommand(
                    "INSERT INTO Students (StudentID, Name, Grade, Class) VALUES (@ID, @N, @G, @C)", conn)
                    cmd.Parameters.AddWithValue("@ID", action.StudentID)
                    cmd.Parameters.AddWithValue("@N",  action.OldName)
                    cmd.Parameters.AddWithValue("@G",  action.OldGrade)
                    cmd.Parameters.AddWithValue("@C",  action.OldClass)
                    cmd.ExecuteNonQuery()
                End Using
        End Select
    End Using

    If _undo.Count = 0 Then btnUndo.Enabled = False
    LoadData()   ' refresh DataGridView
End Sub
Try It — Simulation 31.5: Undo Stack (Ctrl+Z)

Every Add, Edit, and Delete pushes a snapshot to the undo stack. Click Undo (or press Ctrl+Z) to reverse the last action. The stack display shows all pending undos — LIFO order.

Undo Stack — Stack(Of UndoAction)
Name:
Grade:
Class:
Stack: 0 items
Undo stack (top = most recent):
— empty —

31.6 GitHub Copilot — Validated CRUD with Undo

GitHub Copilot — Full CRUD with Validation + Undo
You typed: ' Generate a complete validated CRUD form for an Inventory table (ProductID, Name, Category, Price, StockQty). Validate: Name required, Price > 0, StockQty >= 0, Category not empty. Use transactions for all writes. Support undo for the last 10 operations. Show a status bar with record count and "Unsaved changes" indicator.'
' Validated inventory CRUD with undo — generated by Copilot.
Imports Microsoft.Data.Sqlite

Public Class frmInventory
    Private Const DB   = "Data Source=inventory.db"
    Private _bs         As New BindingSource()
    Private _dt         As New DataTable()
    Private _undo       As New Stack(Of UndoAction)()
    Private Const MAX_UNDO = 10

    ' --- Validation ---
    Private Function Validate() As List(Of String)
        Dim e As New List(Of String)
        If txtName.Text.Trim() = ""                  Then e.Add("Name is required.")
        If cboCategory.SelectedIndex < 0            Then e.Add("Select a category.")
        Dim price As Decimal
        If Not Decimal.TryParse(txtPrice.Text, price) _
            Or price <= 0                            Then e.Add("Price must be > 0.")
        If nudStock.Value < 0                        Then e.Add("StockQty cannot be negative.")
        Return e
    End Function

    ' --- Save with transaction ---
    Private Sub btnSave_Click(...) Handles btnSave.Click
        Dim errs = Validate()
        If errs.Count > 0 Then
            MessageBox.Show(String.Join(vbNewLine, errs), "Validation")
            Return
        End If
        Dim isNew = (_selectedId = -1)
        PushUndo(If(isNew, UndoType.Added, UndoType.Edited))
        Using conn As New SqliteConnection(DB)
            conn.Open()
            Dim tx = conn.BeginTransaction()
            Try
                If isNew Then
                    Using cmd As New SqliteCommand(
                        "INSERT INTO Products(Name,Category,Price,StockQty) VALUES(@N,@C,@P,@S)", conn, tx)
                        cmd.Parameters.AddWithValue("@N", txtName.Text.Trim())
                        cmd.Parameters.AddWithValue("@C", cboCategory.Text)
                        cmd.Parameters.AddWithValue("@P", CDec(txtPrice.Text))
                        cmd.Parameters.AddWithValue("@S", CInt(nudStock.Value))
                        cmd.ExecuteNonQuery()
                    End Using
                Else
                    Using cmd As New SqliteCommand(
                        "UPDATE Products SET Name=@N,Category=@C,Price=@P,StockQty=@S WHERE ProductID=@ID", conn, tx)
                        cmd.Parameters.AddWithValue("@N",  txtName.Text.Trim())
                        cmd.Parameters.AddWithValue("@C",  cboCategory.Text)
                        cmd.Parameters.AddWithValue("@P",  CDec(txtPrice.Text))
                        cmd.Parameters.AddWithValue("@S",  CInt(nudStock.Value))
                        cmd.Parameters.AddWithValue("@ID", _selectedId)
                        cmd.ExecuteNonQuery()
                    End Using
                End If
                tx.Commit()
                LoadProducts()
                UpdateStatus()
            Catch ex As Exception
                tx.Rollback() : _undo.TryPop(Nothing)
                MessageBox.Show(ex.Message, "Save Error")
            End Try
        End Using
    End Sub

    Private Sub UpdateStatus()
        lblStatus.Text = $"{_bs.Count} products  |  Undo: {_undo.Count}/{MAX_UNDO}"
        If _undo.Count > MAX_UNDO Then _undo.Clear()
    End Sub
End Class
Copilot Chat Prompts for This Lesson

Try these in the Copilot Chat panel:

  • "Generate an audit log table (AuditLog: LogID, TableName, Action, OldValues, NewValues, ChangedAt, ChangedBy) and a Sub that inserts a JSON-serialised audit record inside every transaction that modifies the Students table"
  • "Write a Generic(Of T) helper function SaveWithUndo(T) that pushes to the undo stack, runs an Action, catches exceptions, and pops from the stack on failure"
  • "Add DataGridView.UserDeletingRow handler: before deleting, push undo, ask for confirmation with MessageBox.YesNo, and e.Cancel = True if the user clicks No"
  • "Implement a bulk-import feature: OpenFileDialog filters to .csv, parse each line into a Student, validate all rows first, then insert them all in a single transaction with a progress bar"

Lesson Summary

  • Validate before writing. Collect all errors into a List(Of String) and return them from a ValidateStudent() function. Show them all at once with String.Join(Environment.NewLine, errors). Provide live feedback (green/red background) on TextChanged.
  • The CommandBuilder (SqliteCommandBuilder(_da)) auto-generates INSERT/UPDATE/DELETE from your SELECT. Edit DataRow objects directly — RowState tracks what changed — then call da.Update(dt) and dt.AcceptChanges(). Call dt.RejectChanges() to discard pending edits.
  • Transactions: Dim tx = conn.BeginTransaction() → pass tx to every command → tx.Commit() on success → tx.Rollback() in Catch. Always in a Using block. Batch inserts inside a single transaction can be 100× faster than auto-commit mode.
  • Inline DataGridView editing: set ReadOnly = False, handle CellValidating (set e.Cancel = True to reject), and save the change in CellEndEdit. Block primary-key editing in CellBeginEdit.
  • An undo stack (Stack(Of UndoAction)) stores a before-snapshot before every change. On Ctrl+Z, pop and reverse: undo an INSERT with DELETE, undo an UPDATE by restoring old values, undo a DELETE with re-INSERT.

Exercises

Exercise 31.1 — Validated Student Entry

  • Build a student entry form with live validation: Name (required, max 60 chars), Grade (0–100, numeric), Class (required ComboBox).
  • Name field turns green/red as the user types. Save button disabled until all fields are valid.
  • Add a duplicate-name check using ExecuteScalar — warn but allow save after confirmation.
  • Wrap the INSERT in a transaction. On failure, show the full exception message in a label.
  • Copilot challenge: "Add a RequiredField validator component that automatically highlights any TextBox bound to it when it loses focus while empty, using the Control.Validating event"

Exercise 31.2 — Batch Grade Update

  • Add a "Apply Grade Bonus" feature: a NumericUpDown for the bonus value (1–10) and a ComboBox for the class to apply it to.
  • Run UPDATE Students SET Grade = MIN(Grade + @Bonus, 100) WHERE Class = @Class in a transaction. Show how many rows were affected.
  • Push an undo action that stores all affected rows before the update — undo restores each row individually in its own transaction.
  • Show before/after statistics: average grade before and after the bonus using ExecuteScalar.
  • Copilot challenge: "Log each batch update to an AuditLog table with columns: Action, AffectedRows, OldAvg, NewAvg, Timestamp"

Exercise 31.3 — Inline Editable Inventory Grid

  • Products DataGridView with ReadOnly = False. Product name and StockQty are editable; Price is read-only (edit via form below).
  • CellValidating: StockQty must be ≥ 0 integer; Name must not be empty. Reject with e.Cancel = True and tooltip.
  • CellEndEdit: save changed cell to database with parameterised UPDATE. Colour-code changed rows yellow until saved.
  • Add Ctrl+Z undo for the last 5 inline edits.
  • Copilot challenge: "Add a DataGridView.UserDeletingRow handler: push to undo stack, confirm with MessageBox.YesNo, call DELETE with transaction, cancel the default deletion, and reload the grid"

Next: Lesson 32 — Console Application Part 1

Leave Windows Forms behind — build command-line programs with Console.ReadLine, Console.Write, loops, and structured output formatting.

Continue »

Related Resources


Featured Books

Visual Basic 2022 Made Easy

Visual Basic 2022 Made Easy

by Dr. Liew Voon Kiong

Data editing chapters: validation patterns, CommandBuilder, and transactions with worked examples.

View on Amazon →
VB Programming With Code Examples

VB Programming With Code Examples

by Dr. Liew Voon Kiong

Full CRUD programs with error handling, validation, and undo functionality.

View on Amazon →