I'm using a Blazor WebApp with C# and .net 6. In this example I'm using the standard project "Blazor Server App" (VS 2022) for testing.
I built a own "table helper", where I can pass a dataset in the codebehind which is displayed on the page with a single command. The full file is here: Table.cs
using System.Text;
using Microsoft.AspNetCore.Components;
namespace MarkusH.Modulxo.Webclient.Components
{
public class Table
{
private Columnset _columnset = null;
private Dataset _dataset = null;
private List<string> _classes = new List<string>();
private string _linkNew = "";
private string _linkEdit = "";
public Table()
{
AddClasses("table", "table-striped");
}
public void SetColumnset(Columnset columnset)
{
_columnset = columnset;
}
public void SetDataset(Dataset dataset, string orderByAttributeName = "")
{
_dataset = dataset;
if (!string.IsNullOrWhiteSpace(orderByAttributeName))
{
SortRecordsBy(orderByAttributeName);
}
}
public void SortRecordsBy(string orderByAttributeName)
{
_dataset?.OrderBy(orderByAttributeName);
}
public void SetLinkNew(string linkNew)
{
_linkNew = linkNew;
}
public void SetLinkEdit(string linkEdit)
{
_linkEdit = linkEdit;
}
public void AddClasses(params string[] classes)
{
_classes.AddRange(classes);
}
public MarkupString ShowComponent()
{
var stringBuilder = new StringBuilder();
if (_columnset != null)
{
stringBuilder.Append(BuildTableHeader());
stringBuilder.Append(BuildTableBody());
stringBuilder.Append(BuildTableFooter());
}
return (MarkupString)stringBuilder.ToString();
}
private string BuildTableHeader()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"<table class='{string.Join(" ", _classes)}'>");
stringBuilder.AppendLine("<thead><tr>");
foreach (var column in _columnset.Columns)
{
stringBuilder.Append($"<th @onclick='() => SortRecordsBy({column.AttributeName})'>{column.Header}</th>");
}
stringBuilder.Append("<th>");
if (!string.IsNullOrEmpty(_linkNew))
{
stringBuilder.Append($"<a href='{_linkNew}'>New</a>");
}
stringBuilder.Append("</th>");
stringBuilder.AppendLine("</tr></thead>");
return stringBuilder.ToString();
}
private string BuildTableBody()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("<tbody>");
if (_dataset != null && _dataset.Records.Count > 0)
{
stringBuilder.Append(BuildTableBodyFromData());
}
else
{
stringBuilder.Append(BuildTableBodyNoRecords());
}
stringBuilder.AppendLine("</tbody>");
return stringBuilder.ToString();
}
private string BuildTableBodyFromData()
{
var stringBuilder = new StringBuilder();
foreach (var record in _dataset.Records)
{
stringBuilder.AppendLine("<tr>");
foreach (var column in _columnset.Columns)
{
stringBuilder.Append($"<td>{record.GetAttributeValue<object>(column.AttributeName).ToString()}</td>");
}
stringBuilder.Append("<td>");
if (!string.IsNullOrEmpty(_linkEdit))
{
stringBuilder.Append($"<a href='{_linkEdit.Replace("%id%", record.Id.ToString())}'>Edit</a>");
}
stringBuilder.Append("</td>");
stringBuilder.AppendLine("</tr>");
}
return stringBuilder.ToString();
}
private string BuildTableBodyNoRecords()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("<tr>");
stringBuilder.Append($"<td colspan='{_columnset.Columns.Count + 2}'>No data</td>");
stringBuilder.AppendLine("</tr>");
return stringBuilder.ToString();
}
private string BuildTableFooter()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("</table>");
return stringBuilder.ToString();
}
}
}
The implementation in codebehind looks like: Countrylist.razor.cs
...
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await addressbookService.InitMoxClient();
if (ForwardToLoginPageIfNotAuthenticated(addressbookService)) { return; }
var countries = addressbookService.GetCountryList();
_countryTable.SetDataset(new Dataset(new List<MoxBaseRecord>(countries)), "Countryname");
StateHasChanged();
}
}
...
As you can see, the table headers should sort the data within - but the onclick-function-call is not rendered correctly.
I found multiple topics addressing more or less the same problem, but the most hints I found were to loop trough the objects on the page instead of the code-behind - but I want to build a generic component to use on multiple pages - so I couldn't figerout how to achive the same result.
It would be great if someone can provide a hint/solution to solve the issue.
Thanks and regards
Markus
You can't do this using strings. The Razor Compiler doesn't work that way.
You need to return RenderFragments. Below is some example code based on your question that demonstrates how you can mix html markup and C# code in Razor components.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
@BuildTable
@code{
public class Column
{
public string? Header { get; set; }
public string? AttributeName { get; set; }
}
private List<Column> Columns = new();
private void SortRecordsBy(string? value) { }
private RenderFragment BuildTable => (__builder) =>
{
<table>
@BuildTableHeader
</table>
};
private RenderFragment BuildTableHeader => (__builder) =>
{
<thead>
@foreach(var column in Columns)
{
<th @onclick="() => SortRecordsBy(column.AttributeName)">
@column.Header
</th>
}
</thead>
};
private string CssBuilder(string value)
{
// see https://github.com/EdCharbeneau/BlazorComponentUtilities
return value;
}
}
On building Css see - https://github.com/EdCharbeneau/BlazorComponentUtilities
If you want to use code behind files and different components then you need to build your markup using the RenderTreeBuilder
.
There's a Microsoft article on the subject here: https://learn.microsoft.com/en-us/aspnet/core/blazor/advanced-scenarios?view=aspnetcore-8.0
Two ways to figure out how to write RenderTreeBuilder
code:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
to the project file and then go searching in /obj/debug to find the generated files.