How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete?

The answer shows how to update the Blazor WeatherForecast application to demonstrate the state/notification pattern and how to use it in components. I’ve used the Weather Forecast application because there’s not enough detail in your question to use your code as the basis for an answer, and the Weather Forecast application provides a good template to build on.

The starting point is a standard Blazor Server template project. Mine is called StackOverflow.Answers

Add a Loading.razor component. This will detect load state and display a rotator when the records are loading.

@if (this.IsLoaded)
{
    @this.ChildContent
}
else
{
    <div class="loader"></div>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }

    [Parameter] public bool IsLoaded { get; set; }
}

Add a component CSS file – Loading.razor.css – to format the rotator:

.page-loader {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    width: 150px;
    height: 150px;
    margin: -75px 0 0 -75px;
    border: 16px solid #f3f3f3;
    border-radius: 50%;
    border-top: 16px solid #3498db;
    width: 120px;
    height: 120px;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
}

.loader {
    border: 16px solid #f3f3f3;
    /* Light grey */
    border-top: 16px solid #3498db;
    /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
    margin-left: auto;
    margin-right: auto;
}

@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
    }

    100% {
        -webkit-transform: rotate(360deg);
    }
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

I split the original service into separate data and view services (good design practice).

Update the WeatherForecastService. It’s now the data service and all it needs to do is provide the data. In a real app this will interface with the data brokers to get real data.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace StackOverflow.Answers.Data
{
    public class WeatherForecastService
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private List<WeatherForecast> recordsShort;
        private List<WeatherForecast> recordsLong;

        public WeatherForecastService()
        {
            recordsShort = GetForecastsShort;
            recordsLong = GetForecastsLong;
        }

        public async Task<List<WeatherForecast>> GetForecastsAsync(bool islong = false)
        {
            await Task.Delay(3000);
            return islong ? this.recordsLong : this.recordsShort;
        }

        public List<WeatherForecast> GetForecastsShort
        {
            get
            {
                var rng = new Random();
                return Enumerable.Range(1, 3).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                }).ToList();
            }
        }

        public List<WeatherForecast> GetForecastsLong
        {
            get
            {
                var rng = new Random();
                return Enumerable.Range(1, 6).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                }).ToList();
            }
        }
    }
}

Add a new WeatherForecastViewService class to Data folder. This is our view service. It holds our data and is the service the UI uses. It gets data from the data service and exposes a Records list and ListChanged event that is triggered whenever the list changes.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace StackOverflow.Answers.Data
{
    public class WeatherForecastViewService
    {
        public List<WeatherForecast> Records { get; set; }

        private WeatherForecastService weatherForecastService;

        public WeatherForecastViewService(WeatherForecastService weatherForecastService)
        {
            this.weatherForecastService = weatherForecastService;
        }

        public async Task GetForecastsAsync(bool islong = false)
        {
            this.Records = null;
            this.NotifyListChanged(this.Records, EventArgs.Empty);
            this.Records = await weatherForecastService.GetForecastsAsync(islong);
            this.NotifyListChanged(this.Records, EventArgs.Empty);
        }

        public event EventHandler<EventArgs> ListChanged;

        public void NotifyListChanged(object sender, EventArgs e)
            => ListChanged?.Invoke(sender, e);
    }
}

Add a new Component – WeatherForecastList.razor. This is the guts from Fetchdata. It:

  1. Uses the new Loading component.
  2. Uses the new WeatherForecastViewService.
  3. Uses the list directly from WeatherForecastViewService. It doesn’t have it’s own copy – all components use the same list.
  4. Wires into the view service ListChanged event and calls StateHasChanged whenever the evwnt is triggered.
@implements IDisposable
@using StackOverflow.Answers.Data

<h1>Weather forecast</h1>
<Loading IsLoaded="this.isLoaded" >
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in viewService.Records)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
</Loading>

@code {

    [Inject] private WeatherForecastViewService viewService { get; set; }

    private bool isLoaded => viewService.Records is not null;

    protected override async Task OnInitializedAsync()
    {
        await GetForecastsAsync();
        this.viewService.ListChanged += this.OnListChanged;
    }

    private async Task GetForecastsAsync()
        =>  await viewService.GetForecastsAsync();

    private void OnListChanged(object sender, EventArgs e)
        => this.InvokeAsync(this.StateHasChanged);

    public void Dispose()
    {
        this.viewService.ListChanged -= this.OnListChanged;
    }
}

Update Startup Services for the new services.

    services.AddSingleton<WeatherForecastService>();
    services.AddScoped<WeatherForecastViewService>();

Update FetchData. It now uses the WeatherForecastList component. The button provides a mechanism to change the List and see the UI updates.

@page "/fetchdata"
@using StackOverflow.Answers.Data

<WeatherForecastList/>
<div class="m-2">
    <button class="btn btn-dark" @onclick="this.LoadRecords">Reload Records</button>
</div>
@code {

    [Inject] WeatherForecastViewService viewService { get; set; }

    private bool isLong = true;

    private async Task LoadRecords()
    {
        await this.viewService.GetForecastsAsync(isLong);
        this.isLong = !this.isLong;
    }
}

Hopefully I’ve got all the code right first time! I’m sure someone will point out any glaring errors, or improvements.

Leave a Comment