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

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.

1 Form / control designs1 complete BASIC filesOriginal web lesson

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
DataWorkbench.mcproj
{
  "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 propertyValue
Caption"Data Workbench"
Width700
Height500
MinWidth660
MinHeight460
TypeNameParent
DataGridgrdAccountsForm
CheckBoxchkRollbackForm
CommandButtoncmdTransferForm
TextBoxtxtNameForm
CommandButtoncmdRenameForm
CommandButtoncmdBackupForm
LabellblStatusForm
grdAccounts · exact Inspector values
PropertyValue
Left24
Top24
Width640
Height180
IsReadOnlyTrue
TabIndex0
Anchor edgesLeft, Top, Right
chkRollback · exact Inspector values
PropertyValue
Left24
Top220
Width290
Height30
Caption"Deliberately roll back the transfer"
ValueFalse
TabIndex1
cmdTransfer · exact Inspector values
PropertyValue
Left340
Top220
Width170
Height36
Caption"Transfer 10 units"
EnabledFalse
TabIndex2
txtName · exact Inspector values
PropertyValue
Left24
Top274
Width260
Height36
Text"Primary account"
AccessibleName"First account name"
TabIndex3
cmdRename · exact Inspector values
PropertyValue
Left304
Top274
Width170
Height36
Caption"Save draft name"
EnabledFalse
TabIndex4
cmdBackup · exact Inspector values
PropertyValue
Left494
Top274
Width170
Height36
Caption"Back up database"
EnabledFalse
TabIndex5
lblStatus · exact Inspector values
PropertyValue
Left24
Top334
Width640
Height110
Caption"Opening the practice database…"
WordWrapTrue
Anchor edgesLeft, Top, Right

Step 3Connect the events explicitly#

frmMain

ObjectEventProcedureParameters
cmdTransferClickcmdTransfer_ClickClick()
cmdRenameClickcmdRename_ClickClick()
cmdBackupClickcmdBackup_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.

frmMain.bas

Enter these declarations at file scope, before the procedures.

frmMain.bas · declarations
Option Explicit

Private mDb As Database
Private mRows As BindingSource
Private mBusy As Boolean
Private 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.

frmMain.bas · SetBusy
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()

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

frmMain.bas · Form_Shown
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

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

frmMain.bas · RefreshRows
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()

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

frmMain.bas · cmdTransfer_Click
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()

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

frmMain.bas · cmdRename_Click
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()

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

frmMain.bas · cmdBackup_Click
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)

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

frmMain.bas · Form_QueryUnload
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()

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

frmMain.bas · Form_Unload
Private Sub Form_Unload()
    If Not mDb Is Nothing Then mDb.Dispose
End Sub
Copy the complete frmMain.bas file

Replace the file once; do not append duplicate procedures.

frmMain.bas
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 Sub

Step 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:

Developer CLI
mcbasic check DataWorkbench.mcproj
mcbasic run DataWorkbench.mcproj

Acceptance 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.

Source baseline 20f4bfed6b · Build manifest