Category: Blazor

Poor man display errors–part 3–Display

In part 2 I have exposed the errors via the API to the world. Now I should show in a Blazor/ Razor page . It is enough to read with an HTTP call and display data.

Display data in a table

Because
– the API that gives error can change , however it will be an array of objects
– I do not want to use a custom grid

I took the decision to create the display of errors as a table that has dynamic columns based on the array .

This is the code for javascript to create a table from an array ( it assumes that a div with id = mytable exists)

var table= null;
function displayData(a){
console.log(a);
arrayToTable(JSON.parse(a));
};

function removeTableCreated(){
var parent =document.getElementById("myTable");
while (parent.lastChild) {
parent.removeChild(parent.lastChild);
}
}

function arrayToTable(data) {
removeTableCreated();
if((!Array.isArray(data)) || data.length == 0){
window.alert(‘ no data ‘);
return;
}

// Create table element
table = document.createElement(‘table’);
table.border = ‘1’;

// Create table header row
const headerRow = document.createElement(‘tr’);
Object.keys(data[0]).forEach(key => {
const th = document.createElement(‘th’);
th.textContent = key;
headerRow.appendChild(th);
});
table.appendChild(headerRow);

// Create table rows
data.forEach(item => {
const row = document.createElement(‘tr’);
Object.values(item).forEach(value => {
const td = document.createElement(‘td’);
td.textContent = value;
row.appendChild(td);
});
table.appendChild(row);
});

// Append table to the body (or any other container)
//document.body.appendChild(table);
document.getElementById("myTable").appendChild(table);
}

Obtain Data

Now the blazor razor page is simple

<h3>PoorManErrors</h3>
<FluentButton Appearance="Appearance.Accent" @onclick="async()=> await GenerateError()">Generate Error</FluentButton>

<FluentButton Appearance="Appearance.Accent" @onclick="async()=> await SeeErrors()">Refresh</FluentButton>

<FluentButton Appearance="Appearance.Accent" @onclick="async()=> await ClearErrors()">Clear</FluentButton>
<div id="myTable"></div>

And the code for retrieving

    [Inject(Key = Program.apiNameConfig)]
    private HttpClient? httpClient { get; set; }
    [Inject]
    private IJSRuntime? JSRuntimeData { get; set; }

    private async Task GenerateError()
    {

        ArgumentNullException.ThrowIfNull(httpClient);
        var r = new ReadFromAPI(httpClient);

        var res = await r.ReadDataGet("api/usefull/error/WithILogger");

        await SeeErrors();
        StateHasChanged();

    }
    protected override async Task OnInitializedAsync()
    {
        await SeeErrors();
    }
    private async Task ClearErrors()
    {
        ArgumentNullException.ThrowIfNull(httpClient);
        var r = new ReadFromAPI(httpClient);
        var res = await r.ReadDataGet("nlog/Memory/stringData/clear");
        await SeeErrors();
    }
    private async Task SeeErrors()
    {
        ArgumentNullException.ThrowIfNull(httpClient);
        var r = new ReadFromAPI(httpClient);


        var res = await r.ReadDataGet("nlog/Memory/stringData/list");
        if (string.IsNullOrEmpty(res))
        {
            return;
        }
        ;
        if (res != null)
        {
            ArgumentNullException.ThrowIfNull(JSRuntimeData);
            await JSRuntimeData.InvokeVoidAsync("displayData", res);

        }
    }

Side note 1 : the button GenerateErrors take advantage of package NetCoreUsefullEndpoints that registers an endpoint that logs an error.

Side note 2: See MainLayout for tag ErrorContent . You may wanto to modify this one too, to go to the error page

Side note 3: You may want to add a “copy to clipboard” button or “send email”

That’s all, folks!

Blazor and RowNumber in Grid

The data that comes from the backend does not, usually, provide a row number . So how to obtain this ? Create a class, will say any programmer …

public  record DataWithNumber<T>(int number, T data) 
    where T: class
{
}

And the usage

 var data = HttpClient_WebApi.GetFromJsonAsAsyncEnumerable<Order_Details_Extended>(url);
 ArgumentNullException.ThrowIfNull(data);
 int i = 0;

 await foreach (var item in data)
 {
     if (item == null) continue;
     i++;
     dataArr.Add(new DataWithNumber<Order_Details_Extended>(i, item));
     if ((i < 500 && i % 100 == 0) || (i > 500 && i % 1000 == 0))
     {
         Console.WriteLine($"i={i}");
         nrRecordsLoaded = i;

         await InvokeAsync(StateHasChanged);
         await Task.Delay(1000);
     }
 }
 nrRecordsLoaded = i;

And the grid:

<FluentDataGrid Items="@dataForQuery" Virtualize="true" GenerateHeader="GenerateHeaderOption.Sticky">
    <PropertyColumn Property="@(p => p.number)" Sortable="true" />
    <PropertyColumn Property="@(p => p.data.OrderID)" Sortable="true">
        <ColumnOptions>
            <div class="search-box">
                <FluentSearch type="search" Autofocus=true @bind-Value=nameOrderIdFilter @oninput="HandleOrderIdFilter" @bind-Value:after="HandleClearIdFilter" Placeholder="Order Id..." />
            </div>
        </ColumnOptions>
    </PropertyColumn>
    
    <PropertyColumn Property="@(p => p.data.UnitPrice)" Sortable="true" />
    <PropertyColumn Property="@(p => p.data.ExtendedPrice)" Sortable="true" />
    <PropertyColumn Property="@(p => p.data.ProductID)" Sortable="true" />
    <PropertyColumn Property="@(p => p.data.Quantity)" Sortable="true" />
    <PropertyColumn Property="@(p => p.data.Discount)" Sortable="true" />
</FluentDataGrid>

Aspire Blazor WebAssembly and WebAPI

 

Aspire is the new visualizer – see https://github.com/dotnet/aspire

I am very fond of WebAPI  –  it allows for all people to see the functionality of a site , in a programmatic way ( side note: , my nuget package, https://www.nuget.org/packages/NetCore2Blockly , allows to make workflows from your WebAPI)

And Blazor WebAssembly is a nice addition that the WebAPI . I am talking about Interactive WebAssembly (https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?preserve-view=true&view=aspnetcore-8.0  )  . I do want ( for the moment ) to use Interactive Server because

  1. it is easy to forget to add functionality to the WebAPI
  2. it is not separating UI from BL

So I decided to add an Blazor WebAssembly and WebAPI into Aspire to see how they work together.

The first problem that  I have is how to transmit the WebAPI URL to the Blazor WebAssembly . Think that is not Interactive Server or Auto – in order to have the environment or configuration . Blazor Interactive WebAssembly  are just static files that are downloaded to the client. And they are executed in the browser.

But I have tried with adding to the Environment in usual way

builder.AddProject<projects.exampleblazorapp>(nameof(Projects.ExampleBlazorApp))
.WithEnvironment(ctx =&gt;
{
if (api.Resource.TryGetAllocatedEndPoints(out var end))
{
if (end.Any())
	ctx.EnvironmentVariables["HOSTAPI"] = end.First().UriString;
}

 

And no use!

After reading ASP.NET Core Blazor configuration | Microsoft Learn  and aspire/src/Microsoft.Extensions.ServiceDiscovery at main · dotnet/aspire (github.com) and API review for Service Discovery · Issue #789 · dotnet/aspire (github.com) I realized that the ONLY way is to put in wwwroot/appsettings.json

So I came with the following code that tries to write DIRECTLY to wwwroot/appsettings.json file


namespace Aspire.Hosting;
public static class BlazorWebAssemblyProjectExtensions
{
    public static IResourceBuilder<ProjectResource> AddWebAssemblyProject<TProject>(
        this IDistributedApplicationBuilder builder, string name,
        IResourceBuilder<ProjectResource> api) 
        where TProject : IServiceMetadata, new()
    {
        var projectbuilder = builder.AddProject<TProject>(name);
        var p=new TProject();
        string hostApi= p.ProjectPath;
        var dir = Path.GetDirectoryName(hostApi);
        ArgumentNullException.ThrowIfNull(dir);
        var wwwroot = Path.Combine(dir, "wwwroot");
        if (!Directory.Exists(wwwroot)) {
            Directory.CreateDirectory(wwwroot);
        }
        var file = Path.Combine(wwwroot, "appsettings.json");
        if (!File.Exists(file))
            File.WriteAllText(file, "{}");
        projectbuilder =projectbuilder.WithEnvironment(ctx =>
        {
            if (api.Resource.TryGetAllocatedEndPoints(out var end))
            {
                if (end.Any())
                {
                    
                    var fileContent = File.ReadAllText(file);

                    Dictionary<string, object>? dict;
                    if (!string.IsNullOrWhiteSpace(fileContent))
                        dict = new Dictionary<string, object>();
                    else
                        dict = JsonSerializer.Deserialize<Dictionary<string,object>>(fileContent!);

                    ArgumentNullException.ThrowIfNull(dict);
                    dict["HOSTAPI"] = end.First().UriString;                    
                    JsonSerializerOptions opt = new JsonSerializerOptions(JsonSerializerOptions.Default)
                            { WriteIndented=true};
                    File.WriteAllText(file,JsonSerializer.Serialize(dict,opt));
                    ctx.EnvironmentVariables["HOSTAPI"]=end.First().UriString;
                    
                }
                    
            }

        });
        return projectbuilder;

    }
}

And in Aspire

var api = builder.AddProject<Projects.ExampleWebAPI>(nameof(Projects.ExampleWebAPI));
builder.AddWebAssemblyProject<Projects.ExampleBlazorApp>(nameof(Projects.ExampleBlazorApp), api);

And in Blazor Interactive WebAssembly


var hostApi = builder.Configuration["HOSTAPI"];
if (string.IsNullOrEmpty(hostApi))
{
    hostApi = builder.HostEnvironment.BaseAddress;
    var dict = new Dictionary<string, string?> { { "HOSTAPI", hostApi } };
    builder.Configuration.AddInMemoryCollection(dict.ToArray());
}

builder.Services.AddKeyedScoped("db",(sp,_) => new HttpClient { BaseAddress = new Uri(hostApi) });

What about deploying the code to production ? Well, I think that is better to wrote yourself to wwwroot/appsettings.json and remove the data . But I will try to deploy and let you know….

Deploy Blazor WASM to Github Pages in 7 steps

Assumptions:

You have an Blazor Interactive WebAssembly ( CSR ) , not a Server ( static or interactive)

I will make as the repo is  https://github.com/ignatandrei/tilt  . Change my name with yours and TILT  with your repo

So let’s start

Step 1   You must configure GitHub Pages – create a docs folder and put an index.html  . Then  goto github Settings => Pages (https://github.com/ignatandrei/tilt/settings/pages )  and put there main / docs  . 

Step 2  Verify it is working. If your repo is https://github.com/ignatandrei/tilt , then browse to https://ignatandrei.github.io/TILT/ and ensure that you can see the index. If not, goto Step 1

Step 3  Add 2 files .nojekyll (content : null , empty …. just create it ) and .gitattributes ( content : *.js binary )

Step 4 dotnet publish your Blazor WASM csproj . Find the folder wwwroot where was published.

Step  5  Find index.html  in the folder . Edit base href , put the repo name  (<base href=”/TILT/” />  ). Also you can modify the css/js  by adding the date ( e.g.  <link rel=”stylesheet” href=”css/app.css?202312162300″ /> ) .

Step 6  Copy the index.html and the other files inside the docs folder . Also, copy the index.html as 404.html file

Step 7 Commit and push. Now you can enjoy your Blazor site hosted for free in github  : https://github.com/ignatandrei/tilt 

Note 1 : If some url’s do not work , then try to add the following

@inject IWebAssemblyHostEnvironment HostEnvironment
@{
     var baseAddress = HostEnvironment.BaseAddress;
     if (!baseAddress.EndsWith(“/”)) baseAddress += “/”;

}

to the url

Note 2:  For more deployments please read https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-8.0

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.