Blazor WebAssembly DataRepeater Component Advanced Example

As an example, here is a custom DataRepeaterComponent that allows for the editing of data. It should be run as client-side (WebAssembly) Blazor code.

See simple DataRepeaterComponent (link) for a simplier example that does not allow for editing. Note that this does not use the built-in EditForm component.

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<TItem> 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. Also, the name of the field being edited must be added to each call to the Save function.

<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>
							<input type="text" class="form-control" value="@cust.name" @onchange="@((ChangeEventArgs e) => Save(e, cust, "name"))" />
						</td>
						<td>
							<input type="text" class="form-control" value="@cust.address" @onchange="@((ChangeEventArgs e) => Save(e, cust, "address"))" />
						</td>
						<td>
							<input type="text" class="form-control" value="@cust.zip" @onchange="@((ChangeEventArgs e) => Save(e, 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.

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

<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>
							<input type="text" class="form-control" value="@cust.name" @onchange="@((ChangeEventArgs e) => Save(e, cust, "name"))" />
						</td>
						<td>
							<input type="text" class="form-control" value="@cust.address" @onchange="@((ChangeEventArgs e) => Save(e, cust, "address"))" />
						</td>
						<td>
							<input type="text" class="form-control" value="@cust.zip" @onchange="@((ChangeEventArgs e) => Save(e, 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" class="form-control" autocomplete="off" required @bind-value="newcust.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="newcust.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="newcust.zip" />
		<button class="btn btn-success" @onclick="Add">Add</button>
	</div>
</form>

@code {

	private List<Customer> custs;
	private Customer newcust = 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", newcust, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				custs.Add(await msg.Content.ReadFromJsonAsync<Customer>());
				//StateHasChanged();
			}
		}
	}

	private async Task Save(ChangeEventArgs e, Customer cust, string propField)
	{
		PropertyInfo pinfo = typeof(Customer).GetProperty(propField);
		pinfo.SetValue(cust, e.Value);

		using (var msg = await Http.PutAsJsonAsync<Customer>($"/api/customers/{cust.id}", cust, System.Threading.CancellationToken.None))
		{
			if (msg.IsSuccessStatusCode)
			{
				//StateHasChanged();
			}
			else
			{
				// DO SOMETHING
			}
		}
	}

	private async Task Delete(string id)
	{
		using (var msg = await Http.DeleteAsync($"/api/customers/{id}"))
		{
			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.

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

Coding Video

https://youtu.be/wBJ-VCrd1Yk


Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site.
OK