Blazor WebAssembly Timer Example (Refresh REST API Data)

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 (WebAssembly) Blazor code.

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(); // MUST CALL StateHasChanged() BECAUSE THIS IS TRIGGERED BY A TIMER INSTEAD OF A USER EVENT
		}, new System.Threading.AutoResetEvent(false), 1000, 1000);
	}

	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.

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.Runtime.Serialization.Formatters.Binary;
using BlazorExample.Shared;

namespace BlazorExample.Controllers
{
	[ApiController]
	[Route("api/[controller]")]
	public class CustomersController : ControllerBase
	{
		private static byte[] ObjectToBytes(object obj)
		{
			BinaryFormatter bf = new BinaryFormatter();
			using (var ms = new MemoryStream())
			{
				bf.Serialize(ms, obj);
				return ms.ToArray();
			}
		}
		private static object BytesToObject(byte[] bytes)
		{
			try
			{
				using (MemoryStream ms = new System.IO.MemoryStream(bytes))
				{
					BinaryFormatter bf = new BinaryFormatter();
					ms.Position = 0;
					return bf.Deserialize(ms);
				}
			}
			catch { return null; }
		}
		private string DataFolder { get; }
		private IWebHostEnvironment env { get; }
		public CustomersController(IWebHostEnvironment env)
		{
			this.env = env;
			DataFolder = env.ContentRootPath + "\\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.WriteAllBytes(DataFolder + '\\' + id + ".bin", ObjectToBytes(cust));
					return cust;
				}
				catch { return null; }
			});
		}

		[HttpPatch("{id}")]
		public async Task<Customer> Patch(string id, [FromBody] Customer update)
		{
			return await Task.Run(() =>
			{
				var ip = HttpContext.Connection.RemoteIpAddress.ToString().Replace(':', '-');
				try
				{
					var cust = (Customer)BytesToObject(System.IO.File.ReadAllBytes(DataFolder + '\\' + ip + '-' + id + ".bin"));
					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.WriteAllBytes(DataFolder + '\\' + ip + '-' + id + ".bin", ObjectToBytes(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 + '\\' + id + ".bin";
					var cust = (Customer)BytesToObject(System.IO.File.ReadAllBytes(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.WriteAllBytes(DataFolder + '\\' + id + ".bin", ObjectToBytes(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((Customer)BytesToObject(System.IO.File.ReadAllBytes(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 (Customer)BytesToObject(System.IO.File.ReadAllBytes(DataFolder + '\\' + id + ".bin"));
				}
				catch { return null; }
			});
		}
	}
}