Build a complete customer workspace
Combine migrations, list/detail Forms, cancellable writes and reporting.
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
{
"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 property | Value |
|---|---|
Caption | "CustomerDesk — Customers" |
Width | 900 |
Height | 620 |
| Type | Name | Parent |
|---|---|---|
| Label | lblFilter | Form |
| TextBox | txtFilter | Form |
| CommandButton | cmdFilter | Form |
| CommandButton | cmdRefresh | Form |
| Label | lblName | Form |
| TextBox | txtName | Form |
| Label | lblEmail | Form |
| TextBox | txtEmail | Form |
| CommandButton | cmdAdd | Form |
| CommandButton | cmdOpen | Form |
| CommandButton | cmdExport | Form |
| CommandButton | cmdReport | Form |
| CommandButton | cmdQuit | Form |
| CommandButton | cmdCancelAdd | Form |
| Label | lblStatus | Form |
| DataGrid | grdCustomers | Form |
lblFilter · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 24 |
Width | 70 |
Height | 30 |
Caption | "Filter" |
| Anchor edges | Left, Top |
txtFilter · exact Inspector values
| Property | Value |
|---|---|
Left | 100 |
Top | 20 |
Width | 230 |
Height | 36 |
Text | "" |
TabIndex | 0 |
| Anchor edges | Left, Top |
cmdFilter · exact Inspector values
| Property | Value |
|---|---|
Left | 342 |
Top | 20 |
Width | 100 |
Height | 36 |
Caption | "Apply" |
TabIndex | 1 |
| Anchor edges | Left, Top |
cmdRefresh · exact Inspector values
| Property | Value |
|---|---|
Left | 454 |
Top | 20 |
Width | 110 |
Height | 36 |
Caption | "Refresh" |
TabIndex | 2 |
| Anchor edges | Left, Top |
lblName · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 78 |
Width | 65 |
Height | 30 |
Caption | "Name" |
| Anchor edges | Left, Top |
txtName · exact Inspector values
| Property | Value |
|---|---|
Left | 100 |
Top | 74 |
Width | 220 |
Height | 36 |
Text | "" |
TabIndex | 3 |
| Anchor edges | Left, Top |
lblEmail · exact Inspector values
| Property | Value |
|---|---|
Left | 340 |
Top | 78 |
Width | 65 |
Height | 30 |
Caption | "Email" |
| Anchor edges | Left, Top |
txtEmail · exact Inspector values
| Property | Value |
|---|---|
Left | 410 |
Top | 74 |
Width | 240 |
Height | 36 |
Text | "" |
TabIndex | 4 |
| Anchor edges | Left, Top |
cmdAdd · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 128 |
Width | 110 |
Height | 36 |
Caption | "Add" |
TabIndex | 5 |
| Anchor edges | Left, Top |
cmdOpen · exact Inspector values
| Property | Value |
|---|---|
Left | 146 |
Top | 128 |
Width | 130 |
Height | 36 |
Caption | "Edit selected" |
TabIndex | 6 |
| Anchor edges | Left, Top |
cmdExport · exact Inspector values
| Property | Value |
|---|---|
Left | 288 |
Top | 128 |
Width | 130 |
Height | 36 |
Caption | "Export CSV" |
TabIndex | 7 |
| Anchor edges | Left, Top |
cmdReport · exact Inspector values
| Property | Value |
|---|---|
Left | 430 |
Top | 128 |
Width | 130 |
Height | 36 |
Caption | "Report PDF" |
TabIndex | 8 |
| Anchor edges | Left, Top |
cmdQuit · exact Inspector values
| Property | Value |
|---|---|
Left | 572 |
Top | 128 |
Width | 100 |
Height | 36 |
Caption | "Quit" |
TabIndex | 9 |
| Anchor edges | Left, Top |
cmdCancelAdd · exact Inspector values
| Property | Value |
|---|---|
Left | 684 |
Top | 128 |
Width | 140 |
Height | 36 |
Caption | "Cancel Add" |
Enabled | False |
TabIndex | 10 |
| Anchor edges | Left, Top |
lblStatus · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 178 |
Width | 820 |
Height | 34 |
Caption | "Starting..." |
| Anchor edges | Left, Top |
grdCustomers · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 220 |
Width | 850 |
Height | 360 |
TabIndex | 11 |
| Anchor edges | Left, 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 property | Value |
|---|---|
Caption | "CustomerDesk — Edit customer" |
Width | 500 |
Height | 340 |
| Type | Name | Parent |
|---|---|---|
| Label | lblName | Form |
| TextBox | txtName | Form |
| Label | lblEmail | Form |
| TextBox | txtEmail | Form |
| CommandButton | cmdSave | Form |
| CommandButton | cmdClose | Form |
| CommandButton | cmdReload | Form |
| CommandButton | cmdCancelSave | Form |
| Label | lblStatus | Form |
lblName · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 24 |
Width | 70 |
Height | 30 |
Caption | "Name" |
| Anchor edges | Left, Top |
txtName · exact Inspector values
| Property | Value |
|---|---|
Left | 100 |
Top | 20 |
Width | 350 |
Height | 36 |
Text | "" |
TabIndex | 0 |
| Anchor edges | Left, Top |
lblEmail · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 78 |
Width | 70 |
Height | 30 |
Caption | "Email" |
| Anchor edges | Left, Top |
txtEmail · exact Inspector values
| Property | Value |
|---|---|
Left | 100 |
Top | 74 |
Width | 350 |
Height | 36 |
Text | "" |
TabIndex | 1 |
| Anchor edges | Left, Top |
cmdSave · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 132 |
Width | 130 |
Height | 36 |
Caption | "Save" |
TabIndex | 2 |
| Anchor edges | Left, Top |
cmdClose · exact Inspector values
| Property | Value |
|---|---|
Left | 318 |
Top | 132 |
Width | 130 |
Height | 36 |
Caption | "Close" |
TabIndex | 4 |
| Anchor edges | Left, Top |
cmdReload · exact Inspector values
| Property | Value |
|---|---|
Left | 166 |
Top | 132 |
Width | 140 |
Height | 36 |
Caption | "Discard & reload" |
TabIndex | 3 |
| Anchor edges | Left, Top |
cmdCancelSave · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 188 |
Width | 130 |
Height | 36 |
Caption | "Cancel Save" |
Enabled | False |
TabIndex | 5 |
| Anchor edges | Left, Top |
lblStatus · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 238 |
Width | 430 |
Height | 60 |
Caption | "Opening..." |
| Anchor edges | Left, Top |
Step 3Connect the events explicitly#
frmCustomers
| Object | Event | Procedure | Parameters |
|---|---|---|---|
cmdFilter | Click | cmdFilter_Click | Click() |
cmdRefresh | Click | cmdRefresh_Click | Click() |
cmdAdd | Click | cmdAdd_Click | Click() |
cmdOpen | Click | cmdOpen_Click | Click() |
cmdExport | Click | cmdExport_Click | Click() |
cmdReport | Click | cmdReport_Click | Click() |
cmdQuit | Click | cmdQuit_Click | Click() |
cmdCancelAdd | Click | cmdCancelAdd_Click | Click() |
| Form | Shown | Form_Shown | () |
| Form | QueryUnload | Form_QueryUnload | (ByRef Cancel As Integer, ByVal UnloadMode As Integer) |
| Form | Unload | Form_Unload | () |
frmCustomer
| Object | Event | Procedure | Parameters |
|---|---|---|---|
cmdSave | Click | cmdSave_Click | Click() |
cmdClose | Click | cmdClose_Click | Click() |
cmdReload | Click | cmdReload_Click | Click() |
cmdCancelSave | Click | cmdCancelSave_Click | Click() |
| Form | Shown | Form_Shown | () |
| Form | QueryUnload | Form_QueryUnload | (ByRef Cancel As Integer, ByVal UnloadMode As Integer) |
| Form | Unload | Form_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.
Option Explicit
Private mDb As Database
Private mRows As Recordset
Private mBusy As Boolean
Private mAddCommand As DbCommand
Private mAddCancelRequested As BooleanPrivate Async Sub Form_Shown()
Initialize state before enabling dependent actions. Keep the shared declarations and startup dependencies with this procedure.
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 SubPrivate Async Sub cmdReport_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate 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.
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 FunctionPrivate Async Sub cmdFilter_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Async Sub cmdRefresh_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Async Sub cmdAdd_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Sub cmdCancelAdd_Click()
Enter the complete action, including its input checks and calls to helper procedures.
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 SubPrivate Sub cmdOpen_Click()
Enter the complete action, including its input checks and calls to helper procedures.
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 SubPrivate Async Sub cmdExport_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Sub cmdQuit_Click()
Enter the complete action, including its input checks and calls to helper procedures.
Private Sub cmdQuit_Click()
If mBusy Then Exit Sub
App.Quit
End SubPrivate 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.
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 SubPrivate Sub Form_Unload()
This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.
Private Sub Form_Unload()
If Not mDb Is Nothing Then
mDb.Dispose
Set mDb = Nothing
End If
End SubCopy the complete Forms/frmCustomers.bas file
Replace the file once; do not append duplicate procedures.
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 SubForms/frmCustomer.bas
Enter these declarations at file scope, before the procedures.
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 BooleanPrivate Async Sub Form_Shown()
Initialize state before enabling dependent actions. Keep the shared declarations and startup dependencies with this procedure.
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 SubPrivate Async Sub cmdSave_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Sub cmdCancelSave_Click()
Enter the complete action, including its input checks and calls to helper procedures.
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 SubPrivate Async Sub cmdReload_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 SubPrivate Sub cmdClose_Click()
Enter the complete action, including its input checks and calls to helper procedures.
Private Sub cmdClose_Click()
If mBusy Then Exit Sub
Unload Me
End SubPrivate 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.
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 SubPrivate Sub Form_Unload()
This is the lifetime boundary. Preserve the cancellation signature where present and release resources owned by this Form.
Private Sub Form_Unload()
If Not mDb Is Nothing Then
mDb.Dispose
Set mDb = Nothing
End If
End SubCopy the complete Forms/frmCustomer.bas file
Replace the file once; do not append duplicate procedures.
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 SubStep 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.
| File | Purpose |
|---|---|
Database/migrations/0001_initial.sql | Migration 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:
mcbasic check CustomerDesk.mcproj
mcbasic run CustomerDesk.mcprojAcceptance 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.
20f4bfed6b · Build manifest