Blazor DataRepeater Component Example

As an example, here is a custom DataRepeaterComponent. It should be run as client-side (WebAssembly) Blazor code.

See DataGridComponent for a simplier example that does not allow for so much customization, or see an ADVANCED version of this repeater which allows for editing data.

As can be seen here, the code for the repeater itself is very simple because most of it is done in the page.

@typeparam TItem

@if (Items != null)
{
	@foreach (var item in Items)
	{
		@Row(item)
	}
}

@code {

	[Parameter]
	public RenderFragment<TItem> Row { get; set; }

	[Parameter]
	public List Items { get; set; }
}

Here is the fragment of the razor page that uses the DataRepeaterComponent. A value for the Items parameter must be supplied and the Context must be set and then used in the HTML fragment to be rendered.

<div class="table-responsive">
	<table class="table table-hover table-striped">
		<thead>
			<tr><th>name</th><th>address</th><th colspan="2">zip</th></tr>
		</thead>
		<tbody>
			<DataRepeaterComponent Items="custs">
				<Row Context="cust">
					<tr>
						<td>@cust.name</td>
						<td>@cust.address</td>
						<td>@cust.zip</td>
						<td><button class="btn btn-sm btn-danger" @onclick="@(()=> Delete(cust.id))">delete</button></td>
					</tr>
				</Row>
			</DataRepeaterComponent>
		</tbody>
	</table>
</div>

Here is the whole razor page. Most of the client-side code lies in this page.

@page "/datarepeater"
@using BlazorExample.Shared
@inject HttpClient Http
@using BlazorExample.Client.Components

<h1>Customers</h1>

@if (custs == null)
{
	<p><em>Loading...</em></p>
}

<div class="table-responsive">
	<table class="table table-hover table-striped">
		<thead>
			<tr><th>name</th><th>address</th><th colspan="2">zip</th></tr>
		</thead>
		<tbody>
			<DataRepeaterComponent Items="custs">
				<Row Context="cust">
					<tr>
						<td>@cust.name</td>
						<td>@cust.address</td>
						<td>@cust.zip</td>
						<td><button class="btn btn-sm btn-danger" @onclick="@(()=> Delete(cust.id))">delete</button></td>
					</tr>
				</Row>
			</DataRepeaterComponent>
		</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" name="name" 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" name="address" 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">Zipcode</span>
		<input type="text" name="zip" class="form-control" autocomplete="off" required @bind-value="customer.zip" />
		<button class="btn btn-success" @onclick="Add">Add</button>
	</div>
</form>

@code {

	private List<Customer> custs;
	private Customer customer = new Customer();

	protected override async Task OnInitializedAsync()
	{
		await base.OnInitializedAsync();

		custs = await Http.GetFromJsonAsync<List<Customer>>("api/customers");
	}

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

	private async Task Delete(string id)
	{
		using(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);
				//StateHasChanged();
			}
		}
	}
}

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 BlazerExample.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; }
			});
		}
	}
}