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

Build a library with local data and web search

Connect search, JSON parsing, SQLite storage and CSV export.

1 Form / control designs1 complete BASIC filesGuided sample project

The workflow has two sources of truth: remote search results and saved local books. Save only after a valid selection exists. Apply the declared migration before the first query. A failed network request should leave saved books intact and produce a useful message rather than an empty success state.

Step 1Prepare a safe practice project#

Extract the archive into a new writable folder, separate from your own application. Open LibraryDesk.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
LibraryDesk.mcproj
{
  "schemaVersion": 1,
  "projectId": "2d0b0c2d-6db2-4e04-8c1c-7bbd9c5e7a01",
  "name": "LibraryDesk",
  "kind": "DesktopApp",
  "language": {
    "dialect": "McBasic1",
    "requireExplicit": true,
    "compare": "Binary",
    "defaultArrayBase": 0
  },
  "application": {
    "bundleIdentifier": "com.mcbasic.samples.librarydesk",
    "version": "0.1.0",
    "buildNumber": "1",
    "startupFormId": "form-main",
    "shutdownMode": "OnStartupFormClosed"
  },
  "forms": [
    {
      "id": "form-main",
      "typeName": "frmLibrary",
      "layout": "Forms/frmLibrary.form.json",
      "code": "Forms/frmLibrary.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#

frmLibrary

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"LibraryDesk - Open Library search"
Width900
Height620
MinWidth780
MinHeight560
TypeNameParent
LabellblQueryForm
TextBoxtxtQueryForm
CommandButtoncmdSearchForm
CommandButtoncmdClearForm
LabellblTitleForm
TextBoxtxtTitleForm
LabellblAuthorForm
TextBoxtxtAuthorForm
LabellblYearForm
TextBoxtxtYearForm
CommandButtoncmdSaveForm
CommandButtoncmdExportForm
LabellblStatusForm
DataGridgrdBooksForm
lblQuery · exact Inspector values
PropertyValue
Left24
Top24
Width70
Height30
Caption"Search"
Anchor edgesLeft, Top
txtQuery · exact Inspector values
PropertyValue
Left104
Top20
Width340
Height36
Text"The Hobbit"
TabIndex0
Anchor edgesLeft, Top
cmdSearch · exact Inspector values
PropertyValue
Left458
Top20
Width120
Height36
Caption"Search"
TabIndex1
Anchor edgesLeft, Top
cmdClear · exact Inspector values
PropertyValue
Left592
Top20
Width100
Height36
Caption"Clear"
TabIndex2
Anchor edgesLeft, Top
lblTitle · exact Inspector values
PropertyValue
Left24
Top78
Width70
Height30
Caption"Title"
Anchor edgesLeft, Top
txtTitle · exact Inspector values
PropertyValue
Left104
Top74
Width340
Height36
Text""
ReadOnlyTrue
TabIndex3
Anchor edgesLeft, Top
lblAuthor · exact Inspector values
PropertyValue
Left458
Top78
Width70
Height30
Caption"Author"
Anchor edgesLeft, Top
txtAuthor · exact Inspector values
PropertyValue
Left538
Top74
Width300
Height36
Text""
ReadOnlyTrue
TabIndex4
Anchor edgesLeft, Top
lblYear · exact Inspector values
PropertyValue
Left24
Top126
Width70
Height30
Caption"Year"
Anchor edgesLeft, Top
txtYear · exact Inspector values
PropertyValue
Left104
Top122
Width120
Height36
Text""
ReadOnlyTrue
TabIndex5
Anchor edgesLeft, Top
cmdSave · exact Inspector values
PropertyValue
Left244
Top122
Width130
Height36
Caption"Save book"
TabIndex6
Anchor edgesLeft, Top
cmdExport · exact Inspector values
PropertyValue
Left388
Top122
Width130
Height36
Caption"Export CSV"
TabIndex7
Anchor edgesLeft, Top
lblStatus · exact Inspector values
PropertyValue
Left24
Top176
Width820
Height36
Caption"Opening the local library..."
Anchor edgesLeft, Top, Right
grdBooks · exact Inspector values
PropertyValue
Left24
Top224
Width820
Height350
TabIndex8
Anchor edgesLeft, Top, Right, Bottom

Step 3Connect the events explicitly#

frmLibrary

ObjectEventProcedureParameters
cmdSearchClickcmdSearch_ClickClick()
cmdClearClickcmdClear_ClickClick()
cmdSaveClickcmdSave_ClickClick()
cmdExportClickcmdExport_ClickClick()
FormShownForm_Shown()
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/frmLibrary.bas

Enter these declarations at file scope, before the procedures.

Forms/frmLibrary.bas · declarations
Option Explicit

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

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

Forms/frmLibrary.bas · Form_Shown
Private Async Sub Form_Shown()
    Dim version As Long

    mBusy = True
    cmdSearch.Enabled = False
    cmdSave.Enabled = False
    cmdExport.Enabled = False
    lblStatus.Caption = "Opening the local library..."

    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("librarydesk.db"))
        version = Await mDb.ApplyMigrationsAsync(App.Migrations)
        Await RefreshBooks()
        cmdSearch.Enabled = True
        cmdSave.Enabled = True
        cmdExport.Enabled = True
        lblStatus.Caption = CStr(mRows.RowCount) & " saved book(s). Search Open Library to add one."
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub
Private Async Function RefreshBooks() As Boolean

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

Forms/frmLibrary.bas · RefreshBooks
Private Async Function RefreshBooks() As Boolean
    Dim command As DbCommand

    Set command = mDb.CreateCommand("SELECT id, title, author, first_publish_year, saved_at FROM books ORDER BY title, id")
    Set mRows = Await command.QueryAsync()
    Set grdBooks.DataSource = mRows.Bind()
    RefreshBooks = True
End Function
Private Async Sub cmdSearch_Click()

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

Forms/frmLibrary.bas · cmdSearch_Click
Private Async Sub cmdSearch_Click()
    Dim query As String
    Dim url As String
    Dim envelopeText As String
    Dim bodyText As String
    Dim envelope As JsonValue
    Dim body As JsonValue
    Dim documents As JsonValue
    Dim firstBook As JsonValue
    Dim authors As JsonValue
    Dim title As String
    Dim author As String
    Dim firstYear As String
    Dim count As Long

    If mBusy Then Exit Sub
    query = Trim$(txtQuery.Text)
    If Len(query) = 0 Then
        lblStatus.Caption = "Enter a title or author to search."
        txtQuery.SetFocus
        Exit Sub
    End If

    mBusy = True
    cmdSearch.Enabled = False
    lblStatus.Caption = "Searching Open Library..."
    Try
        url = "https://openlibrary.org/search.json?q=" & Replace(query, " ", "+") & "&limit=5&fields=key,title,author_name,first_publish_year"
        envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic Library Desk (replace-with-your-email@example.com)""}", "", 10000)
        envelope = Json.Parse(envelopeText)
        If CLng(Json.Value(Json.Get(envelope, "status"))) < 200 Or CLng(Json.Value(Json.Get(envelope, "status"))) >= 300 Then
            Err.Raise 513, "Web request", "The service returned HTTP " & CStr(Json.Value(Json.Get(envelope, "status")))
        End If
        bodyText = CStr(Json.Value(Json.Get(envelope, "body")))
        body = Json.Parse(bodyText)
        documents = Json.Get(body, "docs")
        count = Json.Count(documents)

        If count = 0 Then
            lblStatus.Caption = "No books matched the search."
            Exit Sub
        End If

        firstBook = Json.At(documents, 0)
        title = CStr(Json.Value(Json.Get(firstBook, "title")))
        author = "Unknown author"
        If Json.Has(firstBook, "author_name") Then
            authors = Json.Get(firstBook, "author_name")
            If Json.Count(authors) > 0 Then author = CStr(Json.Value(Json.At(authors, 0)))
        End If
        firstYear = ""
        If Json.Has(firstBook, "first_publish_year") Then
            firstYear = CStr(Json.Value(Json.Get(firstBook, "first_publish_year")))
        End If

        txtTitle.Text = title
        txtAuthor.Text = author
        txtYear.Text = firstYear
        lblStatus.Caption = CStr(count) & " result(s). The first result is ready to save."
    Catch failure As McException
        lblStatus.Caption = "Search error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSearch.Enabled = True
    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/frmLibrary.bas · cmdSave_Click
Private Async Sub cmdSave_Click()
    Dim command As DbCommand

    If mBusy Then Exit Sub
    If Len(Trim$(txtTitle.Text)) = 0 Then
        lblStatus.Caption = "Search for a book before saving."
        Exit Sub
    End If
    If Len(Trim$(txtAuthor.Text)) = 0 Then txtAuthor.Text = "Unknown author"

    mBusy = True
    cmdSave.Enabled = False
    Try
        Set command = mDb.CreateCommand("INSERT INTO books(title, author, first_publish_year) VALUES(@title, @author, @year)")
        command.AddParameter "@title", Trim$(txtTitle.Text)
        command.AddParameter "@author", Trim$(txtAuthor.Text)
        If Len(Trim$(txtYear.Text)) = 0 Then
            command.AddParameter "@year", Null
        Else
            command.AddParameter "@year", CLng(Val(txtYear.Text))
        End If
        Await command.ExecuteAsync()
        Await RefreshBooks()
        lblStatus.Caption = "Book saved locally."
    Catch failure As McException
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSave.Enabled = True
    End Try
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/frmLibrary.bas · cmdExport_Click
Private Async Sub cmdExport_Click()
    Dim command As DbCommand
    Dim rows As Recordset

    If mBusy Then Exit Sub
    mBusy = True
    cmdExport.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT title, author, first_publish_year, saved_at FROM books ORDER BY title, id")
        Set rows = Await command.QueryAsync()
        Await rows.ExportCsvAsync(AppPaths.DataFile("librarydesk.csv"))
        lblStatus.Caption = CStr(rows.RowCount) & " book(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 cmdClear_Click()

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

Forms/frmLibrary.bas · cmdClear_Click
Private Sub cmdClear_Click()
    If mBusy Then Exit Sub
    txtQuery.Text = ""
    txtTitle.Text = ""
    txtAuthor.Text = ""
    txtYear.Text = ""
    lblStatus.Caption = "Ready for another search."
    txtQuery.SetFocus
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/frmLibrary.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/frmLibrary.bas file

Replace the file once; do not append duplicate procedures.

Forms/frmLibrary.bas
Option Explicit

Private mDb As Database
Private mRows As Recordset
Private mBusy As Boolean

Private Async Sub Form_Shown()
    Dim version As Long

    mBusy = True
    cmdSearch.Enabled = False
    cmdSave.Enabled = False
    cmdExport.Enabled = False
    lblStatus.Caption = "Opening the local library..."

    Try
        Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("librarydesk.db"))
        version = Await mDb.ApplyMigrationsAsync(App.Migrations)
        Await RefreshBooks()
        cmdSearch.Enabled = True
        cmdSave.Enabled = True
        cmdExport.Enabled = True
        lblStatus.Caption = CStr(mRows.RowCount) & " saved book(s). Search Open Library to add one."
    Catch failure As McException
        lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
    End Try
End Sub

Private Async Function RefreshBooks() As Boolean
    Dim command As DbCommand

    Set command = mDb.CreateCommand("SELECT id, title, author, first_publish_year, saved_at FROM books ORDER BY title, id")
    Set mRows = Await command.QueryAsync()
    Set grdBooks.DataSource = mRows.Bind()
    RefreshBooks = True
End Function

Private Async Sub cmdSearch_Click()
    Dim query As String
    Dim url As String
    Dim envelopeText As String
    Dim bodyText As String
    Dim envelope As JsonValue
    Dim body As JsonValue
    Dim documents As JsonValue
    Dim firstBook As JsonValue
    Dim authors As JsonValue
    Dim title As String
    Dim author As String
    Dim firstYear As String
    Dim count As Long

    If mBusy Then Exit Sub
    query = Trim$(txtQuery.Text)
    If Len(query) = 0 Then
        lblStatus.Caption = "Enter a title or author to search."
        txtQuery.SetFocus
        Exit Sub
    End If

    mBusy = True
    cmdSearch.Enabled = False
    lblStatus.Caption = "Searching Open Library..."
    Try
        url = "https://openlibrary.org/search.json?q=" & Replace(query, " ", "+") & "&limit=5&fields=key,title,author_name,first_publish_year"
        envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic Library Desk (replace-with-your-email@example.com)""}", "", 10000)
        envelope = Json.Parse(envelopeText)
        If CLng(Json.Value(Json.Get(envelope, "status"))) < 200 Or CLng(Json.Value(Json.Get(envelope, "status"))) >= 300 Then
            Err.Raise 513, "Web request", "The service returned HTTP " & CStr(Json.Value(Json.Get(envelope, "status")))
        End If
        bodyText = CStr(Json.Value(Json.Get(envelope, "body")))
        body = Json.Parse(bodyText)
        documents = Json.Get(body, "docs")
        count = Json.Count(documents)

        If count = 0 Then
            lblStatus.Caption = "No books matched the search."
            Exit Sub
        End If

        firstBook = Json.At(documents, 0)
        title = CStr(Json.Value(Json.Get(firstBook, "title")))
        author = "Unknown author"
        If Json.Has(firstBook, "author_name") Then
            authors = Json.Get(firstBook, "author_name")
            If Json.Count(authors) > 0 Then author = CStr(Json.Value(Json.At(authors, 0)))
        End If
        firstYear = ""
        If Json.Has(firstBook, "first_publish_year") Then
            firstYear = CStr(Json.Value(Json.Get(firstBook, "first_publish_year")))
        End If

        txtTitle.Text = title
        txtAuthor.Text = author
        txtYear.Text = firstYear
        lblStatus.Caption = CStr(count) & " result(s). The first result is ready to save."
    Catch failure As McException
        lblStatus.Caption = "Search error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSearch.Enabled = True
    End Try
End Sub

Private Async Sub cmdSave_Click()
    Dim command As DbCommand

    If mBusy Then Exit Sub
    If Len(Trim$(txtTitle.Text)) = 0 Then
        lblStatus.Caption = "Search for a book before saving."
        Exit Sub
    End If
    If Len(Trim$(txtAuthor.Text)) = 0 Then txtAuthor.Text = "Unknown author"

    mBusy = True
    cmdSave.Enabled = False
    Try
        Set command = mDb.CreateCommand("INSERT INTO books(title, author, first_publish_year) VALUES(@title, @author, @year)")
        command.AddParameter "@title", Trim$(txtTitle.Text)
        command.AddParameter "@author", Trim$(txtAuthor.Text)
        If Len(Trim$(txtYear.Text)) = 0 Then
            command.AddParameter "@year", Null
        Else
            command.AddParameter "@year", CLng(Val(txtYear.Text))
        End If
        Await command.ExecuteAsync()
        Await RefreshBooks()
        lblStatus.Caption = "Book saved locally."
    Catch failure As McException
        lblStatus.Caption = "Save error " & CStr(failure.Number) & ": " & failure.Message
    Finally
        mBusy = False
        cmdSave.Enabled = True
    End Try
End Sub

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

    If mBusy Then Exit Sub
    mBusy = True
    cmdExport.Enabled = False
    Try
        Set command = mDb.CreateCommand("SELECT title, author, first_publish_year, saved_at FROM books ORDER BY title, id")
        Set rows = Await command.QueryAsync()
        Await rows.ExportCsvAsync(AppPaths.DataFile("librarydesk.csv"))
        lblStatus.Caption = CStr(rows.RowCount) & " book(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 cmdClear_Click()
    If mBusy Then Exit Sub
    txtQuery.Text = ""
    txtTitle.Text = ""
    txtAuthor.Text = ""
    txtYear.Text = ""
    lblStatus.Caption = "Ready for another search."
    txtQuery.SetFocus
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 books (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    author TEXT NOT NULL,
    first_publish_year INTEGER NULL,
    openlibrary_key TEXT NULL,
    saved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

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 LibraryDesk.mcproj
mcbasic run LibraryDesk.mcproj

Acceptance checklist

  • Launch with the migration file present and inspect the initial local list.
  • Search for a title with network access available.
  • Save a result, restart and verify persistence.
  • Try an empty query and an unavailable network.
  • Export the saved collection and inspect the report.

Step 7Make one change and repeat the tests#

Add a duplicate-detection rule based on a stable book identifier.

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