Search code examples
webassemblyblazor

Blazor form submit needs two clicks to refresh view


I have the following Blazor component I'm testing an API call to get weather data. The submit button needs to be clicked twice for some reason in order for the table to display the updated object's properties.

When the page initializes I get the location from the browser just fine and the weather data is displayed in the table without issue.

FetchData.razor

@page "/fetchdata"
@using BlazingDemo.Client.Models
@using AspNetMonsters.Blazor.Geolocation

@inject HttpClient Http
@inject LocationService LocationService

<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>

<div>
    <EditForm Model="@weatherForm" 
        OnValidSubmit="@GetWeatherDataAsync">
        <DataAnnotationsValidator />
        <ValidationSummary />
        <InputText Id="cityName" 
                   bind-Value="@weatherForm.CityName" />
        <button type="submit">Submit</button>
    </EditForm>
</div>
<br />

@if (weatherData == null)
{
    <Loader/>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>City</th>
                <th>Conditions</th>
                <th>Temp. (C)</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>@weatherData.Name</td>
                <td>@weatherData.Weather[0].Main</td>
                <td>@weatherData.Main.Temp</td>
            </tr>
        </tbody>
    </table>
}

@functions {

    WeatherFormModel weatherForm = new WeatherFormModel();
    WeatherData weatherData;
    Location location;

    protected override async Task OnInitAsync()
    {
        location = await LocationService.GetLocationAsync();

        if (location != null)
        {
            weatherData = await 
        GetWeatherDataAsync(location.Latitude,location.Longitude);
        }
    }

    protected async Task<WeatherData> GetWeatherDataAsync(decimal 
                        latitude, decimal longitude)
    {
        return await Http.GetJsonAsync<WeatherData> 
          ($"https://api.openweathermap.org/data/2.5/weather?lat=" 
         + $"{location.Latitude}&lon=" 
         + $"{location.Longitude}&units=metric&appid=***removed***");
    }

    protected async void GetWeatherDataAsync()
    {
        weatherData = await Http.GetJsonAsync<WeatherData> 
          ($"https://api.openweathermap.org/data/2.5/weather?q=" 
         + $"{weatherForm.CityName}&units=metric&appid=***removed***");
    }
}

WeatherFormModel.cs

namespace BlazingDemo.Client.Models
{
    public class WeatherFormModel
    {
        [Required, Display(Name = "City Name")]
        public string CityName { get; set; }

        public bool IsCelcius { get; set; }
    }
}

I'm calling the GetWEatherDataAsync() method and I can see the data being returned from the API on each call by inspecting Chrome's return JSON data. It just never updates the table on the first click. I've also tried setting weatherData to null before calling the method but that doesn't work either.

Any suggestions?


Solution

  • The reason the StateHasChanged() call was necessary here is because the GetWeatherDataAsync() method you were referring to is void. So there was no a way for the server to know when the call has completed. So the original form submit request finished earlier than the weather data was populated. Hence, the call to StateHasChanged() would indicate the server that there is a need to rerender the component and that would work just fine. In general, you should simply avoid using async void (unless you know it's truly a fire-and-forget situation) and return some type of Task instead, which would eliminate the need for the explicit StateHasChanged() call.

    Below, I've simply changed your GetWeatherDataAsync method to return Task instead of void:

    protected async Task GetWeatherDataAsync()
    {
        weatherData = await Http.GetJsonAsync<WeatherData>($"https://api.openweathermap.org/data/2.5/weather?q={weatherForm.CityName}&units=metric&appid=***removed***");
    }