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.
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").
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.
' --- 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"))
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.
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.
' --- 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
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.
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").
' --- 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
Toggle column visibility, rename headers, apply colour-coding rules, and switch alignment. Every option shows the equivalent VB property assignment.
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.
' --- 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
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.
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.
' 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
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.
30.6 GitHub Copilot — Auto-generated Database Form
' 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
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 setdgv.DataSource = _bsand addDataBindingsto 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 withAND._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 usingRowPrePaint. - Always wrap data access in Try…Catch. Catch specific exception types first (
SqliteException,FileNotFoundException) before a generalExceptioncatch. - Read the current record with
CType(_bs.Current, DataRowView).Row("ColumnName"). Navigate programmatically withMoveFirst / MovePrevious / MoveNext / MoveLastor 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 onPositionChanged. - 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"
Related Resources
← Lesson 29
Database Intro — ADO.NET, SQL, DataAdapter.
Lesson 31 →
Editing Data — CommandBuilder, transactions.
MS Docs — BindingSource
Full BindingSource API reference.
MS Docs — DataGridView
Column types, events, and customisation.
Featured Books
Visual Basic 2022 Made Easy
BindingSource, BindingNavigator, and DataGridView chapters with complete worked examples.
View on Amazon →
VB Programming With Code Examples
Data-bound form projects: employee records, product catalogues, and library systems.
View on Amazon →