Search code examples
c#onclickblazorweb-component

Button-Markupstring in Blazor (c#) from CodeBehind does not trigger onclick-event


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


Solution

  • 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

    More Advanced Scenarios

    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:

    1. Look at what the Razor Compiler builds. Add:
          <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    

    to the project file and then go searching in /obj/debug to find the generated files.

    1. Look at the Input code files here https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Forms and QuickGrid code here https://github.com/dotnet/aspnetcore/tree/main/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src.