Originally, the plaintext and images were defined in a single data model class, but the issue is that it fetches everything before it renders on the client-side.
Therefore, I decided to separate the plaintext from the images, meaning I have two data models for the two instances of the data sets.
The concept is that I needed to improve the UX of my Blazor web app, where the plaintext should be rendered first before the images. But even if I implemented a separation for these two data sets, it still renders both at the same time.
Backend
data model class:
[Keyless]public class PlainTextModel{ [Required] [Column("id")] public string? ID { get; set; } [Column("role")] public string? Role { get; set; } [Column("p_stat")] public bool PStat { get; set; } [Column("v_stat")] public bool VStat { get; set; } [Column("name")] public string? Name { get; set; } [Column("remarks")] public string? Remarks { get; set; } [Column("dt_registered")] public string? DtRegistered { get; set; } [Column("dt_modified")] public string? DtModified { get; set; } [Column("verified_by")] public string? VerifiedBy { get; set; }}[Keyless]public class ImageModel{ [Column("img1")] public byte[]? Image1 { get; set; } [Column("img2")] public byte[]? Image2 { get; set; } [Column("img3")] public byte[]? Image3 { get; set; }}DBContext configurations:
using Microsoft.EntityFrameworkCore;namespace MyProject.Data.SQLServer{ public class SQLServerContext(DbContextOptions<SQLServerContext> options) : DbContext(options) { public DbSet<Models.SQLServer.PlainTextModel> PlainTextModel { get; set; } = default!; public DbSet<Models.SQLServer.ImageModel> ImageModel { get; set; } = default!; protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Models.SQLServer.PlainTextModel>() .HasNoKey(); modelBuilder.Entity<Models.SQLServer.ImageModel>() .HasNoKey().ToView(null); } }}Method for fetching of plaintext data:
public async Task<PlainTextModel?> PlainTextAsync(string spName, Dictionary<string, object?> parameters){ var paramDefs = SQLServerInnerHelper.GlobalMethodSPParam(); SqlParameter[] sqlParameter = MinimalDbSettings.SPArrayOfTuplesAndDict(paramDefs, parameters); var sqlParam = MinimalDbSettings.SQLParamLessDynamic(spName, sqlParameter); var result = await _context.PlainTextModel .FromSql(sqlParam) .ToListAsync(); return result.FirstOrDefault();}Method for fetching of images data:
public async Task<ImageModel?> ImageAsync(string spName, Dictionary<string, object?> parameters){ var paramDefs = SQLServerInnerHelper.GlobalMethodSPParam(); SqlParameter[] sqlParameter = MinimalDbSettings.SPArrayOfTuplesAndDict(paramDefs, parameters); var sqlParam = MinimalDbSettings.SQLParamLessDynamic(spName, sqlParameter); var result = await _context.ImageModel .FromSql(sqlParam) .ToListAsync(); return result.FirstOrDefault();}Global method for parameter definition (for flexible parameter fetching from a Razor page):
public static Dictionary<string, object?> GlobalMethodRazorPageParam( object? id = null, object? key = null, object? spOutput = null) // for OUTPUT parameter, but I am not using it at the moment.{ var pageParam = new Dictionary<string, object?> { ["id"] = id, ["key"] = key, ["sp_output"] = spOutput }; return pageParam;}Global method for parameter definition (for flexible SQL stored procedure parameters):
public static (string Name, SqlDbType Type, int? Size)[] GlobalMethodSPParam(){ var paramDefs = new (string Name, SqlDbType Type, int? Size)[] { ("id", SqlDbType.VarChar, 50), ("key", SqlDbType.VarChar, 100), ("sp_output", SqlDbType.NVarChar, 100), }; return paramDefs;}Global method for SqlParameter[] definition:
public static SqlParameter[] SPArrayOfTuplesAndDict((string Name, SqlDbType Type, int? Size)[] paramDefs, Dictionary<string, object?> parameters){ var paramUpdatedValues = parameters.Values.ToList(); return [.. paramDefs .Select((def, i) => { var param = def.Size is null ? new SqlParameter(def.Name, def.Type) : new SqlParameter(def.Name, def.Type, def.Size.Value); if (def.Name == parameters.Keys.ToList()[i]) { param.Value = paramUpdatedValues[i] ?? DBNull.Value; } else { throw new ArgumentException("Parameters misconfiguration found."); } return param; })];}Global method for FormattableString definition:
public static FormattableString SQLParamLessDynamic(string spName, SqlParameter[] sqlParameters){ return sqlParameters.Length switch { // 0 => $"EXEC {storedProcedure}", // this isn't allowed here. 1 => $"EXEC {spName} {sqlParameters[0]}", 2 => $"EXEC {spName} {sqlParameters[0]}, {sqlParameters[1]}", 3 => $"EXEC {spName} {sqlParameters[0]}, {sqlParameters[1]}, {sqlParameters[2]}", _ => throw new ArgumentException("Too many parameters provided."), };}Backend/Frontend
Razor page:
@page "/users/review-user"@page "/users/review/{Id}/user"@rendermode InteractiveServer@using Microsoft.EntityFrameworkCore@using MyProject.Models.SQLServer@using MyProject.Data.SQLServer@implements IAsyncDisposable@inject IDbContextFactory<MyProject.Data.SQLServer.SQLServerContext> DbFactory@inject NavigationManager NavigationManager<PageTitle>Details</PageTitle><h3>Review User</h3><div><hr /> @if (plainTextModel is null) {<div class="position-absolute top-50 start-50 translate-middle"><div class="spinner-grow text-danger spinner-grow-xxl" role="status"><span class="visually-hidden">Loading...</span></div></div> } else {<div class="container-fluid overflow-x-auto mw-40"><div class="row py-1 border rounded border-secondary-subtle text-center my-1"><!-- Left side: user info --><div class="col-6 d-flex flex-column justify-content-center align-items-center"><dl class="row m-0"><dt class="col-sm-3 text-start p-0">#</dt><dd class="col-sm-9">@rowNum</dd><dt class="col-sm-3 text-start p-0">Role</dt><dd class="col-sm-9">@plainTextModel.Role</dd><dt class="col-sm-3 text-start p-0">Display Name</dt><dd class="col-sm-9">@plainTextModel.Name</dd><dt class="col-sm-3 text-start p-0">Remarks</dt><dd class="col-sm-9">@plainTextModel.Remarks</dd><dt class="col-sm-3 text-start p-0">Date Registered</dt><dd class="col-sm-9">@plainTextModel.DtRegistered</dd><dt class="col-sm-3 text-start p-0">Date Modified</dt><dd class="col-sm-9">@plainTextModel.DtModified</dd><dt class="col-sm-3 text-start p-0">Verified By</dt><dd class="col-sm-9">@plainTextModel.VerifiedBy</dd></dl></div><!-- Right side: actions --><div class="col-6 d-flex flex-column justify-content-center align-items-center gap-3 py-3"> @* other code here... *@</div></div> @if (imageModel is null) {<div class="position-absolute top-50 start-50 translate-middle"><div class="spinner-grow text-danger spinner-grow-xxl" role="status"><span class="visually-hidden">Loading...</span></div></div> } else {<div class="container-fluid p-0"><div class="row gap-2"><div class="col"><div class="row py-1 border rounded border-secondary-subtle text-center my-1"><dl class="row d-flex justify-content-center gap-2"><dt class="col-sm-1 text-start">Image 1</dt><dd class="col-sm-10 m-0"> @if (image1 is not null) {<a href="@image1" target="_blank"><img src="@image1" class="img-thumbnail my-3 p-0 w-50 h-auto" alt="Front ID" /></a> } else {<span class="text-muted fw-bold">-</span> }</dd></dl></div></div><div class="col"><div class="row py-1 border rounded border-secondary-subtle text-center my-1"><dl class="row d-flex justify-content-center gap-2"><dt class="col-sm-1 text-start">Image 2</dt><dd class="col-sm-10 m-0"> @if (image2 is not null) {<a href="@image2" target="_blank"><img src="@image2" class="img-thumbnail my-3 p-0 w-50 h-auto" alt="Back ID" /></a> } else {<span class="text-muted fw-bold">-</span> }</dd></dl></div></div><div class="col"><div class="row py-1 border rounded border-secondary-subtle text-center my-1"><dl class="row d-flex justify-content-center gap-2"><dt class="col-sm-1 text-start">Image 3</dt><dd class="col-sm-10 m-0"> @if (image3 is not null) {<a href="@image3" target="_blank"><img src="@image3" class="img-thumbnail my-3 p-0 w-50 h-auto" alt="Selfie" /></a> } else {<span class="text-muted fw-bold">-</span> }</dd></dl></div></div></div></div> }</div> }</div>@code { private PlainTextModel? plainTextModel = new(); private ImageModel? imageModel = new(); private SQLServerContext context = default!; [SupplyParameterFromQuery(Name = "row")] private long rowNum { get; set; } [Parameter] public string? Id { get; set; } private string? image1; private string? image2; private string? image3; protected override async Task OnInitializedAsync() { context = DbFactory.CreateDbContext(); await LoadPlainText(); await LoadImages(); } private async Task LoadPlainText() { var sQLServerHelper = new SQLServerHelper(context); var spParamPlainText = SQLServerInnerHelper.GlobalMethodRazorPageParam(id: Id, key:"REVIEW_USER_PLAINTEXT"); plainTextModel = await sQLServerHelper.PlainTextAsync("myStoredProcedure", spParamPlainText); // TODO: Let this load first. if (plainTextModel is null) { NavigationManager.NavigateTo("notfound"); } } private async Task LoadImages() { var sQLServerHelper = new SQLServerHelper(context); var spParamImage = SQLServerInnerHelper.GlobalMethodRazorPageParam(id: Id, key:"REVIEW_USER_IMAGES"); imageModel = await sQLServerHelper.ImageAsync("myStoredProcedure", spParamImage); if (imageModel is null) { NavigationManager.NavigateTo("notfound"); } image1 = dbDataToImg(imageModel?.Image1); image2 = dbDataToImg(imageModel?.Image2); image3 = dbDataToImg(imageModel?.Image3); } private string? dbDataToImg(byte[]? imgBytes) { if (imgBytes is null || imgBytes.Length == 0) { return null; } else { return $"data:image/png;base64,{Convert.ToBase64String(imgBytes)}"; } } public async ValueTask DisposeAsync() // this is important { await context.DisposeAsync(); }}I was expecting the plaintext to be rendered first before the images, but it wasn't.