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:
- Uses the new
Loading
component. - Uses the new WeatherForecastViewService.
- Uses the list directly from
WeatherForecastViewService
. It doesn’t have it’s own copy – all components use the same list. - Wires into the view service
ListChanged
event and callsStateHasChanged
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.