Use transactions, editable drafts and backups
Keep a two-record update atomic, validate and save a BindingSource draft, inspect schema and back up the database.
Startup applies a migration before reading Accounts and verifies the schema. Both transfer commands are created on the same DbTransaction. Commit changes both balances; the explicit rollback path changes neither. Draft editing uses BeginEdit, Require, SetText, Validate and SaveAsync, then requeries committed data. Backup refuses to overwrite an existing backup. This is a practice dataset of units, not financial software.
Step 1Prepare a safe practice project#
Extract the archive into a new writable folder, separate from your own application. Open DataWorkbench.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": "mcbasic-web-data-workbench",
"name": "DataWorkbench",
"kind": "DesktopApp",
"language": {
"dialect": "McBasic1",
"requireExplicit": true,
"compare": "Binary",
"defaultArrayBase": 0
},
"application": {
"bundleIdentifier": "org.mcbasic.web.dataworkbench",
"version": "1.0.0",
"buildNumber": "1",
"startupFormId": "main",
"shutdownMode": "OnStartupFormClosed"
},
"forms": [
{
"id": "main",
"typeName": "frmMain",
"layout": "frmMain.form.json",
"code": "frmMain.bas"
}
],
"modules": [],
"classes": [],
"tests": [],
"references": [],
"resources": [],
"targets": [
"osx-arm64"
],
"capabilities": []
}Step 2Build the Form and set its properties#
frmMain
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 | "Data Workbench" |
Width | 700 |
Height | 500 |
MinWidth | 660 |
MinHeight | 460 |
| Type | Name | Parent |
|---|---|---|
| DataGrid | grdAccounts | Form |
| CheckBox | chkRollback | Form |
| CommandButton | cmdTransfer | Form |
| TextBox | txtName | Form |
| CommandButton | cmdRename | Form |
| CommandButton | cmdBackup | Form |
| Label | lblStatus | Form |
grdAccounts · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 24 |
Width | 640 |
Height | 180 |
IsReadOnly | True |
TabIndex | 0 |
| Anchor edges | Left, Top, Right |
chkRollback · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 220 |
Width | 290 |
Height | 30 |
Caption | "Deliberately roll back the transfer" |
Value | False |
TabIndex | 1 |
cmdTransfer · exact Inspector values
| Property | Value |
|---|---|
Left | 340 |
Top | 220 |
Width | 170 |
Height | 36 |
Caption | "Transfer 10 units" |
Enabled | False |
TabIndex | 2 |
txtName · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 274 |
Width | 260 |
Height | 36 |
Text | "Primary account" |
AccessibleName | "First account name" |
TabIndex | 3 |
cmdRename · exact Inspector values
| Property | Value |
|---|---|
Left | 304 |
Top | 274 |
Width | 170 |
Height | 36 |
Caption | "Save draft name" |
Enabled | False |
TabIndex | 4 |
cmdBackup · exact Inspector values
| Property | Value |
|---|---|
Left | 494 |
Top | 274 |
Width | 170 |
Height | 36 |
Caption | "Back up database" |
Enabled | False |
TabIndex | 5 |
lblStatus · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 334 |
Width | 640 |
Height | 110 |
Caption | "Opening the practice database…" |
WordWrap | True |
| Anchor edges | Left, Top, Right |
Step 3Connect the events explicitly#
frmMain
| Object | Event | Procedure | Parameters |
|---|---|---|---|
cmdTransfer | Click | cmdTransfer_Click | Click() |
cmdRename | Click | cmdRename_Click | Click() |
cmdBackup | Click | cmdBackup_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.
frmMain.bas
Enter these declarations at file scope, before the procedures.
Option Explicit
Private mDb As Database
Private mRows As BindingSource
Private mBusy As BooleanPrivate Sub SetBusy(ByVal value As Boolean)
Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.
Private Sub SetBusy(ByVal value As Boolean)
mBusy = value
cmdTransfer.Enabled = Not value
cmdRename.Enabled = Not value
cmdBackup.Enabled = Not value
End SubPrivate 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 migrations As MigrationSet
Dim schema As DbSchema
Dim version As Long
mBusy = True
Try
Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("data-workbench.db"))
Set migrations = Sqlite.CreateMigrationSet()
migrations.Add 1, "CREATE TABLE Accounts (ID INTEGER PRIMARY KEY, Name TEXT NOT NULL, Balance INTEGER NOT NULL CHECK(Balance >= 0)); INSERT INTO Accounts VALUES(1, 'First', 100); INSERT INTO Accounts VALUES(2, 'Second', 100);"
version = Await mDb.ApplyMigrationsAsync(migrations)
Set schema = Await mDb.GetSchemaAsync()
If Not schema.HasTable("Accounts") Then Err.Raise 513, "DataWorkbench", "Accounts table missing."
Await RefreshRows()
SetBusy False
lblStatus.Caption = "Migration " & CStr(version) & "; " & CStr(schema.TableCount) & " table(s). Transfers preserve a total of 200 units."
Catch failure As McException
lblStatus.Caption = "Startup failed: " & failure.Message
mBusy = False
End Try
End SubPrivate Async Function RefreshRows() 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 RefreshRows() As Boolean
Dim command As DbCommand
Dim rows As Recordset
Set command = mDb.CreateCommand("SELECT ID, Name, Balance FROM Accounts ORDER BY ID")
Set rows = Await command.QueryAsync()
Set mRows = rows.Bind()
Set grdAccounts.DataSource = mRows
RefreshRows = True
End FunctionPrivate Async Sub cmdTransfer_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdTransfer_Click()
Dim transaction As DbTransaction
Dim command As DbCommand
Dim changed As LongLong
If mBusy Then Exit Sub
SetBusy True
Try
Set transaction = Await mDb.BeginTransactionAsync()
Set command = transaction.CreateCommand("UPDATE Accounts SET Balance = Balance - @amount WHERE ID = 1 AND Balance >= @amount")
command.AddParameter "@amount", 10
changed = Await command.ExecuteAsync()
If changed <> 1 Then Err.Raise 513, "DataWorkbench", "The first account has fewer than 10 units."
Set command = transaction.CreateCommand("UPDATE Accounts SET Balance = Balance + @amount WHERE ID = 2")
command.AddParameter "@amount", 10
Await command.ExecuteAsync()
If chkRollback.Value Then
Await transaction.RollbackAsync()
lblStatus.Caption = "Rolled back: neither account changed."
Else
Await transaction.CommitAsync()
lblStatus.Caption = "Committed: 10 units moved between accounts."
End If
Catch failure As McException
lblStatus.Caption = "Transfer failed: " & failure.Message
Finally
If Not transaction Is Nothing Then transaction.Dispose
SetBusy False
End Try
Await RefreshRows()
End SubPrivate Async Sub cmdRename_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdRename_Click()
Dim saved As Long
If mBusy Then Exit Sub
SetBusy True
Try
mRows.BeginEdit
mRows.Require "Name"
mRows.SetText 0, "Name", Trim$(txtName.Text)
If Not mRows.Validate() Then
lblStatus.Caption = "Draft rejected: " & mRows.ValidationError
mRows.CancelEdit
Exit Sub
End If
saved = Await mRows.SaveAsync(mDb, "Accounts", "ID")
lblStatus.Caption = "Saved " & CStr(saved) & " draft change(s)."
Await RefreshRows()
Catch failure As McException
mRows.CancelEdit
lblStatus.Caption = "Draft save failed: " & failure.Message
Finally
SetBusy False
End Try
End SubPrivate Async Sub cmdBackup_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdBackup_Click()
Dim destination As String
If mBusy Then Exit Sub
destination = AppPaths.DataFile("data-workbench-backup.db")
If File.Exists(destination) Then
lblStatus.Caption = "Backup already exists; preserve it or move it before making a new backup."
Exit Sub
End If
SetBusy True
Try
Await mDb.BackupAsync(destination)
lblStatus.Caption = "Backup created: " & destination
Catch failure As McException
lblStatus.Caption = "Backup failed: " & failure.Message
Finally
SetBusy False
End Try
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 = "Finish the current data operation before closing."
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
End SubCopy the complete frmMain.bas file
Replace the file once; do not append duplicate procedures.
Option Explicit
Private mDb As Database
Private mRows As BindingSource
Private mBusy As Boolean
Private Sub SetBusy(ByVal value As Boolean)
mBusy = value
cmdTransfer.Enabled = Not value
cmdRename.Enabled = Not value
cmdBackup.Enabled = Not value
End Sub
Private Async Sub Form_Shown()
Dim migrations As MigrationSet
Dim schema As DbSchema
Dim version As Long
mBusy = True
Try
Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("data-workbench.db"))
Set migrations = Sqlite.CreateMigrationSet()
migrations.Add 1, "CREATE TABLE Accounts (ID INTEGER PRIMARY KEY, Name TEXT NOT NULL, Balance INTEGER NOT NULL CHECK(Balance >= 0)); INSERT INTO Accounts VALUES(1, 'First', 100); INSERT INTO Accounts VALUES(2, 'Second', 100);"
version = Await mDb.ApplyMigrationsAsync(migrations)
Set schema = Await mDb.GetSchemaAsync()
If Not schema.HasTable("Accounts") Then Err.Raise 513, "DataWorkbench", "Accounts table missing."
Await RefreshRows()
SetBusy False
lblStatus.Caption = "Migration " & CStr(version) & "; " & CStr(schema.TableCount) & " table(s). Transfers preserve a total of 200 units."
Catch failure As McException
lblStatus.Caption = "Startup failed: " & failure.Message
mBusy = False
End Try
End Sub
Private Async Function RefreshRows() As Boolean
Dim command As DbCommand
Dim rows As Recordset
Set command = mDb.CreateCommand("SELECT ID, Name, Balance FROM Accounts ORDER BY ID")
Set rows = Await command.QueryAsync()
Set mRows = rows.Bind()
Set grdAccounts.DataSource = mRows
RefreshRows = True
End Function
Private Async Sub cmdTransfer_Click()
Dim transaction As DbTransaction
Dim command As DbCommand
Dim changed As LongLong
If mBusy Then Exit Sub
SetBusy True
Try
Set transaction = Await mDb.BeginTransactionAsync()
Set command = transaction.CreateCommand("UPDATE Accounts SET Balance = Balance - @amount WHERE ID = 1 AND Balance >= @amount")
command.AddParameter "@amount", 10
changed = Await command.ExecuteAsync()
If changed <> 1 Then Err.Raise 513, "DataWorkbench", "The first account has fewer than 10 units."
Set command = transaction.CreateCommand("UPDATE Accounts SET Balance = Balance + @amount WHERE ID = 2")
command.AddParameter "@amount", 10
Await command.ExecuteAsync()
If chkRollback.Value Then
Await transaction.RollbackAsync()
lblStatus.Caption = "Rolled back: neither account changed."
Else
Await transaction.CommitAsync()
lblStatus.Caption = "Committed: 10 units moved between accounts."
End If
Catch failure As McException
lblStatus.Caption = "Transfer failed: " & failure.Message
Finally
If Not transaction Is Nothing Then transaction.Dispose
SetBusy False
End Try
Await RefreshRows()
End Sub
Private Async Sub cmdRename_Click()
Dim saved As Long
If mBusy Then Exit Sub
SetBusy True
Try
mRows.BeginEdit
mRows.Require "Name"
mRows.SetText 0, "Name", Trim$(txtName.Text)
If Not mRows.Validate() Then
lblStatus.Caption = "Draft rejected: " & mRows.ValidationError
mRows.CancelEdit
Exit Sub
End If
saved = Await mRows.SaveAsync(mDb, "Accounts", "ID")
lblStatus.Caption = "Saved " & CStr(saved) & " draft change(s)."
Await RefreshRows()
Catch failure As McException
mRows.CancelEdit
lblStatus.Caption = "Draft save failed: " & failure.Message
Finally
SetBusy False
End Try
End Sub
Private Async Sub cmdBackup_Click()
Dim destination As String
If mBusy Then Exit Sub
destination = AppPaths.DataFile("data-workbench-backup.db")
If File.Exists(destination) Then
lblStatus.Caption = "Backup already exists; preserve it or move it before making a new backup."
Exit Sub
End If
SetBusy True
Try
Await mDb.BackupAsync(destination)
lblStatus.Caption = "Backup created: " & destination
Catch failure As McException
lblStatus.Caption = "Backup failed: " & failure.Message
Finally
SetBusy False
End Try
End Sub
Private Sub Form_QueryUnload(ByRef Cancel As Integer, ByVal UnloadMode As Integer)
If mBusy Then
Cancel = 1
lblStatus.Caption = "Finish the current data operation before closing."
End If
End Sub
Private Sub Form_Unload()
If Not mDb Is Nothing Then mDb.Dispose
End SubStep 5Keep resources and storage reproducible#
No external resource or migration files are declared. Any inline migration, seed data or local JSON is present in the code above. Runtime application data and private preferences are not included in the archive.
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 DataWorkbench.mcproj
mcbasic run DataWorkbench.mcprojAcceptance checklist
- On a fresh database, each account has 100 units.
- Transfer once: balances become 90 and 110 and still sum to 200.
- Select rollback and transfer again: neither balance changes.
- Enter an empty draft name: validation rejects it. Enter a name and save; reopening preserves it.
- Create a backup; a second attempt must preserve the existing backup.
- Try closing while an operation is busy: the Form asks you to wait.
Step 7Make one change and repeat the tests#
Add a third account through a new migration and preserve the stable ID used by draft saving.
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