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

Build a weather and saved-location workspace

Combine geocoding, current weather, SQLite migrations, saved-location CRUD and CSV export.

1 Form / control designs1 complete BASIC filesGuided sample project

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
project.mcproj
{
  "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 propertyValue
Caption"WeatherDesk - Open-Meteo forecast"
Width1060
Height720
MinWidth900
MinHeight600
TypeNameParent
LabellblCityForm
TextBoxtxtCityForm
CommandButtoncmdFindForm
CommandButtoncmdClearForm
LabellblSelectedForm
TextBoxtxtSelectedForm
CommandButtoncmdSaveForm
CommandButtoncmdRefreshForm
LabellblLabelForm
TextBoxtxtLabelForm
CheckBoxchkTrackedForm
CommandButtoncmdUpdateForm
CommandButtoncmdDeleteForm
LabellblCurrentForm
LabellblStatusForm
DataGridgrdLocationsForm
CommandButtoncmdExportForm
lblCity · exact Inspector values
PropertyValue
Left24
Top24
Width80
Height30
Caption"City"
Anchor edgesLeft, Top
txtCity · exact Inspector values
PropertyValue
Left112
Top20
Width300
Height36
Text"Singapore"
TabIndex0
Anchor edgesLeft, Top
cmdFind · exact Inspector values
PropertyValue
Left428
Top20
Width120
Height36
Caption"Find city"
TabIndex1
Anchor edgesLeft, Top
cmdClear · exact Inspector values
PropertyValue
Left562
Top20
Width100
Height36
Caption"Clear"
TabIndex2
Anchor edgesLeft, Top
lblSelected · exact Inspector values
PropertyValue
Left24
Top78
Width110
Height30
Caption"Selected place"
Anchor edgesLeft, Top
txtSelected · exact Inspector values
PropertyValue
Left144
Top74
Width320
Height36
ReadOnlyTrue
TabIndex3
Anchor edgesLeft, Top
cmdSave · exact Inspector values
PropertyValue
Left484
Top74
Width132
Height36
Caption"Add location"
TabIndex4
Anchor edgesLeft, Top
cmdRefresh · exact Inspector values
PropertyValue
Left630
Top74
Width130
Height36
Caption"Refresh list"
TabIndex5
Anchor edgesLeft, Top
lblLabel · exact Inspector values
PropertyValue
Left24
Top126
Width80
Height30
Caption"Label"
Anchor edgesLeft, Top
txtLabel · exact Inspector values
PropertyValue
Left112
Top122
Width280
Height36
Text""
TabIndex6
Anchor edgesLeft, Top
chkTracked · exact Inspector values
PropertyValue
Left412
Top124
Width180
Height32
Caption"Track this location"
CheckedTrue
TabIndex7
Anchor edgesLeft, Top
cmdUpdate · exact Inspector values
PropertyValue
Left612
Top122
Width120
Height36
Caption"Update"
TabIndex8
Anchor edgesLeft, Top
cmdDelete · exact Inspector values
PropertyValue
Left744
Top122
Width120
Height36
Caption"Delete"
TabIndex9
Anchor edgesLeft, Top
lblCurrent · exact Inspector values
PropertyValue
Left24
Top174
Width980
Height34
Caption"Find a city to load its current weather."
Anchor edgesLeft, Top, Right
lblStatus · exact Inspector values
PropertyValue
Left24
Top210
Width980
Height36
Caption"Opening the local weather database..."
Anchor edgesLeft, Top, Right
grdLocations · exact Inspector values
PropertyValue
Left24
Top260
Width980
Height360
TabIndex10
EmptyText"No saved locations yet."
Anchor edgesLeft, Top, Right, Bottom
cmdExport · exact Inspector values
PropertyValue
Left24
Top638
Width130
Height36
Caption"Export CSV"
TabIndex11
Anchor edgesLeft, Bottom

Step 3Connect the events explicitly#

frmWeather

ObjectEventProcedureParameters
cmdFindClickcmdFind_ClickClick()
cmdClearClickcmdClear_ClickClick()
cmdSaveClickcmdSave_ClickClick()
cmdRefreshClickcmdRefresh_ClickClick()
cmdUpdateClickcmdUpdate_ClickClick()
cmdDeleteClickcmdDelete_ClickClick()
grdLocationsSelectionChangedgrdLocations_SelectionChangedSelectionChanged()
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/frmWeather.bas

Enter these declarations at file scope, before the procedures.

Forms/frmWeather.bas · declarations
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()

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

Forms/frmWeather.bas · Form_Shown
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

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

Forms/frmWeather.bas · RefreshLocations
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()

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

Forms/frmWeather.bas · cmdFind_Click
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

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

Forms/frmWeather.bas · LoadCurrentWeather
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 & "&current=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()

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

Forms/frmWeather.bas · cmdSave_Click
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()

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

Forms/frmWeather.bas · cmdUpdate_Click
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()

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

Forms/frmWeather.bas · cmdDelete_Click
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()

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

Forms/frmWeather.bas · cmdRefresh_Click
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()

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

Forms/frmWeather.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 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()

Read the current value or selection and update its dependent interface. The binding above determines which object calls this handler.

Forms/frmWeather.bas · grdLocations_SelectionChanged
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()

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

Forms/frmWeather.bas · cmdClear_Click
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()

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

Forms/frmWeather.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/frmWeather.bas file

Replace the file once; do not append duplicate procedures.

Forms/frmWeather.bas
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 & "&current=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 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 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:

Developer CLI
mcbasic check WeatherDesk.mcbproj
mcbasic run WeatherDesk.mcbproj

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

Source baseline 20f4bfed6b · Build manifest