Build a library with local data and web search
Connect search, JSON parsing, SQLite storage and CSV export.
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
{
"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 property | Value |
|---|---|
Caption | "LibraryDesk - Open Library search" |
Width | 900 |
Height | 620 |
MinWidth | 780 |
MinHeight | 560 |
| Type | Name | Parent |
|---|---|---|
| Label | lblQuery | Form |
| TextBox | txtQuery | Form |
| CommandButton | cmdSearch | Form |
| CommandButton | cmdClear | Form |
| Label | lblTitle | Form |
| TextBox | txtTitle | Form |
| Label | lblAuthor | Form |
| TextBox | txtAuthor | Form |
| Label | lblYear | Form |
| TextBox | txtYear | Form |
| CommandButton | cmdSave | Form |
| CommandButton | cmdExport | Form |
| Label | lblStatus | Form |
| DataGrid | grdBooks | Form |
lblQuery · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 24 |
Width | 70 |
Height | 30 |
Caption | "Search" |
| Anchor edges | Left, Top |
txtQuery · exact Inspector values
| Property | Value |
|---|---|
Left | 104 |
Top | 20 |
Width | 340 |
Height | 36 |
Text | "The Hobbit" |
TabIndex | 0 |
| Anchor edges | Left, Top |
cmdSearch · exact Inspector values
| Property | Value |
|---|---|
Left | 458 |
Top | 20 |
Width | 120 |
Height | 36 |
Caption | "Search" |
TabIndex | 1 |
| Anchor edges | Left, Top |
cmdClear · exact Inspector values
| Property | Value |
|---|---|
Left | 592 |
Top | 20 |
Width | 100 |
Height | 36 |
Caption | "Clear" |
TabIndex | 2 |
| Anchor edges | Left, Top |
lblTitle · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 78 |
Width | 70 |
Height | 30 |
Caption | "Title" |
| Anchor edges | Left, Top |
txtTitle · exact Inspector values
| Property | Value |
|---|---|
Left | 104 |
Top | 74 |
Width | 340 |
Height | 36 |
Text | "" |
ReadOnly | True |
TabIndex | 3 |
| Anchor edges | Left, Top |
lblAuthor · exact Inspector values
| Property | Value |
|---|---|
Left | 458 |
Top | 78 |
Width | 70 |
Height | 30 |
Caption | "Author" |
| Anchor edges | Left, Top |
txtAuthor · exact Inspector values
| Property | Value |
|---|---|
Left | 538 |
Top | 74 |
Width | 300 |
Height | 36 |
Text | "" |
ReadOnly | True |
TabIndex | 4 |
| Anchor edges | Left, Top |
lblYear · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 126 |
Width | 70 |
Height | 30 |
Caption | "Year" |
| Anchor edges | Left, Top |
txtYear · exact Inspector values
| Property | Value |
|---|---|
Left | 104 |
Top | 122 |
Width | 120 |
Height | 36 |
Text | "" |
ReadOnly | True |
TabIndex | 5 |
| Anchor edges | Left, Top |
cmdSave · exact Inspector values
| Property | Value |
|---|---|
Left | 244 |
Top | 122 |
Width | 130 |
Height | 36 |
Caption | "Save book" |
TabIndex | 6 |
| Anchor edges | Left, Top |
cmdExport · exact Inspector values
| Property | Value |
|---|---|
Left | 388 |
Top | 122 |
Width | 130 |
Height | 36 |
Caption | "Export CSV" |
TabIndex | 7 |
| Anchor edges | Left, Top |
lblStatus · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 176 |
Width | 820 |
Height | 36 |
Caption | "Opening the local library..." |
| Anchor edges | Left, Top, Right |
grdBooks · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 224 |
Width | 820 |
Height | 350 |
TabIndex | 8 |
| Anchor edges | Left, Top, Right, Bottom |
Step 3Connect the events explicitly#
frmLibrary
| Object | Event | Procedure | Parameters |
|---|---|---|---|
cmdSearch | Click | cmdSearch_Click | Click() |
cmdClear | Click | cmdClear_Click | Click() |
cmdSave | Click | cmdSave_Click | Click() |
cmdExport | Click | cmdExport_Click | Click() |
| Form | Shown | Form_Shown | () |
| 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/frmLibrary.bas
Enter these declarations at file scope, before the procedures.
Option Explicit
Private mDb As Database
Private mRows As Recordset
Private mBusy 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
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 SubPrivate 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.
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 FunctionPrivate Async Sub cmdSearch_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
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 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
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 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
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 SubPrivate Sub cmdClear_Click()
Enter the complete action, including its input checks and calls to helper procedures.
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 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/frmLibrary.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 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 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 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:
mcbasic check LibraryDesk.mcproj
mcbasic run LibraryDesk.mcprojAcceptance 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.
20f4bfed6b · Build manifest