Blazor DataGrid Component Example

As an example, here is a custom DataGridComponent. It allows for its data to be sorted by clicking on the column heading. It should be run as client-side (WebAssembly) Blazor code.

See DataRepeaterComponent for an example that allows for more customization to allow for deleting data, for example.

@typeparam TItem
@using System.Reflection

@if (Items != null)
{
	<div class="@ParentClassName">
		@if (info != null && info.Length > 0)
		{
			<table class="@TableClassName">
				<thead>
					<tr>
						@foreach (var member in info)
						{
							if (member.MemberType == MemberTypes.Property && !string.IsNullOrWhiteSpace(HideColumns) && Array.IndexOf(hideColumns, member.Name) == -1)
							{
								<th><a href="javascript:;" @onclick="@(()=>Sort(member.Name))">@member.Name</a></th>
							}
						}
					</tr>
				</thead>
				<tbody>
					@for (int index = 0; index < Items.Count; index++)
					{
						var item = Items[index];
						var key = GetDataKey(item);
						<tr data-index="@index">
							@foreach (var member in info)
							{
								if (member.MemberType == MemberTypes.Property && !string.IsNullOrWhiteSpace(HideColumns) && !HideColumns.Contains(member.Name))
								{
									<td data-key="@key">@item.GetType().GetProperty(member.Name).GetValue(item)</td>
								}
							}
						</tr>
					}
				</tbody>
			</table>
		}
	</div>
}

@code {

	[Parameter]
	public List<TItem> Items { get; set; }

	[Parameter]
	public string ParentClassName { get; set; }

	[Parameter]
	public string TableClassName { get; set; }

	private string[] hideColumns = { };
	[Parameter]
	public string HideColumns
	{
		get
		{
			return string.Join(',', hideColumns);
		}

		set
		{
			hideColumns = value.Split(',');
		}
	}

	[Parameter]
	public string DataKeyColumn { get; set; }

	private MemberInfo[] info;

	private string GetDataKey(TItem item)
	{
		if (!string.IsNullOrEmpty(DataKeyColumn))
		{
			foreach (var member in info)
			{
				if (member.MemberType == MemberTypes.Property && member.Name == DataKeyColumn)
					return item.GetType().GetProperty(member.Name).GetValue(item).ToString();
			}
		}
		return string.Empty;
	}

	protected override void OnParametersSet()
	{
		base.OnParametersSet();
		if (Items != null && Items.Count > 0)
		{
			Type type = Items[0].GetType();
			info = type.GetMembers();
		}
	}
	private string sortedField;
	private bool descending = true;
	private void Sort(string field)
	{
		if (sortedField == field)
			descending = !descending;
		sortedField = field;
		if (descending)
		{
			Items.Sort((a, b) =>
			{
				var obj = typeof(TItem).GetProperty(field).GetValue(a);
				if (obj is DateTime)
					return ((DateTime)typeof(TItem).GetProperty(field).GetValue(a)).ToString("yyyyMMddHHmmssfff").CompareTo(((DateTime)typeof(TItem).GetProperty(field).GetValue(b)).ToString("yyyyMMddHHmmssfff"));
				else if (obj is byte || obj is sbyte || obj is short || obj is ushort || obj is int)
					return ((int)typeof(TItem).GetProperty(field).GetValue(a)) - (int)typeof(TItem).GetProperty(field).GetValue(b);
				else
					return typeof(TItem).GetProperty(field).GetValue(a).ToString().CompareTo(typeof(TItem).GetProperty(field).GetValue(b).ToString());
			});
		}
		else
		{
			Items.Sort((a, b) =>
			{
				var obj = typeof(TItem).GetProperty(field).GetValue(a);
				if (obj is DateTime)
					return ((DateTime)typeof(TItem).GetProperty(field).GetValue(b)).ToString("yyyyMMddHHmmssfff").CompareTo(((DateTime)typeof(TItem).GetProperty(field).GetValue(a)).ToString("yyyyMMddHHmmssfff"));
				else if (obj is byte || obj is sbyte || obj is short || obj is ushort || obj is int)
					return ((int)typeof(TItem).GetProperty(field).GetValue(b)) - (int)typeof(TItem).GetProperty(field).GetValue(a);
				else
					return typeof(TItem).GetProperty(field).GetValue(b).ToString().CompareTo(typeof(TItem).GetProperty(field).GetValue(a).ToString());
			});
		}
	}

}

Here is a razor page that uses the DataGridComponent. In contrary to the page for the DataRepeaterComponent, the code is very simple.

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

<h1>Customers</h1>

@if (custs == null)
{
	<p><em>Loading...</em></p>
}
<DataGridComponent ParentClassName="table-responsive" TableClassName="table table-hover table-striped" HideColumns="id" DataKeyColumn="id" Items="custs"></DataGridComponent>

<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();
			}
		}
	}
}

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