Lesson 30 · Connecting to a Database

Connecting to a Database

Go beyond raw ADO.NET — use BindingSource, BindingNavigator, and DataGridView column customisation to build polished, navigable data applications with minimal code.

Key Takeaway: A BindingSource sits between your DataTable and your UI controls — it manages the current row position, filtering, sorting, and change tracking so you don't have to. Bind your DataGridView, TextBoxes, Labels, and ComboBoxes all to the same BindingSource, and they all stay in sync automatically. The BindingNavigator toolbar wires First/Previous/Next/Last/Add/Delete navigation to the BindingSource with zero code. Customise DataGridView columns at runtime (headers, widths, read-only, formatting) after you set DataSource by accessing dgv.Columns("Name").
BindingSource
Component
Wraps a DataTable. Manages current position, filter, sort. Bind all controls to it.
BindingNavigator
Toolbar control
Auto-wired navigation bar: ◀◀ ◀ pos/total ▶ ▶▶ + ✕. Bind to a BindingSource.
.DataSource
Property
Set on BindingSource (= DataTable) and on DataGridView (= BindingSource).
.Filter
BindingSource
bs.Filter = "Grade >= 80 AND Class = '4A'" — live row filtering, no re-query.
.Sort
BindingSource
bs.Sort = "Grade DESC" — sorts without hitting the database again.
.Current / .Position
BindingSource
Current row as DataRowView. Position = index. MoveNext() / MovePrevious().
dgv.Columns("Name")
DataGridViewColumn
Customise header, width, ReadOnly, DefaultCellStyle.Format, Visible after binding.
RowPrePaint
Event
Fires before each row is drawn — use to colour-code rows based on data values.

30.1 BindingSource — The Data Hub

A BindingSource acts as a central hub between your database data and all the controls on the form. Once your DataTable is loaded, assign it as the BindingSource's DataSource. Then point every control at the BindingSource — they all share the same current row cursor and stay synchronised without any extra code.

BindingSource.vb — Visual Basic 2026
' --- Module-level: shared by the whole form ---
Private _bs As New BindingSource()
Private _dt As New DataTable()

' --- Form_Load: load data, wire everything up ---
Private Sub Form_Load(...) Handles MyBase.Load
    LoadData()
    WireUpControls()
End Sub

Private Sub LoadData()
    _dt.Clear()
    Using conn As New SqliteConnection(CONN_STR)
        conn.Open()
        Using da As New SqliteDataAdapter("SELECT * FROM Students ORDER BY Name", conn)
            da.Fill(_dt)
        End Using
    End Using
    _bs.DataSource = _dt      ' attach DataTable to BindingSource
End Sub

Private Sub WireUpControls()
    ' DataGridView shows the whole table
    dgvStudents.DataSource = _bs

    ' Individual TextBoxes track the current row automatically
    txtName.DataBindings.Add("Text", _bs, "Name",  True)
    txtGrade.DataBindings.Add("Text", _bs, "Grade", True)

    ' A Label can show any field from the current row
    lblClass.DataBindings.Add("Text", _bs, "Class", True)

    ' BindingNavigator: wire to the same BindingSource
    BindingNavigator1.BindingSource = _bs

    ' Record counter label (e.g., "3 / 15")
    _bs.PositionChanged += Sub()
        lblPos.Text = $"{_bs.Position + 1} / {_bs.Count}"
    End Sub
End Sub

' --- Navigate programmatically ---
_bs.MoveFirst()     ' go to row 0
_bs.MovePrevious()  ' row - 1 (clamped at 0)
_bs.MoveNext()      ' row + 1 (clamped at Count-1)
_bs.MoveLast()      ' go to last row
_bs.Position = 4    ' jump to index 4

' --- Read the current row ---
Dim row   = CType(_bs.Current, DataRowView).Row
Dim name  = row("Name").ToString()
Dim grade = CDbl(row("Grade"))
Try It — Simulation 30.1: BindingSource Navigation

A BindingNavigator toolbar controls which row is "current". The detail panel below shows bound TextBoxes updating automatically as you navigate. Clicking a grid row also moves the current position — all controls stay in sync.

BindingSource Demo — All controls share current row
of 15 | BindingNavigator1.BindingSource = _bs
dgvStudents.DataSource = _bs
Bound TextBoxes (current row)
StudentID: Name: Grade: Class:
Position: 0 / 15

30.2 Filtering and Sorting with BindingSource

One of the most powerful BindingSource features is live filtering and sorting — no new database round-trip, no new SQL query. Assign a filter expression to _bs.Filter and the DataGridView instantly shows only matching rows. The underlying DataTable is unchanged; the BindingSource simply restricts its view.

FilterSort.vb — Visual Basic 2026
' --- Live filter: same syntax as DataTable.Select() ---
Private Sub ApplyFilter()
    Dim parts As New List(Of String)

    ' Text search on Name
    If txtSearch.Text.Length > 0 Then
        parts.Add($"Name LIKE '%{txtSearch.Text}%'")
    End If

    ' Minimum grade
    If nudMinGrade.Value > 0 Then
        parts.Add($"Grade >= {nudMinGrade.Value}")
    End If

    ' Class filter
    If cboClass.SelectedIndex > 0 Then
        parts.Add($"Class = '{cboClass.Text}'")
    End If

    _bs.Filter = If(parts.Count > 0, String.Join(" AND ", parts), "")
    lblCount.Text = $"{_bs.Count} of {_dt.Rows.Count} records"
End Sub

' --- Sort by column ---
Private Sub SortByColumn(colName As String)
    ' Toggle ASC / DESC
    If _bs.Sort = colName & " ASC" Then
        _bs.Sort = colName & " DESC"
    Else
        _bs.Sort = colName & " ASC"
    End If
End Sub

' --- Clear all filters ---
Private Sub ClearFilter()
    _bs.Filter = ""
    _bs.Sort   = ""
    txtSearch.Clear()
    nudMinGrade.Value = 0
    cboClass.SelectedIndex = 0
End Sub

' --- Live search as user types ---
Private Sub txtSearch_TextChanged(...) Handles txtSearch.TextChanged
    ApplyFilter()    ' filter updates instantly — no database round-trip
End Sub
Try It — Simulation 30.2: Live Filter & Sort

Type in the search box and the grid updates instantly — no database query. Combine name search, minimum grade, and class filter. Click a column header to sort ASC/DESC. Shows the _bs.Filter expression in the code panel.

BindingSource.Filter — No database round-trip
Search name:
Min grade:
Class:
15 of 15 records

30.3 Customising DataGridView Columns

After binding data, you can customise every column programmatically — rename headers, set widths, hide columns, make specific columns read-only, apply number/date format strings, and change cell colours. Access columns by name or index via dgv.Columns("ColumnName").

DGVCustomise.vb — Visual Basic 2026
' --- After dgv.DataSource is set, customise columns ---
Private Sub CustomiseGrid()
    With dgvStudents
        .AutoGenerateColumns     = True    ' let ADO.NET create columns first
        .AutoSizeColumnsMode     = DataGridViewAutoSizeColumnsMode.Fill
        .SelectionMode           = DataGridViewSelectionMode.FullRowSelect
        .MultiSelect             = False
        .ReadOnly                = True    ' grid itself read-only; edit via form fields
        .RowHeadersVisible       = False   ' hide left row-number header
        .AlternatingRowsDefaultCellStyle.BackColor = Color.AliceBlue

        ' Rename column headers
        .Columns("StudentID").HeaderText = "ID"
        .Columns("Name").HeaderText      = "Full Name"
        .Columns("Grade").HeaderText     = "Grade (%)"

        ' Set column widths
        .Columns("StudentID").Width = 40
        .Columns("Name").Width      = 180
        .Columns("Grade").Width     = 80
        .Columns("Class").Width     = 60

        ' Hide a column (e.g., keep ID for lookups but don't show it)
        .Columns("StudentID").Visible = False

        ' Format a numeric column (2 decimal places)
        .Columns("Grade").DefaultCellStyle.Format      = "N1"
        .Columns("Grade").DefaultCellStyle.Alignment    = DataGridViewContentAlignment.MiddleCenter

        ' Right-align numbers
        .Columns("Grade").DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight

        ' Bold header font
        .ColumnHeadersDefaultCellStyle.Font = New Font("Segoe UI", 9, FontStyle.Bold)
    End With
End Sub

' --- Colour-code rows: red if grade < 60, green if >= 90 ---
Private Sub dgvStudents_RowPrePaint(...) Handles dgvStudents.RowPrePaint
    Dim grade = CDbl(dgvStudents.Rows(e.RowIndex).Cells("Grade").Value)
    If      grade < 60 Then dgvStudents.Rows(e.RowIndex).DefaultCellStyle.BackColor = Color.MistyRose
    ElseIf  grade >= 90 Then dgvStudents.Rows(e.RowIndex).DefaultCellStyle.BackColor = Color.Honeydew
    Else                      dgvStudents.Rows(e.RowIndex).DefaultCellStyle.BackColor = Color.White
    End If
End Sub

' --- Tooltip on hover ---
Private Sub dgvStudents_CellMouseEnter(...) Handles dgvStudents.CellMouseEnter
    If e.RowIndex < 0 Then Return
    Dim grade = CDbl(dgvStudents.Rows(e.RowIndex).Cells("Grade").Value)
    Dim tip = If(grade < 60, "⚠ Below passing grade",
              If(grade >= 90, "⭐ Excellent!", "✓ Passing"))
    dgvStudents.Rows(e.RowIndex).Cells(e.ColumnIndex).ToolTipText = tip
End Sub
Try It — Simulation 30.3: DataGridView Column Customisation

Toggle column visibility, rename headers, apply colour-coding rules, and switch alignment. Every option shows the equivalent VB property assignment.

DataGridView Column Customisation
Grade format:
Column widths:

30.4 Robust Connection Error Handling

Database connections can fail — the file is missing, the server is down, or the user has no read permission. Wrap all data access in Try…Catch blocks and give the user a clear error message. Separate the connection logic into a helper function for reuse.

ErrorHandling.vb — Visual Basic 2026
' --- Safe load: Try/Catch around all data access ---
Private Sub LoadData()
    Try
        _dt.Clear()
        Using conn As New SqliteConnection(CONN_STR)
            conn.Open()
            Using da As New SqliteDataAdapter("SELECT * FROM Students", conn)
                da.Fill(_dt)
            End Using
        End Using
        _bs.DataSource = _dt
        lblStatus.Text = $"Loaded {_dt.Rows.Count} records."
        lblStatus.ForeColor = Color.DarkGreen
    Catch ex As SqliteException
        ' SQLite-specific error (bad SQL, locked file, etc.)
        ShowError($"Database error: {ex.Message}")
    Catch ex As IO.FileNotFoundException
        ShowError($"Database file not found: {CONN_STR}")
    Catch ex As Exception
        ShowError($"Unexpected error: {ex.Message}")
    End Try
End Sub

Private Sub ShowError(msg As String)
    lblStatus.Text      = $"⚠ {msg}"
    lblStatus.ForeColor = Color.Red
    MessageBox.Show(msg, "Database Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Sub

' --- Connection test helper ---
Function TestConnection() As Boolean
    Try
        Using conn As New SqliteConnection(CONN_STR)
            conn.Open()
            Return True
        End Using
    Catch
        Return False
    End Try
End Function

' --- Form_Load: check before loading ---
Private Sub Form_Load(...) Handles MyBase.Load
    If Not TestConnection() Then
        MessageBox.Show("Cannot connect to database. Check file path.",
                        "Startup Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)
        Return
    End If
    LoadData()
End Sub
Try It — Simulation 30.4: Connection Error Simulation

Simulate different connection failure modes — missing file, wrong password, SQL syntax error — and see how the Try…Catch hierarchy catches each type and shows the right error message.

Connection Error Handling Simulator
Simulate scenario:

30.5 Complete Data-Bound Application

Putting it all together: a form with a BindingSource, BindingNavigator, customised DataGridView, live search, and a detail panel that lets you view and edit the current record — all wired up with fewer than 60 lines of code.

CompleteApp.vb — Visual Basic 2026
' Complete student database form — ~60 lines of logic
Imports Microsoft.Data.Sqlite

Public Class frmStudents
    Private Const CONN_STR = "Data Source=students.db"
    Private _bs As New BindingSource()
    Private _dt As New DataTable()

    Private Sub Form_Load(...) Handles MyBase.Load
        EnsureTable() : LoadData() : SetupGrid() : WireBindings()
    End Sub

    Private Sub EnsureTable()
        Using conn As New SqliteConnection(CONN_STR)
            conn.Open()
            Using cmd As New SqliteCommand(
                "CREATE TABLE IF NOT EXISTS Students (" &
                "StudentID INTEGER PRIMARY KEY AUTOINCREMENT," &
                "Name TEXT, Grade REAL, Class TEXT)", conn)
                cmd.ExecuteNonQuery()
            End Using
        End Using
    End Sub

    Private Sub LoadData()
        Try
            _dt.Clear()
            Using conn As New SqliteConnection(CONN_STR)
                conn.Open()
                Using da As New SqliteDataAdapter("SELECT * FROM Students ORDER BY Name", conn)
                    da.Fill(_dt)
                End Using
            End Using
            _bs.DataSource = _dt
            lblCount.Text  = $"{_bs.Count} records"
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Load Error")
        End Try
    End Sub

    Private Sub SetupGrid()
        dgv.DataSource              = _bs
        dgv.SelectionMode           = DataGridViewSelectionMode.FullRowSelect
        dgv.ReadOnly                = True
        dgv.AlternatingRowsDefaultCellStyle.BackColor = Color.AliceBlue
        dgv.Columns("StudentID").Visible    = False
        dgv.Columns("Name").HeaderText        = "Full Name"
        dgv.Columns("Grade").HeaderText       = "Grade (%)"
        dgv.Columns("Grade").DefaultCellStyle.Format = "N1"
    End Sub

    Private Sub WireBindings()
        BindingNavigator1.BindingSource = _bs
        txtName.DataBindings.Add("Text", _bs, "Name",  True)
        txtGrade.DataBindings.Add("Text", _bs, "Grade", True)
        cboClass.DataBindings.Add("Text", _bs, "Class", True)
    End Sub

    ' --- Live search ---
    Private Sub txtSearch_TextChanged(...) Handles txtSearch.TextChanged
        _bs.Filter = If(txtSearch.Text = "", "", $"Name LIKE '%{txtSearch.Text}%'")
        lblCount.Text = $"{_bs.Count} records"
    End Sub

    ' --- Save changes back to DB ---
    Private Sub btnSave_Click(...) Handles btnSave.Click
        Dim id    = CInt(CType(_bs.Current, DataRowView).Row("StudentID"))
        Dim name  = txtName.Text
        Dim grade = CDbl(txtGrade.Text)
        Dim cls   = cboClass.Text
        Using conn As New SqliteConnection(CONN_STR)
            conn.Open()
            Using cmd As New SqliteCommand(
                "UPDATE Students SET Name=@N, Grade=@G, Class=@C WHERE StudentID=@ID", conn)
                cmd.Parameters.AddWithValue("@N",  name)
                cmd.Parameters.AddWithValue("@G",  grade)
                cmd.Parameters.AddWithValue("@C",  cls)
                cmd.Parameters.AddWithValue("@ID", id)
                cmd.ExecuteNonQuery()
            End Using
        End Using
        LoadData()
    End Sub
End Class
Try It — Simulation 30.5: Complete Data-Bound Application

The full application: BindingNavigator, live search, a detail panel with editable fields, colour-coded rows, and Save changes. Everything a real student records system would need.

frmStudents — Complete Data-Bound Application
of 15 | |
15 records
Edit current record:
Name: Grade: Class:

30.6 GitHub Copilot — Auto-generated Database Form

GitHub Copilot — BindingSource + BindingNavigator setup
You typed: ' Generate a complete VB 2026 form class that uses BindingSource and BindingNavigator to navigate an Employees table (EmployeeID, FullName, Department, Salary, HireDate). Include live search by name, salary range filter, colour-code rows by department, and save edits back to SQLite.'
' Auto-generated by Copilot — Employees form with full binding.
Imports Microsoft.Data.Sqlite

Public Class frmEmployees
    Private Const DB  = "Data Source=hr.db"
    Private _bs         As New BindingSource()
    Private _dt         As New DataTable()
    Private _deptColors As New Dictionary(Of String, Color) From {
        { "Engineering", Color.AliceBlue   },
        { "Marketing",   Color.LavenderBlush},
        { "Finance",     Color.Honeydew     },
        { "HR",          Color.LemonChiffon }
    }

    Private Sub Form_Load(...) Handles MyBase.Load
        EnsureSchema() : LoadEmployees() : ConfigureGrid() : BindControls()
        PopulateDeptFilter()
    End Sub

    Private Sub LoadEmployees(Optional nameFilter As String = "",
                                Optional minSal As Double = 0,
                                Optional dept As String = "")
        _dt.Clear()
        Using conn As New SqliteConnection(DB)
            conn.Open()
            Using da As New SqliteDataAdapter(
                "SELECT * FROM Employees ORDER BY FullName", conn)
                da.Fill(_dt)
            End Using
        End Using
        _bs.DataSource = _dt
        ' Apply BindingSource filters (no DB round-trip)
        Dim parts As New List(Of String)
        If nameFilter <> ""  Then parts.Add($"FullName LIKE '%{nameFilter}%'")
        If minSal > 0        Then parts.Add($"Salary >= {minSal}")
        If dept <> ""        Then parts.Add($"Department = '{dept}'")
        _bs.Filter = String.Join(" AND ", parts)
        lblCount.Text = $"{_bs.Count} / {_dt.Rows.Count}"
    End Sub

    Private Sub ConfigureGrid()
        dgv.DataSource          = _bs
        dgv.SelectionMode       = DataGridViewSelectionMode.FullRowSelect
        dgv.ReadOnly            = True
        dgv.Columns("EmployeeID").Visible = False
        dgv.Columns("Salary").DefaultCellStyle.Format = "C0"   ' currency
        dgv.Columns("HireDate").DefaultCellStyle.Format = "d"   ' short date
    End Sub

    Private Sub BindControls()
        BindingNavigator1.BindingSource = _bs
        txtName.DataBindings.Add("Text", _bs, "FullName",   True)
        txtSalary.DataBindings.Add("Text", _bs, "Salary",    True)
        cboDept.DataBindings.Add("Text", _bs, "Department", True)
        dtpHire.DataBindings.Add("Value", _bs, "HireDate",  True)
    End Sub

    Private Sub dgv_RowPrePaint(...) Handles dgv.RowPrePaint
        Dim dept = dgv.Rows(e.RowIndex).Cells("Department").Value?.ToString()
        If _deptColors.ContainsKey(dept) Then
            dgv.Rows(e.RowIndex).DefaultCellStyle.BackColor = _deptColors(dept)
        End If
    End Sub

    Private Sub btnSave_Click(...) Handles btnSave.Click
        Dim row = CType(_bs.Current, DataRowView).Row
        Using conn As New SqliteConnection(DB)
            conn.Open()
            Using cmd As New SqliteCommand(
                "UPDATE Employees SET FullName=@N,Salary=@S,Department=@D WHERE EmployeeID=@ID", conn)
                cmd.Parameters.AddWithValue("@N",  txtName.Text)
                cmd.Parameters.AddWithValue("@S",  CDbl(txtSalary.Text))
                cmd.Parameters.AddWithValue("@D",  cboDept.Text)
                cmd.Parameters.AddWithValue("@ID", CInt(row("EmployeeID")))
                cmd.ExecuteNonQuery()
            End Using
        End Using
        LoadEmployees(txtSearch.Text, nudMinSal.Value, cboDeptFilter.Text)
    End Sub
End Class
Copilot Chat Prompts for This Lesson

Try these in the Copilot Chat panel:

  • "Add a ComboBox that auto-populates with distinct Class values from the DataTable using LINQ: cboFilter.DataSource = _dt.AsEnumerable().Select(Function(r) r("Class")).Distinct().ToList()"
  • "Generate a PrintDocument handler that prints the current filtered view of the DataTable as a formatted report — one row per line with a header and page numbers"
  • "Add an Export to CSV button: iterate _bs (the filtered view, not _dt) and write each row to a StreamWriter as comma-separated values with a header row"
  • "Add double-click on a DataGridView row to open a detail form (frmStudentDetail) passing the selected StudentID as a constructor parameter, then reload the grid when frmStudentDetail closes"

Lesson Summary

  • BindingSource sits between your DataTable and UI controls. Set _bs.DataSource = dt, then set dgv.DataSource = _bs and add DataBindings to TextBoxes — all controls share the same current row cursor.
  • BindingNavigator provides a ready-made navigation toolbar (◀◀ ◀ pos/total ▶ ▶▶ + ✕) with zero extra code — just set BindingNavigator1.BindingSource = _bs.
  • _bs.Filter = "Name LIKE '%Ali%'" filters the grid without a new database query. Clear with _bs.Filter = "". Combine conditions with AND.
  • _bs.Sort = "Grade DESC" sorts in-memory. Toggle ASC/DESC by checking the current sort string before assigning.
  • Customise DataGridView after setting DataSource: rename headers (.HeaderText), hide columns (.Visible = False), set widths, apply format strings ("N1", "C0", "d"), and colour-code using RowPrePaint.
  • Always wrap data access in Try…Catch. Catch specific exception types first (SqliteException, FileNotFoundException) before a general Exception catch.
  • Read the current record with CType(_bs.Current, DataRowView).Row("ColumnName"). Navigate programmatically with MoveFirst / MovePrevious / MoveNext / MoveLast or assign _bs.Position = index.

Exercises

Exercise 30.1 — Employee Navigator

  • Create an Employees table (EmployeeID, Name, Department, Salary). Seed with 10 rows.
  • Use a BindingSource + BindingNavigator for row-by-row navigation. Bind TextBox controls to show Name, Department, and Salary for the current row.
  • Add a live search TextBox that sets _bs.Filter. Update a record-count Label on PositionChanged.
  • Colour-code grid rows: Salary > 8000 = light green, Salary < 4000 = light red.
  • Copilot challenge: "Add a chart panel below the grid that uses GDI+ to draw a horizontal bar chart of the top 5 salaries, updating whenever the filter changes"

Exercise 30.2 — Product Catalogue

  • Products table (ProductID, Name, Category, Price, StockQty). Load into DataGridView with BindingSource.
  • Add a Category ComboBox filter (auto-populated with distinct categories from the DataTable using LINQ). Combine with a price range filter using two NumericUpDown controls.
  • Customise columns: hide ProductID, format Price as "C2", right-align numbers, highlight out-of-stock rows (StockQty = 0) in yellow.
  • Click a row to populate a form for editing. Save button writes changes back with parameterised UPDATE.
  • Copilot challenge: "Add a Summary panel showing Total Stock Value (SUM of Price × StockQty) and Count of Out-of-Stock items, recalculated whenever the filter changes using LINQ on the DataTable"

Exercise 30.3 — Book Library with Navigation

  • Books table (BookID, Title, Author, Year, Available). Full BindingSource + BindingNavigator setup.
  • Filter by title search + availability (ComboBox: All / Available / Borrowed).
  • Column customisation: format Year as "N0", hide BookID, colour-code borrowed rows in salmon.
  • Borrow / Return buttons that update Available in the database and reload, preserving the current filter.
  • Copilot challenge: "Export the current filtered view to HTML: build a string with table tags and rows from _bs, then open it in the WebView2 control on the form"

Next: Lesson 31 — Editing Data

Master the complete INSERT / UPDATE / DELETE cycle — inline DataGridView editing, transaction support, undo/redo, and the CommandBuilder pattern.

Continue »

Related Resources


Featured Books

Visual Basic 2022 Made Easy

Visual Basic 2022 Made Easy

by Dr. Liew Voon Kiong

BindingSource, BindingNavigator, and DataGridView chapters with complete worked examples.

View on Amazon →
VB Programming With Code Examples

VB Programming With Code Examples

by Dr. Liew Voon Kiong

Data-bound form projects: employee records, product catalogues, and library systems.

View on Amazon →