McBasic™Get McBasic™ ↗
Build 881
Hands-on tutorial · Data & services

Build a complete customer workspace

Combine migrations, list/detail Forms, cancellable writes and reporting.

2 Form / control designs2 complete BASIC filesGuided sample project

The list Form and editor Form have separate responsibilities. Keep the selected customer identity explicit across the boundary. Startup applies migrations before querying. A busy operation can delay closure through QueryUnload; cancellation and cleanup must complete before Unload releases the Form.

Step 1Prepare a safe practice project#

Extract the archive into a new writable folder, separate from your own application. Open CustomerDesk.mcproj in McBasic™. Keep every declared source, layout, resource and migration file together.

To reconstruct it yourself, create a DesktopApp project and add the files declared below. In the complete project, follow these steps as a guided rebuild: replace each named source file once and Check after all its procedures are present.

Exact project configuration
CustomerDesk.mcproj
{
  "schemaVersion": 1,
  "projectId": "e3175003-dc5c-4f73-846b-c4dcfb5a87c1",
  "name": "CustomerDesk",
  "kind": "DesktopApp",
  "language": {
    "dialect": "McBasic1",
    "requireExplicit": true,
    "compare": "Binary",
    "defaultArrayBase": 0
  },
  "application": {
    "bundleIdentifier": "com.mcbasic.samples.customerdesk",
    "version": "0.1.0",
    "buildNumber": "1",
    "startupFormId": "form-customers",
    "shutdownMode": "OnStartupFormClosed"
  },
  "forms": [
    {
      "id": "form-customers",
      "typeName": "frmCustomers",
      "layout": "Forms/frmCustomers.form.json",
      "code": "Forms/frmCustomers.bas"
    },
    {
      "id": "form-customer",
      "typeName": "frmCustomer",
      "layout": "Forms/frmCustomer.form.json",
      "code": "Forms/frmCustomer.bas"
    }
  ],
  "modules": [],
  "classes": [],
  "tests": [],
  "references": [],
  "resources": [],
  "targets": [
    "osx-arm64"
  ],
  "capabilities": [],
  "migrations": [
    {
      "id": 1,
      "path": "Database/migrations/0001_initial.sql"
    }
  ]
}

Step 2Build the Form and set its properties#

frmCustomers

Select the Form first and set its properties. Add containers before their children. These are initial Designer values, not statements to paste into a procedure.

Form propertyValue
Caption"CustomerDesk — Customers"
Width900
Height620
TypeNameParent
LabellblFilterForm
TextBoxtxtFilterForm
CommandButtoncmdFilterForm
CommandButtoncmdRefreshForm
LabellblNameForm
TextBoxtxtNameForm
LabellblEmailForm
TextBoxtxtEmailForm
CommandButtoncmdAddForm
CommandButtoncmdOpenForm
CommandButtoncmdExportForm
CommandButtoncmdReportForm
CommandButtoncmdQuitForm
CommandButtoncmdCancelAddForm
LabellblStatusForm
DataGridgrdCustomersForm
lblFilter · exact Inspector values
PropertyValue
Left24
Top24
Width70
Height30
Caption"Filter"
Anchor edgesLeft, Top
txtFilter · exact Inspector values
PropertyValue
Left100
Top20
Width230
Height36
Text""
TabIndex0
Anchor edgesLeft, Top
cmdFilter · exact Inspector values
PropertyValue
Left342
Top20
Width100
Height36
Caption"Apply"
TabIndex1
Anchor edgesLeft, Top
cmdRefresh · exact Inspector values
PropertyValue
Left454
Top20
Width110
Height36
Caption"Refresh"
TabIndex2
Anchor edgesLeft, Top
lblName · exact Inspector values
PropertyValue
Left24
Top78
Width65
Height30
Caption"Name"
Anchor edgesLeft, Top
txtName · exact Inspector values
PropertyValue
Left100
Top74
Width220
Height36
Text""
TabIndex3
Anchor edgesLeft, Top
lblEmail · exact Inspector values
PropertyValue
Left340
Top78
Width65
Height30
Caption"Email"
Anchor edgesLeft, Top
txtEmail · exact Inspector values
PropertyValue
Left410
Top74
Width240
Height36
Text""
TabIndex4
Anchor edgesLeft, Top
cmdAdd · exact Inspector values
PropertyValue
Left24
Top128
Width110
Height36
Caption"Add"
TabIndex5
Anchor edgesLeft, Top
cmdOpen · exact Inspector values
PropertyValue
Left146
Top128
Width130
Height36
Caption"Edit selected"
TabIndex6
Anchor edgesLeft, Top
cmdExport · exact Inspector values
PropertyValue
Left288
Top128
Width130
Height36
Caption"Export CSV"
TabIndex7
Anchor edgesLeft, Top
cmdReport · exact Inspector values
PropertyValue
Left430
Top128
Width130
Height36
Caption"Report PDF"
TabIndex8
Anchor edgesLeft, Top
cmdQuit · exact Inspector values
PropertyValue
Left572
Top128
Width100
Height36
Caption"Quit"
TabIndex9
Anchor edgesLeft, Top
cmdCancelAdd · exact Inspector values
PropertyValue
Left684
Top128
Width140
Height36
Caption"Cancel Add"
EnabledFalse
TabIndex10
Anchor edgesLeft, Top
lblStatus · exact Inspector values
PropertyValue
Left24
Top178
Width820
Height34
Caption"Starting..."
Anchor edgesLeft, Top
grdCustomers · exact Inspector values
PropertyValue
Left24
Top220
Width850
Height360
TabIndex11
Anchor edgesLeft, Top

frmCustomer

Select the Form first and set its properties. Add containers before their children. These are initial Designer values, not statements to paste into a procedure.

Form propertyValue
Caption"CustomerDesk — Edit customer"
Width500
Height340
TypeNameParent
LabellblNameForm
TextBoxtxtNameForm
LabellblEmailForm
TextBoxtxtEmailForm
CommandButtoncmdSaveForm
CommandButtoncmdCloseForm
CommandButtoncmdReloadForm
CommandButtoncmdCancelSaveForm
LabellblStatusForm
lblName · exact Inspector values
PropertyValue
Left24
Top24
Width70
Height30
Caption"Name"
Anchor edgesLeft, Top
txtName · exact Inspector values
PropertyValue
Left100
Top20
Width350
Height36
Text""
TabIndex0
Anchor edgesLeft, Top
lblEmail · exact Inspector values
PropertyValue
Left24
Top78
Width70
Height30
Caption"Email"
Anchor edgesLeft, Top
txtEmail · exact Inspector values
PropertyValue
Left100
Top74
Width350
Height36
Text""
TabIndex1
Anchor edgesLeft, Top
cmdSave · exact Inspector values
PropertyValue
Left24
Top132
Width130
Height36
Caption"Save"
TabIndex2
Anchor edgesLeft, Top
cmdClose · exact Inspector values
PropertyValue
Left318
Top132
Width130
Height36
Caption"Close"
TabIndex4
Anchor edgesLeft, Top
cmdReload · exact Inspector values
PropertyValue
Left166
Top132
Width140
Height36
Caption"Discard & reload"
TabIndex3
Anchor edgesLeft, Top
cmdCancelSave · exact Inspector values
PropertyValue
Left24
Top188
Width130
Height36
Caption"Cancel Save"
EnabledFalse
TabIndex5
Anchor edgesLeft, Top
lblStatus · exact Inspector values
PropertyValue
Left24
Top238
Width430
Height60
Caption"Opening..."
Anchor edgesLeft, Top

Step 3Connect the events explicitly#

frmCustomers

ObjectEventProcedureParameters
cmdFilterClickcmdFilter_ClickClick()
cmdRefreshClickcmdRefresh_ClickClick()
cmdAddClickcmdAdd_ClickClick()
cmdOpenClickcmdOpen_ClickClick()
cmdExportClickcmdExport_ClickClick()
cmdReportClickcmdReport_ClickClick()
cmdQuitClickcmdQuit_ClickClick()
cmdCancelAddClickcmdCancelAdd_ClickClick()
FormShownForm_Shown()
FormQueryUnloadForm_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
FormUnloadForm_Unload()

frmCustomer

ObjectEventProcedureParameters
cmdSaveClickcmdSave_ClickClick()
cmdCloseClickcmdClose_ClickClick()
cmdReloadClickcmdReload_ClickClick()
cmdCancelSaveClickcmdCancelSave_ClickClick()
FormShownForm_Shown()
FormQueryUnloadForm_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
FormUnloadForm_Unload()

Step 4Enter the complete behavior#

Open the named source file. Enter declarations first, then every procedure. Expand a procedure for its full body, or copy the complete file. Helpers are required: do not stop after entering the first Click handler.

Forms/frmCustomers.bas

Enter these declarations at file scope, before the procedures.

Forms/frmCustomers.bas · declarations
Option Explicit

Private mDb As Database
Private mRows As Recordset
Private mBusy As Boolean
Private mAddCommand As DbCommand
Private mAddCancelRequested As Boolean
Private Async Sub Form_Shown()

Initialize state before enabling dependent actions. Keep the shared declarations and startup dependencies with this procedure.

Forms/frmCustomers.bas · Form_Shown
Private Async Sub Form_Shown()
    Dim version As Long
    Dim loaded As Boolean

    mBusy = True
    cmdAdd.Enabled = False
    cmdOpen.Enabled = False
    cmdFilter.Enabled = False
    cmdRefresh.Enabled = False
    cmdExport.Enabled = False
    cmdReport.Enabled = False
    cmdCancelAdd.Enabled = False
    lblStatus.Caption = "Opening customer database..."
    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("customerdesk.db"))
        version = Await mDb.ApplyMigrationsAsync(App.Migrations)
        loaded = Await ReloadCustomers()
        cmdAdd.Enabled = True
        cmdOpen.Enabled = True
        cmdFilter.Enabled = True
        cmdRefresh.Enabled = True
        cmdExport.Enabled = True
        cmdReport.Enabled = True
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub
Private Async Sub cmdReport_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomers.bas · cmdReport_Click
Private Async Sub cmdReport_Click()
    Dim command As DbCommand
    Dim rows As Recordset
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdReport.Enabled = False
    lblStatus.Caption = "Preparing customerdesk.pdf..."
    Try
        Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers ORDER BY name, id")
        Set rows = Await command.QueryAsync()
        Await Reports.ExportTablePdfAsync(rows, "CustomerDesk — Customers", AppPaths.DataFile("customerdesk.pdf"))
        lblStatus.Caption = CStr(rows.RowCount) & " customer(s) reported to customerdesk.pdf."
    Catch failure As McException
        lblStatus.Caption = "Report error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdReport.Enabled = True
    End Try
End Sub
Private Async Function ReloadCustomers() As Boolean

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

Forms/frmCustomers.bas · ReloadCustomers
Private Async Function ReloadCustomers() As Boolean
    Dim command As DbCommand
    Dim pattern As String

    pattern = "%" & Trim$(txtFilter.Text) & "%"
    Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers WHERE name LIKE @pattern ORDER BY name, id")
    command.AddParameter "@pattern", pattern
    Set mRows = Await command.QueryAsync()
    Set grdCustomers.DataSource = mRows.Bind()
    grdCustomers.SelectedIndex = -1
    lblStatus.Caption = CStr(mRows.RowCount) & " customer(s)"
    ReloadCustomers = True
End Function
Private Async Sub cmdFilter_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomers.bas · cmdFilter_Click
Private Async Sub cmdFilter_Click()
    Dim loaded As Boolean
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    Try
        loaded = Await ReloadCustomers()
    Catch failure As McException
        lblStatus.Caption = "Filter error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub
Private Async Sub cmdRefresh_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomers.bas · cmdRefresh_Click
Private Async Sub cmdRefresh_Click()
    Dim loaded As Boolean
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    Try
        loaded = Await ReloadCustomers()
    Catch failure As McException
        lblStatus.Caption = "Refresh error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub
Private Async Sub cmdAdd_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomers.bas · cmdAdd_Click
Private Async Sub cmdAdd_Click()
    Dim command As DbCommand
    Dim affected As LongLong
    Dim loaded As Boolean
    Dim email As Variant
    Dim operationReturned As Boolean
    Dim handledError As Boolean

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    If Len(Trim$(txtName.Text)) = 0 Then
        lblStatus.Caption = "Name is required."
        txtName.SetFocus
        Exit Sub
    End If
    If Len(Trim$(txtEmail.Text)) = 0 Then
        email = Null
    Else
        email = Trim$(txtEmail.Text)
    End If
    mBusy = True
    mAddCancelRequested = False
    cmdAdd.Enabled = False
    Try
        Set command = mDb.CreateCommand("INSERT INTO customers(name, email) VALUES(@name, @email)")
        command.AddParameter "@name", Trim$(txtName.Text)
        command.AddParameter "@email", email
        Set mAddCommand = command
        cmdCancelAdd.Enabled = True
        affected = Await command.ExecuteAsync()
        operationReturned = True
        Set mAddCommand = Nothing
        cmdCancelAdd.Enabled = False
        If affected <> 1 Then
            lblStatus.Caption = "Insert affected an unexpected number of rows."
        Else
            txtName.Text = ""
            txtEmail.Text = ""
            loaded = Await ReloadCustomers()
        End If
    Catch failure As McException
        handledError = True
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        Set mAddCommand = Nothing
        cmdCancelAdd.Enabled = False
        If mAddCancelRequested And Not operationReturned And Not handledError Then
            lblStatus.Caption = "Add stopped after cancellation request. Refresh to confirm the data."
        End If
        mBusy = False
        cmdAdd.Enabled = True
    End Try
End Sub
Private Sub cmdCancelAdd_Click()

Enter the complete action, including its input checks and calls to helper procedures.

Forms/frmCustomers.bas · cmdCancelAdd_Click
Private Sub cmdCancelAdd_Click()
    If Not mBusy Then Exit Sub
    If mAddCommand Is Nothing Then Exit Sub
    mAddCancelRequested = True
    cmdCancelAdd.Enabled = False
    lblStatus.Caption = "Cancelling Add..."
    mAddCommand.Cancel
End Sub
Private Sub cmdOpen_Click()

Enter the complete action, including its input checks and calls to helper procedures.

Forms/frmCustomers.bas · cmdOpen_Click
Private Sub cmdOpen_Click()
    Dim details As frmCustomer
    Dim row As Long

    If mBusy Then Exit Sub
    If mRows Is Nothing Then Exit Sub
    row = CInt(grdCustomers.SelectedIndex)
    If row < 0 Or row >= mRows.RowCount Then
        lblStatus.Caption = "Select a customer first."
        Exit Sub
    End If
    Set details = New frmCustomer
    details.CustomerId = CLng(mRows.Value(row, "id"))
    details.ExpectedVersion = CLng(mRows.Value(row, "version"))
    details.DisplayName = CStr(mRows.Value(row, "name"))
    If IsNull(mRows.Value(row, "email")) Then
        details.EmailAddress = ""
    Else
        details.EmailAddress = CStr(mRows.Value(row, "email"))
    End If
    details.Show
    lblStatus.Caption = "Editing customer " & CStr(details.CustomerId) & "; Refresh after closing details."
End Sub
Private Async Sub cmdExport_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomers.bas · cmdExport_Click
Private Async Sub cmdExport_Click()
    Dim command As DbCommand
    Dim rows As Recordset
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdExport.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers ORDER BY name, id")
        Set rows = Await command.QueryAsync()
        Await rows.ExportCsvAsync(AppPaths.DataFile("customerdesk.csv"))
        lblStatus.Caption = CStr(rows.RowCount) & " customer(s) exported."
    Catch failure As McException
        lblStatus.Caption = "Export error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdExport.Enabled = True
    End Try
End Sub
Private Sub cmdQuit_Click()

Enter the complete action, including its input checks and calls to helper procedures.

Forms/frmCustomers.bas · cmdQuit_Click
Private Sub cmdQuit_Click()
    If mBusy Then Exit Sub
    App.Quit
End Sub
Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)

This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.

Forms/frmCustomers.bas · Form_QueryUnload
Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
    If mBusy Then
        Cancel = 1
        lblStatus.Caption = "Wait for the current operation."
    End If
End Sub
Private Sub Form_Unload()

This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.

Forms/frmCustomers.bas · Form_Unload
Private Sub Form_Unload()
    If Not mDb Is Nothing Then
        mDb.Dispose
        Set mDb = Nothing
    End If
End Sub
Copy the complete Forms/frmCustomers.bas file

Replace the file once; do not append duplicate procedures.

Forms/frmCustomers.bas
Option Explicit

Private mDb As Database
Private mRows As Recordset
Private mBusy As Boolean
Private mAddCommand As DbCommand
Private mAddCancelRequested As Boolean

Private Async Sub Form_Shown()
    Dim version As Long
    Dim loaded As Boolean

    mBusy = True
    cmdAdd.Enabled = False
    cmdOpen.Enabled = False
    cmdFilter.Enabled = False
    cmdRefresh.Enabled = False
    cmdExport.Enabled = False
    cmdReport.Enabled = False
    cmdCancelAdd.Enabled = False
    lblStatus.Caption = "Opening customer database..."
    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("customerdesk.db"))
        version = Await mDb.ApplyMigrationsAsync(App.Migrations)
        loaded = Await ReloadCustomers()
        cmdAdd.Enabled = True
        cmdOpen.Enabled = True
        cmdFilter.Enabled = True
        cmdRefresh.Enabled = True
        cmdExport.Enabled = True
        cmdReport.Enabled = True
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub

Private Async Sub cmdReport_Click()
    Dim command As DbCommand
    Dim rows As Recordset
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdReport.Enabled = False
    lblStatus.Caption = "Preparing customerdesk.pdf..."
    Try
        Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers ORDER BY name, id")
        Set rows = Await command.QueryAsync()
        Await Reports.ExportTablePdfAsync(rows, "CustomerDesk — Customers", AppPaths.DataFile("customerdesk.pdf"))
        lblStatus.Caption = CStr(rows.RowCount) & " customer(s) reported to customerdesk.pdf."
    Catch failure As McException
        lblStatus.Caption = "Report error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdReport.Enabled = True
    End Try
End Sub

Private Async Function ReloadCustomers() As Boolean
    Dim command As DbCommand
    Dim pattern As String

    pattern = "%" & Trim$(txtFilter.Text) & "%"
    Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers WHERE name LIKE @pattern ORDER BY name, id")
    command.AddParameter "@pattern", pattern
    Set mRows = Await command.QueryAsync()
    Set grdCustomers.DataSource = mRows.Bind()
    grdCustomers.SelectedIndex = -1
    lblStatus.Caption = CStr(mRows.RowCount) & " customer(s)"
    ReloadCustomers = True
End Function

Private Async Sub cmdFilter_Click()
    Dim loaded As Boolean
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    Try
        loaded = Await ReloadCustomers()
    Catch failure As McException
        lblStatus.Caption = "Filter error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub

Private Async Sub cmdRefresh_Click()
    Dim loaded As Boolean
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    Try
        loaded = Await ReloadCustomers()
    Catch failure As McException
        lblStatus.Caption = "Refresh error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub

Private Async Sub cmdAdd_Click()
    Dim command As DbCommand
    Dim affected As LongLong
    Dim loaded As Boolean
    Dim email As Variant
    Dim operationReturned As Boolean
    Dim handledError As Boolean

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    If Len(Trim$(txtName.Text)) = 0 Then
        lblStatus.Caption = "Name is required."
        txtName.SetFocus
        Exit Sub
    End If
    If Len(Trim$(txtEmail.Text)) = 0 Then
        email = Null
    Else
        email = Trim$(txtEmail.Text)
    End If
    mBusy = True
    mAddCancelRequested = False
    cmdAdd.Enabled = False
    Try
        Set command = mDb.CreateCommand("INSERT INTO customers(name, email) VALUES(@name, @email)")
        command.AddParameter "@name", Trim$(txtName.Text)
        command.AddParameter "@email", email
        Set mAddCommand = command
        cmdCancelAdd.Enabled = True
        affected = Await command.ExecuteAsync()
        operationReturned = True
        Set mAddCommand = Nothing
        cmdCancelAdd.Enabled = False
        If affected <> 1 Then
            lblStatus.Caption = "Insert affected an unexpected number of rows."
        Else
            txtName.Text = ""
            txtEmail.Text = ""
            loaded = Await ReloadCustomers()
        End If
    Catch failure As McException
        handledError = True
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        Set mAddCommand = Nothing
        cmdCancelAdd.Enabled = False
        If mAddCancelRequested And Not operationReturned And Not handledError Then
            lblStatus.Caption = "Add stopped after cancellation request. Refresh to confirm the data."
        End If
        mBusy = False
        cmdAdd.Enabled = True
    End Try
End Sub

Private Sub cmdCancelAdd_Click()
    If Not mBusy Then Exit Sub
    If mAddCommand Is Nothing Then Exit Sub
    mAddCancelRequested = True
    cmdCancelAdd.Enabled = False
    lblStatus.Caption = "Cancelling Add..."
    mAddCommand.Cancel
End Sub

Private Sub cmdOpen_Click()
    Dim details As frmCustomer
    Dim row As Long

    If mBusy Then Exit Sub
    If mRows Is Nothing Then Exit Sub
    row = CInt(grdCustomers.SelectedIndex)
    If row < 0 Or row >= mRows.RowCount Then
        lblStatus.Caption = "Select a customer first."
        Exit Sub
    End If
    Set details = New frmCustomer
    details.CustomerId = CLng(mRows.Value(row, "id"))
    details.ExpectedVersion = CLng(mRows.Value(row, "version"))
    details.DisplayName = CStr(mRows.Value(row, "name"))
    If IsNull(mRows.Value(row, "email")) Then
        details.EmailAddress = ""
    Else
        details.EmailAddress = CStr(mRows.Value(row, "email"))
    End If
    details.Show
    lblStatus.Caption = "Editing customer " & CStr(details.CustomerId) & "; Refresh after closing details."
End Sub

Private Async Sub cmdExport_Click()
    Dim command As DbCommand
    Dim rows As Recordset
    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdExport.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT id, name, email, version FROM customers ORDER BY name, id")
        Set rows = Await command.QueryAsync()
        Await rows.ExportCsvAsync(AppPaths.DataFile("customerdesk.csv"))
        lblStatus.Caption = CStr(rows.RowCount) & " customer(s) exported."
    Catch failure As McException
        lblStatus.Caption = "Export error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdExport.Enabled = True
    End Try
End Sub

Private Sub cmdQuit_Click()
    If mBusy Then Exit Sub
    App.Quit
End Sub


Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
    If mBusy Then
        Cancel = 1
        lblStatus.Caption = "Wait for the current operation."
    End If
End Sub

Private Sub Form_Unload()
    If Not mDb Is Nothing Then
        mDb.Dispose
        Set mDb = Nothing
    End If
End Sub

Forms/frmCustomer.bas

Enter these declarations at file scope, before the procedures.

Forms/frmCustomer.bas · declarations
Option Explicit

Public CustomerId As Long
Public ExpectedVersion As Long
Public DisplayName As String
Public EmailAddress As String

Private mDb As Database
Private mBusy As Boolean
Private mRecordAvailable As Boolean
Private mSaveCommand As DbCommand
Private mSaveCancelRequested As Boolean
Private Async Sub Form_Shown()

Initialize state before enabling dependent actions. Keep the shared declarations and startup dependencies with this procedure.

Forms/frmCustomer.bas · Form_Shown
Private Async Sub Form_Shown()
    mBusy = True
    cmdSave.Enabled = False
    cmdCancelSave.Enabled = False
    cmdReload.Enabled = False
    txtName.Text = DisplayName
    txtEmail.Text = EmailAddress
    lblStatus.Caption = "Opening customer " & CStr(CustomerId) & "..."
    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("customerdesk.db"))
        mRecordAvailable = True
        cmdSave.Enabled = True
        cmdReload.Enabled = True
        lblStatus.Caption = "Version " & CStr(ExpectedVersion)
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub
Private Async Sub cmdSave_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomer.bas · cmdSave_Click
Private Async Sub cmdSave_Click()
    Dim command As DbCommand
    Dim current As DbCommand
    Dim rows As Recordset
    Dim affected As LongLong
    Dim email As Variant
    Dim updateReturned As Boolean
    Dim handledError As Boolean

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    If Not mRecordAvailable Then Exit Sub
    If Len(Trim$(txtName.Text)) = 0 Then
        lblStatus.Caption = "Name is required."
        txtName.SetFocus
        Exit Sub
    End If
    If Len(Trim$(txtEmail.Text)) = 0 Then
        email = Null
    Else
        email = Trim$(txtEmail.Text)
    End If
    mBusy = True
    mSaveCancelRequested = False
    cmdSave.Enabled = False
    cmdCancelSave.Enabled = False
    Try
        Set command = mDb.CreateCommand("UPDATE customers SET name=@name, email=@email, version=version+1 WHERE id=@id AND version=@expectedVersion")
        command.AddParameter "@name", Trim$(txtName.Text)
        command.AddParameter "@email", email
        command.AddParameter "@id", CustomerId
        command.AddParameter "@expectedVersion", ExpectedVersion
        Set mSaveCommand = command
        cmdCancelSave.Enabled = True
        affected = Await command.ExecuteAsync()
        updateReturned = True
        Set mSaveCommand = Nothing
        cmdCancelSave.Enabled = False
        If affected = 0 Then
            Set current = mDb.CreateCommand("SELECT version FROM customers WHERE id=@id")
            current.AddParameter "@id", CustomerId
            Set rows = Await current.QueryAsync()
            If rows.RowCount = 0 Then
                mRecordAvailable = False
                lblStatus.Caption = "Customer no longer exists. Close this detail."
            Else
                lblStatus.Caption = "Conflict: this customer changed. Discard & reload to see the current version."
            End If
        Else
            ExpectedVersion = ExpectedVersion + 1
            DisplayName = Trim$(txtName.Text)
            lblStatus.Caption = "Saved version " & CStr(ExpectedVersion) & ". Refresh the list."
        End If
    Catch failure As McException
        handledError = True
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        Set mSaveCommand = Nothing
        cmdCancelSave.Enabled = False
        If mSaveCancelRequested And Not updateReturned And Not handledError Then
            lblStatus.Caption = "Save stopped after cancellation request. Discard & reload to confirm the current version."
        End If
        mBusy = False
        cmdSave.Enabled = mRecordAvailable
    End Try
End Sub
Private Sub cmdCancelSave_Click()

Enter the complete action, including its input checks and calls to helper procedures.

Forms/frmCustomer.bas · cmdCancelSave_Click
Private Sub cmdCancelSave_Click()
    If Not mBusy Then Exit Sub
    If mSaveCommand Is Nothing Then Exit Sub
    mSaveCancelRequested = True
    cmdCancelSave.Enabled = False
    lblStatus.Caption = "Cancelling Save..."
    mSaveCommand.Cancel
End Sub
Private Async Sub cmdReload_Click()

Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.

Forms/frmCustomer.bas · cmdReload_Click
Private Async Sub cmdReload_Click()
    Dim command As DbCommand
    Dim rows As Recordset

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdSave.Enabled = False
    cmdReload.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT name, email, version FROM customers WHERE id=@id")
        command.AddParameter "@id", CustomerId
        Set rows = Await command.QueryAsync()
        If rows.RowCount = 0 Then
            mRecordAvailable = False
            lblStatus.Caption = "Customer no longer exists. Close this detail."
        Else
            mRecordAvailable = True
            DisplayName = CStr(rows.Value(0, "name"))
            If IsNull(rows.Value(0, "email")) Then
                EmailAddress = ""
            Else
                EmailAddress = CStr(rows.Value(0, "email"))
            End If
            ExpectedVersion = CLng(rows.Value(0, "version"))
            txtName.Text = DisplayName
            txtEmail.Text = EmailAddress
            lblStatus.Caption = "Loaded version " & CStr(ExpectedVersion) & ". Unsaved edits were discarded."
        End If
    Catch failure As McException
        lblStatus.Caption = "Reload error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSave.Enabled = mRecordAvailable
        cmdReload.Enabled = True
    End Try
End Sub
Private Sub cmdClose_Click()

Enter the complete action, including its input checks and calls to helper procedures.

Forms/frmCustomer.bas · cmdClose_Click
Private Sub cmdClose_Click()
    If mBusy Then Exit Sub
    Unload Me
End Sub
Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)

This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.

Forms/frmCustomer.bas · Form_QueryUnload
Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
    If mBusy Then
        Cancel = 1
        lblStatus.Caption = "Wait for the current operation."
    End If
End Sub
Private Sub Form_Unload()

This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.

Forms/frmCustomer.bas · Form_Unload
Private Sub Form_Unload()
    If Not mDb Is Nothing Then
        mDb.Dispose
        Set mDb = Nothing
    End If
End Sub
Copy the complete Forms/frmCustomer.bas file

Replace the file once; do not append duplicate procedures.

Forms/frmCustomer.bas
Option Explicit

Public CustomerId As Long
Public ExpectedVersion As Long
Public DisplayName As String
Public EmailAddress As String

Private mDb As Database
Private mBusy As Boolean
Private mRecordAvailable As Boolean
Private mSaveCommand As DbCommand
Private mSaveCancelRequested As Boolean

Private Async Sub Form_Shown()
    mBusy = True
    cmdSave.Enabled = False
    cmdCancelSave.Enabled = False
    cmdReload.Enabled = False
    txtName.Text = DisplayName
    txtEmail.Text = EmailAddress
    lblStatus.Caption = "Opening customer " & CStr(CustomerId) & "..."
    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("customerdesk.db"))
        mRecordAvailable = True
        cmdSave.Enabled = True
        cmdReload.Enabled = True
        lblStatus.Caption = "Version " & CStr(ExpectedVersion)
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub

Private Async Sub cmdSave_Click()
    Dim command As DbCommand
    Dim current As DbCommand
    Dim rows As Recordset
    Dim affected As LongLong
    Dim email As Variant
    Dim updateReturned As Boolean
    Dim handledError As Boolean

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    If Not mRecordAvailable Then Exit Sub
    If Len(Trim$(txtName.Text)) = 0 Then
        lblStatus.Caption = "Name is required."
        txtName.SetFocus
        Exit Sub
    End If
    If Len(Trim$(txtEmail.Text)) = 0 Then
        email = Null
    Else
        email = Trim$(txtEmail.Text)
    End If
    mBusy = True
    mSaveCancelRequested = False
    cmdSave.Enabled = False
    cmdCancelSave.Enabled = False
    Try
        Set command = mDb.CreateCommand("UPDATE customers SET name=@name, email=@email, version=version+1 WHERE id=@id AND version=@expectedVersion")
        command.AddParameter "@name", Trim$(txtName.Text)
        command.AddParameter "@email", email
        command.AddParameter "@id", CustomerId
        command.AddParameter "@expectedVersion", ExpectedVersion
        Set mSaveCommand = command
        cmdCancelSave.Enabled = True
        affected = Await command.ExecuteAsync()
        updateReturned = True
        Set mSaveCommand = Nothing
        cmdCancelSave.Enabled = False
        If affected = 0 Then
            Set current = mDb.CreateCommand("SELECT version FROM customers WHERE id=@id")
            current.AddParameter "@id", CustomerId
            Set rows = Await current.QueryAsync()
            If rows.RowCount = 0 Then
                mRecordAvailable = False
                lblStatus.Caption = "Customer no longer exists. Close this detail."
            Else
                lblStatus.Caption = "Conflict: this customer changed. Discard & reload to see the current version."
            End If
        Else
            ExpectedVersion = ExpectedVersion + 1
            DisplayName = Trim$(txtName.Text)
            lblStatus.Caption = "Saved version " & CStr(ExpectedVersion) & ". Refresh the list."
        End If
    Catch failure As McException
        handledError = True
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        Set mSaveCommand = Nothing
        cmdCancelSave.Enabled = False
        If mSaveCancelRequested And Not updateReturned And Not handledError Then
            lblStatus.Caption = "Save stopped after cancellation request. Discard & reload to confirm the current version."
        End If
        mBusy = False
        cmdSave.Enabled = mRecordAvailable
    End Try
End Sub

Private Sub cmdCancelSave_Click()
    If Not mBusy Then Exit Sub
    If mSaveCommand Is Nothing Then Exit Sub
    mSaveCancelRequested = True
    cmdCancelSave.Enabled = False
    lblStatus.Caption = "Cancelling Save..."
    mSaveCommand.Cancel
End Sub

Private Async Sub cmdReload_Click()
    Dim command As DbCommand
    Dim rows As Recordset

    If mBusy Then Exit Sub
    If mDb Is Nothing Then Exit Sub
    mBusy = True
    cmdSave.Enabled = False
    cmdReload.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT name, email, version FROM customers WHERE id=@id")
        command.AddParameter "@id", CustomerId
        Set rows = Await command.QueryAsync()
        If rows.RowCount = 0 Then
            mRecordAvailable = False
            lblStatus.Caption = "Customer no longer exists. Close this detail."
        Else
            mRecordAvailable = True
            DisplayName = CStr(rows.Value(0, "name"))
            If IsNull(rows.Value(0, "email")) Then
                EmailAddress = ""
            Else
                EmailAddress = CStr(rows.Value(0, "email"))
            End If
            ExpectedVersion = CLng(rows.Value(0, "version"))
            txtName.Text = DisplayName
            txtEmail.Text = EmailAddress
            lblStatus.Caption = "Loaded version " & CStr(ExpectedVersion) & ". Unsaved edits were discarded."
        End If
    Catch failure As McException
        lblStatus.Caption = "Reload error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSave.Enabled = mRecordAvailable
        cmdReload.Enabled = True
    End Try
End Sub

Private Sub cmdClose_Click()
    If mBusy Then Exit Sub
    Unload Me
End Sub

Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
    If mBusy Then
        Cancel = 1
        lblStatus.Caption = "Wait for the current operation."
    End If
End Sub

Private Sub Form_Unload()
    If Not mDb Is Nothing Then
        mDb.Dispose
        Set mDb = Nothing
    End If
End Sub

Step 5Keep resources and storage reproducible#

Keep the following files at their declared relative paths. Apply migrations before the first query. Media and report resources must travel with the build.

FilePurpose
Database/migrations/0001_initial.sqlMigration SQL
Database/migrations/0001_initial.sql
Database/migrations/0001_initial.sql
CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NULL,
    version INTEGER NOT NULL DEFAULT 1
);

Step 6Check, run and test the result#

Save all files and run Check. Resolve diagnostics before Run. Developer CLI users can work from the extracted folder:

Developer CLI
mcbasic check CustomerDesk.mcproj
mcbasic run CustomerDesk.mcproj

Acceptance checklist

  • Start with a fresh sample data directory and load the list.
  • Add a customer, open the detail Form and save an edit.
  • Cancel an in-progress action where the sample exposes cancellation.
  • Filter, refresh and export the list.
  • Close during an operation and verify the application handles its lifetime without claiming an unfinished write succeeded.

Step 7Make one change and repeat the tests#

Add a new nullable customer field through a new migration, not by editing the existing applied migration.

Save a working Git checkpoint first. Change one behavior, Check again and repeat the acceptance checklist. Compare with the downloaded solution if a regression appears.

Source baseline 20f4bfed6b · Build manifest