PROWAREtech

articles » current » blazor » wasm » timer-refresh-rest-api-data

Blazor: Timer Example - Refresh Data

An example using the Timer class to refresh data from a REST API or Web API.

This code is compatible with .NET Core 3.1, .NET 5, .NET 6 and .NET 8. If using .NET 5 then follow the .NET Core 3.1 code.

Here is an example that uses the timer System.Threading.Timer to refresh the data on the user's screen. It should be run as client-side Blazor WASM code.

For an example of the Timer class running an analog clock, see this article.

.NET 8 Example

This example uses the new .NET 8 Web App which is both server-side and client-side (WebAssembly). Create a new .NET 8 Web App (as in next image) named BlazorTimer and apply the changes below to its files and code.

.NET 8 Blazor Web App Options

Modify the Client Project's Code

Create a WeatherForecast.cs file located in the root of the client project.

// WeatherForecast.cs

namespace BlazorTimer.Client
{
	public class WeatherForecast
	{
		public DateTime Date { get; set; }

		public int TemperatureC { get; set; }

		public string Summary { get; set; }

		public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
	}
}

Create a new razor page named FetchData.razor for the client project.

@page "/fetchdata"
@rendermode @(new InteractiveAutoRenderMode(prerender: false))
@using BlazorTimer.Client
@inject NavigationManager nav

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server using a timer.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <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 forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;
    private System.Threading.Timer? timer;

    protected override async Task OnInitializedAsync()
    {
        using (var httpClient = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
            forecasts = await System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsync<WeatherForecast[]>(httpClient, "/api/WeatherForecast");

        timer = new System.Threading.Timer(async (object? stateInfo) =>
        {
            using (var httpClient2 = new HttpClient { BaseAddress = new Uri(nav.BaseUri) })
                forecasts = await System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsync<WeatherForecast[]>(httpClient2, "/api/WeatherForecast");
            StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
        }, new System.Threading.AutoResetEvent(false), 2000, 2000); // fire every 2000 milliseconds
    }
}

Modify the Server Project's Code

Here is the Program.cs file of the server project. Notice the code comments.

// Program.cs
using BlazorTimer.Client.Pages;
using BlazorTimer.Components;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
	.AddInteractiveServerComponents()
	.AddInteractiveWebAssemblyComponents();


builder.Services.AddControllers(); // NOTE: line is newly added


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
	app.UseWebAssemblyDebugging();
}
else
{
	app.UseExceptionHandler("/Error", createScopeForErrors: true);
	// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
	app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
	.AddInteractiveServerRenderMode()
	.AddInteractiveWebAssemblyRenderMode()
	.AddAdditionalAssemblies(typeof(BlazorTimer.Client._Imports).Assembly); // NOTE: THIS LINE OF CODE MODIFIED


app.MapControllers(); // NOTE: line is newly added


app.Run();

Create a new folder named Controllers.

Create a new file named WeatherForecastController.cs located in the Controllers folder of the server project.

// WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using BlazorTimer.Client;

namespace BlazorTimer.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

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

	}
}

Modify the NavMenu.razor component to have a new link as done in the code below.

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">BlazorTimer</a>
    </div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
            </NavLink>
        </div>

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="weather">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
            </NavLink>
        </div>

        @* NOTE: THIS BLOCK OF CODE IS NEW *@
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> FetchData
            </NavLink>
        </div>
        @* NOTE: END BLOCK OF NEW CODE *@

    </nav>
</div>

The Visual Studio 2022 Solution Explorer should look like this after applying the above changes.

Blazor Web App Solution Explorer

Hit Ctrl+F5 key combination to run the application. Watch as the page refreshes its weather information every 2 seconds.

.NET 6 Example

Here is a .NET 6 razor page that uses the Timer. It is an example that needs to call StateHasChanged(). The code for the RESTful API is just the default code for "WeatherForecast" controller that Microsoft adds when creating a new project.

@page "/fetchdata"
@using BlazorTimer.Shared
@inject HttpClient Http

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server using a timer.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <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 forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;
    private System.Threading.Timer? timer; // NOTE: THIS LINE OF CODE ADDED

    protected override async Task OnInitializedAsync()
    {
		// NOTE: THE FOLLOWING CODE ADDED
		timer = new System.Threading.Timer(async (object? stateInfo) =>
		{
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
			StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
		}, new System.Threading.AutoResetEvent(false), 2000, 2000); // fire every 2000 milliseconds
    }
}

Run this code and watch the "fetchdata" page update the weather forecast every couple seconds. It is just that simple!

Here is the standard WeatherForecastController code that is supplied by Microsoft. It is unmodified.

using BlazorTimer.Shared;
using Microsoft.AspNetCore.Mvc;

namespace BlazorTimer.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Here is the standard WeatherForecast class that is supplied by Microsoft. It is unmodified.

namespace BlazorTimer.Shared
{
    public class WeatherForecast
    {
        public DateTime Date { get; set; }

        public int TemperatureC { get; set; }

        public string? Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

.NET Core 3.1 Example

Here is a razor page that uses the Timer. It is an example that needs to call StateHasChanged(). Code follows that implements the RESTful API.

@page "/timerexample"
@using BlazorExample.Shared
@inject HttpClient Http

<h1>Customers</h1>

@if (custs == null)
{
	<p><em>Loading...</em></p>
}
else
{
<div class="table-responsive">
	<table class="table table-hover table-striped">
		<thead>
			<tr>
				<th>name</th>
				<th>address</th>
				<th>zip</th>
			</tr>
		</thead>
		<tbody>
		@foreach (var cust in custs)
		{
			<tr>
				<td>@cust.name</td>
				<td>@cust.address</td>
				<td>@cust.zip</td>
			</tr>
		}
		</tbody>
	</table>
</div>
}

<form class="mt-5" onsubmit="return false;">
	<div class="input-group input-group-md mb-2">
		<span class="input-group-text">Name</span>
		<input type="text" class="form-control" autocomplete="off" required @bind-value="customer.name" />
	</div>
	<div class="input-group input-group-md mb-2">
		<span class="input-group-text">Address</span>
		<input type="text" class="form-control" autocomplete="off" required @bind-value="customer.address" />
	</div>
	<div class="input-group input-group-md mb-2">
		<span class="input-group-text">Zip</span>
		<input type="text" class="form-control" autocomplete="off" required @bind-value="customer.zip" />
		<button class="btn btn-success" @onclick="Add" type="button">Add</button>
	</div>
</form>

@code {
	private List<Customer> custs;
	private Customer customer = new Customer();
	private System.Threading.Timer timer;

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();
		custs = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");

		timer = new System.Threading.Timer(async (object stateInfo) =>
		{
			custs = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");
			StateHasChanged(); // NOTE: MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
		}, new System.Threading.AutoResetEvent(false), 1000, 1000); // fire every 1000 milliseconds
	}

	private async Task Add()
	{
		var msg = await Http.PostAsJsonAsync<Customer>("/api/customers", customer, System.Threading.CancellationToken.None);
		if (msg.IsSuccessStatusCode)
		{
			custs.Add(await msg.Content.ReadFromJsonAsync<Customer>());
		}
	}

	private async Task Delete(string id)
	{
		var msg = await Http.DeleteAsync("/api/customers/" + id, System.Threading.CancellationToken.None);
		if (msg.IsSuccessStatusCode)
		{
			int i;
			for (i = 0; i < custs.Count && custs[i].id != id; i++) ;
			custs.RemoveAt(i);
		}
	}
}

Here is the Customer definition.

using System;

namespace BlazorExample.Shared
{
	[Serializable]
	public class Customer
	{
		public string id { get; set; }
		public string name { get; set; }
		public string address { get; set; }
		public string zip { get; set; }
	}
}

Here is the RESTful API implementation used by the above page that does not rely upon a database.

 // CustomersController.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using BlazorExample.Shared;

namespace BlazorExample.Server.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class CustomersController : ControllerBase
	{
		private string DataFolder { get; }
		private IWebHostEnvironment env { get; }
		public CustomersController(IWebHostEnvironment env)
		{
			this.env = env;
			DataFolder = env.ContentRootPath + Path.DirectorySeparatorChar + "CustomersData";
			try
			{
				if (!System.IO.Directory.Exists(DataFolder))
					System.IO.Directory.CreateDirectory(DataFolder);
			}
			catch { }
		}

		[HttpPut("{id}")]
		public async Task<Customer> Put(string id, [FromBody] Customer cust)
		{
			return await Task.Run(() =>
			{
				try
				{
					cust.id = id;
					System.IO.File.WriteAllText(DataFolder + Path.DirectorySeparatorChar + id + ".json", JsonSerializer.Serialize(cust));
					return cust;
				}
				catch { return null; }
			});
		}

		[HttpPatch("{id}")]
		public async Task<Customer> Patch(string id, [FromBody] Customer update)
		{
			return await Task.Run(() =>
			{
				try
				{
					var cust = JsonSerializer.Deserialize<Customer>(System.IO.File.ReadAllText(DataFolder + Path.DirectorySeparatorChar + id + ".json"));
					cust.name = (update.name == null) ? cust.name : update.name;
					cust.address = (update.address == null) ? cust.address : update.address;
					cust.zip = (update.zip == null) ? cust.zip : update.zip;
					System.IO.File.WriteAllText(DataFolder + Path.DirectorySeparatorChar + id + ".json", JsonSerializer.Serialize(cust));
					return cust;
				}
				catch { return null; }
			});
		}

		[HttpDelete("{id}")]
		public async Task<Customer> Delete(string id)
		{
			return await Task.Run(() =>
			{
				var di = new DirectoryInfo(DataFolder);
				try
				{
					string file = DataFolder + Path.DirectorySeparatorChar + id + ".json";
					var cust = JsonSerializer.Deserialize<Customer>(System.IO.File.ReadAllText(file));
					System.IO.File.Delete(file);
					return cust;
				}
				catch { return null; }
			});
		}

		[HttpPost]
		public async Task<Customer> Post([FromBody] Customer cust)
		{
			return await Task.Run(() =>
			{
				var di = new DirectoryInfo(DataFolder);
				string id = Guid.NewGuid().ToString("N");
				cust.id = id;
				if (cust.name == null)
					cust.name = string.Empty;
				else
					cust.name = cust.name.Substring(0, cust.name.Length > 100 ? 100 : cust.name.Length);
				if (cust.address == null)
					cust.address = string.Empty;
				else
					cust.address = cust.address.Substring(0, cust.address.Length > 100 ? 100 : cust.address.Length);
				if (cust.zip == null)
					cust.zip = string.Empty;
				else
					cust.zip = cust.zip.Substring(0, cust.zip.Length > 10 ? 10 : cust.zip.Length);
				try
				{
					System.IO.File.WriteAllText(DataFolder + Path.DirectorySeparatorChar + id + ".json", JsonSerializer.Serialize(cust));
					return cust;
				}
				catch { return null; }
			});
		}

		[HttpGet]
		public async Task<IEnumerable<Customer>> Get()
		{
			return await Task.Run(() =>
			{
				var di = new DirectoryInfo(DataFolder);
				FileInfo[] fi = di.GetFiles("*", SearchOption.TopDirectoryOnly);
				var q = from f in fi orderby f.CreationTime descending select f;
				var custs = new List<Customer>();
				for (int i = 0; i < fi.Length; i++)
					custs.Add(JsonSerializer.Deserialize<Customer>(System.IO.File.ReadAllText(fi[i].FullName)));
				return custs;
			});
		}

		[HttpGet("{id}")]
		public async Task<Customer> Get(string id)
		{
			return await Task.Run(() =>
			{
				var di = new DirectoryInfo(DataFolder);
				try
				{
					return JsonSerializer.Deserialize<Customer>(System.IO.File.ReadAllText(DataFolder + Path.DirectorySeparatorChar + id + ".json"));
				}
				catch { return null; }
			});
		}
	}
}

This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE