Build a weather and saved-location workspace
Combine geocoding, current weather, SQLite migrations, saved-location CRUD and CSV export.
Open the database and apply App.Migrations before RefreshLocations. Geocoding produces a selected place; fetching weather is a second request. Saving, updating and deleting use parameterized database commands and refresh the grid afterward. The complete handlers are included below, including busy guards, selection state and Finally blocks. External Open-Meteo calls require connectivity and compliance with that service’s terms; the sample is a learning project, not a service availability guarantee.
Step 1Prepare a safe practice project#
Extract the archive into a new writable folder, separate from your own application. Open WeatherDesk.mcbproj 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": "f319a0d2-6f8c-4f85-8b89-27e2c2a7f6a1",
"name": "WeatherDesk",
"kind": "DesktopApp",
"ai": {
"storeChatsInProject": false
},
"language": {
"dialect": "McBasic1",
"requireExplicit": true,
"compare": "Binary",
"defaultArrayBase": 0
},
"application": {
"bundleIdentifier": "com.mcbasic.samples.weatherdesk",
"version": "0.1.0",
"buildNumber": "1",
"startupFormId": "form-main",
"shutdownMode": "OnStartupFormClosed"
},
"forms": [
{
"id": "form-main",
"typeName": "frmWeather",
"layout": "Forms/frmWeather.form.json",
"code": "Forms/frmWeather.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#
frmWeather
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 | "WeatherDesk - Open-Meteo forecast" |
Width | 1060 |
Height | 720 |
MinWidth | 900 |
MinHeight | 600 |
| Type | Name | Parent |
|---|---|---|
| Label | lblCity | Form |
| TextBox | txtCity | Form |
| CommandButton | cmdFind | Form |
| CommandButton | cmdClear | Form |
| Label | lblSelected | Form |
| TextBox | txtSelected | Form |
| CommandButton | cmdSave | Form |
| CommandButton | cmdRefresh | Form |
| Label | lblLabel | Form |
| TextBox | txtLabel | Form |
| CheckBox | chkTracked | Form |
| CommandButton | cmdUpdate | Form |
| CommandButton | cmdDelete | Form |
| Label | lblCurrent | Form |
| Label | lblStatus | Form |
| DataGrid | grdLocations | Form |
| CommandButton | cmdExport | Form |
lblCity · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 24 |
Width | 80 |
Height | 30 |
Caption | "City" |
| Anchor edges | Left, Top |
txtCity · exact Inspector values
| Property | Value |
|---|---|
Left | 112 |
Top | 20 |
Width | 300 |
Height | 36 |
Text | "Singapore" |
TabIndex | 0 |
| Anchor edges | Left, Top |
cmdFind · exact Inspector values
| Property | Value |
|---|---|
Left | 428 |
Top | 20 |
Width | 120 |
Height | 36 |
Caption | "Find city" |
TabIndex | 1 |
| Anchor edges | Left, Top |
cmdClear · exact Inspector values
| Property | Value |
|---|---|
Left | 562 |
Top | 20 |
Width | 100 |
Height | 36 |
Caption | "Clear" |
TabIndex | 2 |
| Anchor edges | Left, Top |
lblSelected · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 78 |
Width | 110 |
Height | 30 |
Caption | "Selected place" |
| Anchor edges | Left, Top |
txtSelected · exact Inspector values
| Property | Value |
|---|---|
Left | 144 |
Top | 74 |
Width | 320 |
Height | 36 |
ReadOnly | True |
TabIndex | 3 |
| Anchor edges | Left, Top |
cmdSave · exact Inspector values
| Property | Value |
|---|---|
Left | 484 |
Top | 74 |
Width | 132 |
Height | 36 |
Caption | "Add location" |
TabIndex | 4 |
| Anchor edges | Left, Top |
cmdRefresh · exact Inspector values
| Property | Value |
|---|---|
Left | 630 |
Top | 74 |
Width | 130 |
Height | 36 |
Caption | "Refresh list" |
TabIndex | 5 |
| Anchor edges | Left, Top |
lblLabel · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 126 |
Width | 80 |
Height | 30 |
Caption | "Label" |
| Anchor edges | Left, Top |
txtLabel · exact Inspector values
| Property | Value |
|---|---|
Left | 112 |
Top | 122 |
Width | 280 |
Height | 36 |
Text | "" |
TabIndex | 6 |
| Anchor edges | Left, Top |
chkTracked · exact Inspector values
| Property | Value |
|---|---|
Left | 412 |
Top | 124 |
Width | 180 |
Height | 32 |
Caption | "Track this location" |
Checked | True |
TabIndex | 7 |
| Anchor edges | Left, Top |
cmdUpdate · exact Inspector values
| Property | Value |
|---|---|
Left | 612 |
Top | 122 |
Width | 120 |
Height | 36 |
Caption | "Update" |
TabIndex | 8 |
| Anchor edges | Left, Top |
cmdDelete · exact Inspector values
| Property | Value |
|---|---|
Left | 744 |
Top | 122 |
Width | 120 |
Height | 36 |
Caption | "Delete" |
TabIndex | 9 |
| Anchor edges | Left, Top |
lblCurrent · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 174 |
Width | 980 |
Height | 34 |
Caption | "Find a city to load its current weather." |
| Anchor edges | Left, Top, Right |
lblStatus · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 210 |
Width | 980 |
Height | 36 |
Caption | "Opening the local weather database..." |
| Anchor edges | Left, Top, Right |
grdLocations · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 260 |
Width | 980 |
Height | 360 |
TabIndex | 10 |
EmptyText | "No saved locations yet." |
| Anchor edges | Left, Top, Right, Bottom |
cmdExport · exact Inspector values
| Property | Value |
|---|---|
Left | 24 |
Top | 638 |
Width | 130 |
Height | 36 |
Caption | "Export CSV" |
TabIndex | 11 |
| Anchor edges | Left, Bottom |
Step 3Connect the events explicitly#
frmWeather
| Object | Event | Procedure | Parameters |
|---|---|---|---|
cmdFind | Click | cmdFind_Click | Click() |
cmdClear | Click | cmdClear_Click | Click() |
cmdSave | Click | cmdSave_Click | Click() |
cmdRefresh | Click | cmdRefresh_Click | Click() |
cmdUpdate | Click | cmdUpdate_Click | Click() |
cmdDelete | Click | cmdDelete_Click | Click() |
grdLocations | SelectionChanged | grdLocations_SelectionChanged | SelectionChanged() |
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/frmWeather.bas
Enter these declarations at file scope, before the procedures.
Option Explicit
Private mDb As Database
Private mRows As BindingSource
Private mBusy As Boolean
Private mSelectedId As Long
Private mSelectedRow As Long
Private mDeleteArmed As Boolean
Private mSelectedName As String
Private mSelectedCountry As String
Private mSelectedLatitude As Double
Private mSelectedLongitude As Double
Private mSelectedTimezone As String
Private mTemperature As Double
Private mWindSpeed As Double
Private mWeatherCode As LongPrivate 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
cmdFind.Enabled = False
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
cmdExport.Enabled = False
lblStatus.Caption = "Opening the local weather database..."
Try
Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("weatherdesk.db"))
version = Await mDb.ApplyMigrationsAsync(App.Migrations)
Await RefreshLocations()
cmdFind.Enabled = True
cmdSave.Enabled = True
cmdRefresh.Enabled = True
cmdExport.Enabled = True
lblStatus.Caption = CStr(mRows.RowCount) & " saved location(s)."
Catch failure As McException
lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
End Try
End SubPrivate Async Function RefreshLocations() 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 RefreshLocations() As Boolean
Dim command As DbCommand
Dim rows As Recordset
Set command = mDb.CreateCommand("SELECT id, label, name, country, latitude, longitude, timezone, tracked, saved_at, updated_at FROM saved_locations ORDER BY label, name, country")
Set rows = Await command.QueryAsync()
Set mRows = rows.Bind()
Set grdLocations.DataSource = mRows
grdLocations.SelectedIndex = -1
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
txtLabel.Text = ""
chkTracked.Checked = True
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
RefreshLocations = True
End FunctionPrivate Async Sub cmdFind_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdFind_Click()
Dim query As String
Dim url As String
Dim envelopeText As String
Dim bodyText As String
Dim envelope As JsonValue
Dim document As JsonValue
Dim results As JsonValue
Dim firstResult As JsonValue
Dim count As Long
If mBusy Then Exit Sub
query = Trim$(txtCity.Text)
If Len(query) = 0 Then
lblStatus.Caption = "Enter a city name."
txtCity.SetFocus
Exit Sub
End If
mBusy = True
cmdFind.Enabled = False
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
lblStatus.Caption = "Finding the city..."
Try
url = "https://geocoding-api.open-meteo.com/v1/search?name="
url = url & Replace(query, " ", "+") & "&count=5&language=en&format=json"
envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic WeatherDesk""}", "", 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")))
document = Json.Parse(bodyText)
If Not Json.Has(document, "results") Then
lblStatus.Caption = "The location service returned no result list."
Exit Sub
End If
results = Json.Get(document, "results")
count = Json.Count(results)
If count = 0 Then
lblStatus.Caption = "No city matched the search."
Exit Sub
End If
firstResult = Json.At(results, 0)
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
mSelectedName = CStr(Json.Value(Json.Get(firstResult, "name")))
mSelectedCountry = CStr(Json.Value(Json.Get(firstResult, "country")))
mSelectedLatitude = CDbl(Json.Value(Json.Get(firstResult, "latitude")))
mSelectedLongitude = CDbl(Json.Value(Json.Get(firstResult, "longitude")))
mSelectedTimezone = ""
If Json.Has(firstResult, "timezone") Then mSelectedTimezone = CStr(Json.Value(Json.Get(firstResult, "timezone")))
txtSelected.Text = mSelectedName & ", " & mSelectedCountry
txtLabel.Text = mSelectedName
chkTracked.Checked = True
Await LoadCurrentWeather()
cmdSave.Enabled = True
lblStatus.Caption = CStr(count) & " location(s) found. The first result is ready to add."
Catch failure As McException
lblStatus.Caption = "API error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdFind.Enabled = True
End Try
End SubPrivate Async Function LoadCurrentWeather() 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 LoadCurrentWeather() As Boolean
Dim url As String
Dim envelopeText As String
Dim bodyText As String
Dim envelope As JsonValue
Dim document As JsonValue
Dim current As JsonValue
url = "https://api.open-meteo.com/v1/forecast?latitude="
url = url & CStr(mSelectedLatitude) & "&longitude=" & CStr(mSelectedLongitude)
url = url & "¤t=temperature_2m,wind_speed_10m,weather_code"
url = url & "&timezone=auto"
envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic WeatherDesk""}", "", 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")))
document = Json.Parse(bodyText)
If Not Json.Has(document, "current") Then
lblCurrent.Caption = "The forecast response has no current section."
LoadCurrentWeather = False
Exit Function
End If
current = Json.Get(document, "current")
mTemperature = CDbl(Json.Value(Json.Get(current, "temperature_2m")))
mWindSpeed = CDbl(Json.Value(Json.Get(current, "wind_speed_10m")))
mWeatherCode = CLng(Json.Value(Json.Get(current, "weather_code")))
lblCurrent.Caption = "Current: " & CStr(mTemperature) & " °C | Wind: " & CStr(mWindSpeed) & " km/h | Weather code: " & CStr(mWeatherCode)
LoadCurrentWeather = True
End FunctionPrivate 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 lookup As DbCommand
Dim saved As Recordset
Dim locationId As Long
Dim tracked As Long
If mBusy Then Exit Sub
If Len(Trim$(mSelectedName)) = 0 Then
lblStatus.Caption = "Find a city before adding it."
Exit Sub
End If
If Len(Trim$(txtLabel.Text)) = 0 Then
lblStatus.Caption = "Enter a label for the location."
txtLabel.SetFocus
Exit Sub
End If
tracked = 0
If chkTracked.Checked Then tracked = 1
mBusy = True
cmdSave.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("INSERT INTO saved_locations(label, name, country, latitude, longitude, timezone, tracked) VALUES(@label, @name, @country, @latitude, @longitude, @timezone, @tracked)")
command.AddParameter "@label", Trim$(txtLabel.Text)
command.AddParameter "@name", mSelectedName
command.AddParameter "@country", mSelectedCountry
command.AddParameter "@latitude", mSelectedLatitude
command.AddParameter "@longitude", mSelectedLongitude
command.AddParameter "@timezone", mSelectedTimezone
command.AddParameter "@tracked", tracked
Await command.ExecuteAsync()
Set lookup = mDb.CreateCommand("SELECT id FROM saved_locations WHERE name=@name AND country=@country")
lookup.AddParameter "@name", mSelectedName
lookup.AddParameter "@country", mSelectedCountry
Set saved = Await lookup.QueryAsync()
locationId = CLng(saved.Value(0, "id"))
Set command = mDb.CreateCommand("INSERT INTO forecast_snapshots(location_id, temperature, wind_speed, weather_code) VALUES(@location_id, @temperature, @wind_speed, @weather_code)")
command.AddParameter "@location_id", locationId
command.AddParameter "@temperature", mTemperature
command.AddParameter "@wind_speed", mWindSpeed
command.AddParameter "@weather_code", mWeatherCode
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location added and weather snapshot saved in SQLite."
Catch failure As McException
lblStatus.Caption = "Add error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
End Try
End SubPrivate Async Sub cmdUpdate_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdUpdate_Click()
Dim command As DbCommand
Dim tracked As Long
If mBusy Then Exit Sub
If mSelectedId = 0 Then
lblStatus.Caption = "Select a saved location first."
Exit Sub
End If
If Len(Trim$(txtLabel.Text)) = 0 Then
lblStatus.Caption = "Enter a label for the location."
txtLabel.SetFocus
Exit Sub
End If
tracked = 0
If chkTracked.Checked Then tracked = 1
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("UPDATE saved_locations SET label=@label, tracked=@tracked, updated_at=CURRENT_TIMESTAMP WHERE id=@id")
command.AddParameter "@label", Trim$(txtLabel.Text)
command.AddParameter "@tracked", tracked
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location updated."
Catch failure As McException
lblStatus.Caption = "Update error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
End Try
End SubPrivate Async Sub cmdDelete_Click()
Follow the success, failure and cleanup paths together. Do not omit a guard or Finally block when entering this procedure.
Private Async Sub cmdDelete_Click()
Dim command As DbCommand
If mBusy Then Exit Sub
If mSelectedId = 0 Then
lblStatus.Caption = "Select a saved location first."
Exit Sub
End If
If Not mDeleteArmed Then
mDeleteArmed = True
lblStatus.Caption = "Click Delete again to confirm removal of the selected location."
Exit Sub
End If
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("DELETE FROM forecast_snapshots WHERE location_id=@id")
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Set command = mDb.CreateCommand("DELETE FROM saved_locations WHERE id=@id")
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location and its weather snapshots deleted."
Catch failure As McException
lblStatus.Caption = "Delete error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
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()
If mBusy Then Exit Sub
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Await RefreshLocations()
lblStatus.Caption = CStr(mRows.RowCount) & " saved location(s) loaded from SQLite."
Catch failure As McException
lblStatus.Caption = "Refresh error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.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 label, name, country, latitude, longitude, timezone, tracked, saved_at, updated_at FROM saved_locations ORDER BY label, name, country")
Set rows = Await command.QueryAsync()
Await rows.ExportCsvAsync(AppPaths.DataFile("weatherdesk-locations.csv"))
lblStatus.Caption = CStr(rows.RowCount) & " location(s) exported."
Catch failure As McException
lblStatus.Caption = "Export error " & CStr(failure.Number) & ": " & CStr(failure.Message)
Finally
mBusy = False
cmdExport.Enabled = True
End Try
End SubPrivate Sub grdLocations_SelectionChanged()
Read the current value or selection and update its dependent interface. The binding above determines which object calls this handler.
Private Sub grdLocations_SelectionChanged()
Dim row As Long
If mRows Is Nothing Then Exit Sub
row = CInt(grdLocations.SelectedIndex)
If row < 0 Or row >= mRows.RowCount Then Exit Sub
mSelectedId = CLng(mRows.Value(row, "id"))
mSelectedRow = row
mSelectedName = CStr(mRows.Value(row, "name"))
mSelectedCountry = CStr(mRows.Value(row, "country"))
mSelectedLatitude = CDbl(mRows.Value(row, "latitude"))
mSelectedLongitude = CDbl(mRows.Value(row, "longitude"))
If IsNull(mRows.Value(row, "timezone")) Then
mSelectedTimezone = ""
Else
mSelectedTimezone = CStr(mRows.Value(row, "timezone"))
End If
txtSelected.Text = mSelectedName & ", " & mSelectedCountry
txtLabel.Text = CStr(mRows.Value(row, "label"))
chkTracked.Checked = (CInt(mRows.Value(row, "tracked")) <> 0)
mDeleteArmed = False
cmdUpdate.Enabled = True
cmdDelete.Enabled = True
lblStatus.Caption = "Editing location " & CStr(mSelectedId) & "."
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
txtCity.Text = ""
txtSelected.Text = ""
txtLabel.Text = ""
chkTracked.Checked = True
lblCurrent.Caption = "Find a city to load its current weather."
lblStatus.Caption = "Ready for another search."
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
mSelectedName = ""
mSelectedCountry = ""
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
txtCity.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/frmWeather.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 mSelectedId As Long
Private mSelectedRow As Long
Private mDeleteArmed As Boolean
Private mSelectedName As String
Private mSelectedCountry As String
Private mSelectedLatitude As Double
Private mSelectedLongitude As Double
Private mSelectedTimezone As String
Private mTemperature As Double
Private mWindSpeed As Double
Private mWeatherCode As Long
Private Async Sub Form_Shown()
Dim version As Long
mBusy = True
cmdFind.Enabled = False
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
cmdExport.Enabled = False
lblStatus.Caption = "Opening the local weather database..."
Try
Set mDb = Await Sqlite.OpenAsync(AppPaths.DataFile("weatherdesk.db"))
version = Await mDb.ApplyMigrationsAsync(App.Migrations)
Await RefreshLocations()
cmdFind.Enabled = True
cmdSave.Enabled = True
cmdRefresh.Enabled = True
cmdExport.Enabled = True
lblStatus.Caption = CStr(mRows.RowCount) & " saved location(s)."
Catch failure As McException
lblStatus.Caption = "Database error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
End Try
End Sub
Private Async Function RefreshLocations() As Boolean
Dim command As DbCommand
Dim rows As Recordset
Set command = mDb.CreateCommand("SELECT id, label, name, country, latitude, longitude, timezone, tracked, saved_at, updated_at FROM saved_locations ORDER BY label, name, country")
Set rows = Await command.QueryAsync()
Set mRows = rows.Bind()
Set grdLocations.DataSource = mRows
grdLocations.SelectedIndex = -1
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
txtLabel.Text = ""
chkTracked.Checked = True
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
RefreshLocations = True
End Function
Private Async Sub cmdFind_Click()
Dim query As String
Dim url As String
Dim envelopeText As String
Dim bodyText As String
Dim envelope As JsonValue
Dim document As JsonValue
Dim results As JsonValue
Dim firstResult As JsonValue
Dim count As Long
If mBusy Then Exit Sub
query = Trim$(txtCity.Text)
If Len(query) = 0 Then
lblStatus.Caption = "Enter a city name."
txtCity.SetFocus
Exit Sub
End If
mBusy = True
cmdFind.Enabled = False
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
lblStatus.Caption = "Finding the city..."
Try
url = "https://geocoding-api.open-meteo.com/v1/search?name="
url = url & Replace(query, " ", "+") & "&count=5&language=en&format=json"
envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic WeatherDesk""}", "", 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")))
document = Json.Parse(bodyText)
If Not Json.Has(document, "results") Then
lblStatus.Caption = "The location service returned no result list."
Exit Sub
End If
results = Json.Get(document, "results")
count = Json.Count(results)
If count = 0 Then
lblStatus.Caption = "No city matched the search."
Exit Sub
End If
firstResult = Json.At(results, 0)
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
mSelectedName = CStr(Json.Value(Json.Get(firstResult, "name")))
mSelectedCountry = CStr(Json.Value(Json.Get(firstResult, "country")))
mSelectedLatitude = CDbl(Json.Value(Json.Get(firstResult, "latitude")))
mSelectedLongitude = CDbl(Json.Value(Json.Get(firstResult, "longitude")))
mSelectedTimezone = ""
If Json.Has(firstResult, "timezone") Then mSelectedTimezone = CStr(Json.Value(Json.Get(firstResult, "timezone")))
txtSelected.Text = mSelectedName & ", " & mSelectedCountry
txtLabel.Text = mSelectedName
chkTracked.Checked = True
Await LoadCurrentWeather()
cmdSave.Enabled = True
lblStatus.Caption = CStr(count) & " location(s) found. The first result is ready to add."
Catch failure As McException
lblStatus.Caption = "API error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdFind.Enabled = True
End Try
End Sub
Private Async Function LoadCurrentWeather() As Boolean
Dim url As String
Dim envelopeText As String
Dim bodyText As String
Dim envelope As JsonValue
Dim document As JsonValue
Dim current As JsonValue
url = "https://api.open-meteo.com/v1/forecast?latitude="
url = url & CStr(mSelectedLatitude) & "&longitude=" & CStr(mSelectedLongitude)
url = url & "¤t=temperature_2m,wind_speed_10m,weather_code"
url = url & "&timezone=auto"
envelopeText = Await Http.RequestAsync("GET", url, "{""User-Agent"":""McBasic WeatherDesk""}", "", 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")))
document = Json.Parse(bodyText)
If Not Json.Has(document, "current") Then
lblCurrent.Caption = "The forecast response has no current section."
LoadCurrentWeather = False
Exit Function
End If
current = Json.Get(document, "current")
mTemperature = CDbl(Json.Value(Json.Get(current, "temperature_2m")))
mWindSpeed = CDbl(Json.Value(Json.Get(current, "wind_speed_10m")))
mWeatherCode = CLng(Json.Value(Json.Get(current, "weather_code")))
lblCurrent.Caption = "Current: " & CStr(mTemperature) & " °C | Wind: " & CStr(mWindSpeed) & " km/h | Weather code: " & CStr(mWeatherCode)
LoadCurrentWeather = True
End Function
Private Async Sub cmdSave_Click()
Dim command As DbCommand
Dim lookup As DbCommand
Dim saved As Recordset
Dim locationId As Long
Dim tracked As Long
If mBusy Then Exit Sub
If Len(Trim$(mSelectedName)) = 0 Then
lblStatus.Caption = "Find a city before adding it."
Exit Sub
End If
If Len(Trim$(txtLabel.Text)) = 0 Then
lblStatus.Caption = "Enter a label for the location."
txtLabel.SetFocus
Exit Sub
End If
tracked = 0
If chkTracked.Checked Then tracked = 1
mBusy = True
cmdSave.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("INSERT INTO saved_locations(label, name, country, latitude, longitude, timezone, tracked) VALUES(@label, @name, @country, @latitude, @longitude, @timezone, @tracked)")
command.AddParameter "@label", Trim$(txtLabel.Text)
command.AddParameter "@name", mSelectedName
command.AddParameter "@country", mSelectedCountry
command.AddParameter "@latitude", mSelectedLatitude
command.AddParameter "@longitude", mSelectedLongitude
command.AddParameter "@timezone", mSelectedTimezone
command.AddParameter "@tracked", tracked
Await command.ExecuteAsync()
Set lookup = mDb.CreateCommand("SELECT id FROM saved_locations WHERE name=@name AND country=@country")
lookup.AddParameter "@name", mSelectedName
lookup.AddParameter "@country", mSelectedCountry
Set saved = Await lookup.QueryAsync()
locationId = CLng(saved.Value(0, "id"))
Set command = mDb.CreateCommand("INSERT INTO forecast_snapshots(location_id, temperature, wind_speed, weather_code) VALUES(@location_id, @temperature, @wind_speed, @weather_code)")
command.AddParameter "@location_id", locationId
command.AddParameter "@temperature", mTemperature
command.AddParameter "@wind_speed", mWindSpeed
command.AddParameter "@weather_code", mWeatherCode
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location added and weather snapshot saved in SQLite."
Catch failure As McException
lblStatus.Caption = "Add error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
End Try
End Sub
Private Async Sub cmdUpdate_Click()
Dim command As DbCommand
Dim tracked As Long
If mBusy Then Exit Sub
If mSelectedId = 0 Then
lblStatus.Caption = "Select a saved location first."
Exit Sub
End If
If Len(Trim$(txtLabel.Text)) = 0 Then
lblStatus.Caption = "Enter a label for the location."
txtLabel.SetFocus
Exit Sub
End If
tracked = 0
If chkTracked.Checked Then tracked = 1
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("UPDATE saved_locations SET label=@label, tracked=@tracked, updated_at=CURRENT_TIMESTAMP WHERE id=@id")
command.AddParameter "@label", Trim$(txtLabel.Text)
command.AddParameter "@tracked", tracked
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location updated."
Catch failure As McException
lblStatus.Caption = "Update error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
End Try
End Sub
Private Async Sub cmdDelete_Click()
Dim command As DbCommand
If mBusy Then Exit Sub
If mSelectedId = 0 Then
lblStatus.Caption = "Select a saved location first."
Exit Sub
End If
If Not mDeleteArmed Then
mDeleteArmed = True
lblStatus.Caption = "Click Delete again to confirm removal of the selected location."
Exit Sub
End If
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Set command = mDb.CreateCommand("DELETE FROM forecast_snapshots WHERE location_id=@id")
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Set command = mDb.CreateCommand("DELETE FROM saved_locations WHERE id=@id")
command.AddParameter "@id", mSelectedId
Await command.ExecuteAsync()
Await RefreshLocations()
lblStatus.Caption = "Location and its weather snapshots deleted."
Catch failure As McException
lblStatus.Caption = "Delete error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.Enabled = True
End Try
End Sub
Private Async Sub cmdRefresh_Click()
If mBusy Then Exit Sub
mBusy = True
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
cmdRefresh.Enabled = False
Try
Await RefreshLocations()
lblStatus.Caption = CStr(mRows.RowCount) & " saved location(s) loaded from SQLite."
Catch failure As McException
lblStatus.Caption = "Refresh error " & CStr(failure.Number) & ": " & failure.Message
Finally
mBusy = False
cmdSave.Enabled = True
cmdRefresh.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 label, name, country, latitude, longitude, timezone, tracked, saved_at, updated_at FROM saved_locations ORDER BY label, name, country")
Set rows = Await command.QueryAsync()
Await rows.ExportCsvAsync(AppPaths.DataFile("weatherdesk-locations.csv"))
lblStatus.Caption = CStr(rows.RowCount) & " location(s) exported."
Catch failure As McException
lblStatus.Caption = "Export error " & CStr(failure.Number) & ": " & CStr(failure.Message)
Finally
mBusy = False
cmdExport.Enabled = True
End Try
End Sub
Private Sub grdLocations_SelectionChanged()
Dim row As Long
If mRows Is Nothing Then Exit Sub
row = CInt(grdLocations.SelectedIndex)
If row < 0 Or row >= mRows.RowCount Then Exit Sub
mSelectedId = CLng(mRows.Value(row, "id"))
mSelectedRow = row
mSelectedName = CStr(mRows.Value(row, "name"))
mSelectedCountry = CStr(mRows.Value(row, "country"))
mSelectedLatitude = CDbl(mRows.Value(row, "latitude"))
mSelectedLongitude = CDbl(mRows.Value(row, "longitude"))
If IsNull(mRows.Value(row, "timezone")) Then
mSelectedTimezone = ""
Else
mSelectedTimezone = CStr(mRows.Value(row, "timezone"))
End If
txtSelected.Text = mSelectedName & ", " & mSelectedCountry
txtLabel.Text = CStr(mRows.Value(row, "label"))
chkTracked.Checked = (CInt(mRows.Value(row, "tracked")) <> 0)
mDeleteArmed = False
cmdUpdate.Enabled = True
cmdDelete.Enabled = True
lblStatus.Caption = "Editing location " & CStr(mSelectedId) & "."
End Sub
Private Sub cmdClear_Click()
If mBusy Then Exit Sub
txtCity.Text = ""
txtSelected.Text = ""
txtLabel.Text = ""
chkTracked.Checked = True
lblCurrent.Caption = "Find a city to load its current weather."
lblStatus.Caption = "Ready for another search."
mSelectedId = 0
mSelectedRow = -1
mDeleteArmed = False
mSelectedName = ""
mSelectedCountry = ""
cmdSave.Enabled = False
cmdUpdate.Enabled = False
cmdDelete.Enabled = False
txtCity.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 saved_locations (
id INTEGER PRIMARY KEY,
label TEXT NOT NULL,
name TEXT NOT NULL,
country TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
timezone TEXT NULL,
tracked INTEGER NOT NULL DEFAULT 1,
saved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(name, country)
);
CREATE TABLE forecast_snapshots (
id INTEGER PRIMARY KEY,
location_id INTEGER NOT NULL,
fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
temperature REAL NULL,
wind_speed REAL NULL,
weather_code INTEGER NULL
);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 WeatherDesk.mcbproj
mcbasic run WeatherDesk.mcbprojAcceptance checklist
- Run against a fresh sample database: startup applies the initial migration.
- Search a city and inspect its selected name, coordinates and weather.
- Save it, select it in the grid, change its label/tracked state and update it.
- Delete the selected location and refresh.
- Export CSV and inspect the output.
- Try an empty search and a failed network request; previously saved locations must remain intact.
Step 7Make one change and repeat the tests#
Add a retry action for the last failed request without automatically repeating a database write. Test a non-success HTTP response, then confirm saved locations are unchanged.
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