asp.net.netasp.net-corerazorrazor-pages

How to pass js function argument to inline c# in cshtml?


Preface: I have a static formatting function in my backend, which takes an int Id and returns string SerialNumber. I want to keep it in on place. In one of the Views - I am showing a column of Serial Number. This column is sortable (by sql query), so the content of the column should be Id value, so I am modifying the displayed value with js to convert Id to Serial.

So, I got this script in my .cshtml View:

<script>
    function formatToSerial(id) {
        return @MyFormater.GetFormatedSerial(int.Parse(id));
    }
</script>

Problem: argument "id" is not recognized in this context.

Question: is there a nice built-in way to pass the js function argument to inline c# call? Or should I stick with duplication string formatting function separately for js?

What I did tried so far: I was looking into making the formatting JSInvokable and use it that way. But If I understood correctly - this is Blazor functionality, which I am not using in this project.


Solution

  • Problem: argument "id" is not recognized in this context.

    In the code sample in your question, the C# code @MyFormater.GetFormatedSerial(int.Parse(id)); executes on the server when the Razor Page view is requested.

    The id argument in your view is declared as a JavaScript variable that will be processed on the client through your JS function formatToSerial(id).

    The error "argument id is not recognized in this context" is the result of the Razor View engine on the server trying to process the unknown/undeclared id variable the engine sees in the C# code int.Parse(id).

    Question: Is there a nice built-in way to pass the JS function argument to inline C# call?

    The answer to this question is no, not for Razor Pages (or a Razor View) out-of-the-box. That is, the default solution template for an ASP.NET Core Web App (Razor Pages) in Visual Studio does not provide a mechanism for JavaScript on the client to interoperate with a C# function on the server.

    JavaScript/C# interoperability

    There are two ways to create this interoperability in a Razor Pages project (or a MVC project) in order to leverage your server's serial formatting implementation from the client:

    fetch() and a Web API Razor Component Integration
    Make a post request using the fetch() method in JavaScript with one or more id values in its payload. The URL path in the fetch() method is set to a Web API controller route.

    The API controller endpoint can execute MyFormater.GetFormatedSerial(), return the results to the Promise methods of the fetch() method, and the Promise method can update your UI with the response results.
    Integrate Razor components into your ASP.NET Razor Pages app.

    There are two ways to use Razor component integration that enable you to leverage your server's serial formatting implementation:
    1. Call .NET methods from JavaScript functions in ASP.NET Core Blazor.
    2. Use non-routable components in pages or views.

    Should I stick with duplication string formatting function separately for JS?

    The solutions summarized in the table above – and expanded upon in the samples below – each come with the usual trade-offs:

    Two separate functions

    One function with post to Web API

    One function with Razor component interop

    fetch() and a Web API

    fetch() sample

    The following initiates a post request using the fetch() method in JavaScript with one id value in its payload.

    function formatToSerial(id) {
    
        // For demo purposes, add multiple IDs in the 'data' array
        const data = [id, 123, 456];
    
        const csrfToken = document.querySelector('input[name="__RequestVerificationToken"]').value;
    
        const url = "api/SerialFormat";
    
        fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'RequestVerificationToken': csrfToken
            },
            body: JSON.stringify(data)
        })
            .then(response => response.json())
            .then(responseData => {
                if (!responseData.serializedList || !Array.isArray(responseData.serializedList)) {
                    document.querySelector("#web-api-result").textContent = "Empty serialized list";
                    return;
                }
                let kvpConcat = "";
                const numOfEntries= responseData.serializedList.length;
                responseData.serializedList.forEach((kvp, index) => {
                    const sep = index === numOfEntries -1? "": ", ";
                    kvpConcat += kvp.key + "/" + kvp.value + sep;
                });
                document.querySelector("#web-api-result").textContent = kvpConcat;
            })
            .catch(error => {
                console.error(error);
            });
    }
    

    Web API sample

    The Web API controller endpoint that processes the post request executes the MyFormater.GetFormatedSerial() method. The endpoint returns the results to the client and fulfills the fetch() function's Promise.

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    
    namespace WebApplication1.Controllers
    {
        [Route("api/[controller]")]
        [ApiController]
        [ValidateAntiForgeryToken]
        public class SerialFormatController : Controller
        {
            [HttpPost]
            [AllowAnonymous]
            public JsonResult Post([FromBody] List<int?> idList)
            {
                List<KeyValuePair<string?, string?>> kvList = [];
                foreach (int? id in idList)
                {
                    kvList.Add(new KeyValuePair<string?, string?>(
                        key: id?.ToString(),
                        value: MyFormater.GetFormatedSerial(id))
                    );
                }
    
                return new JsonResult(new
                {
                    Success = true,
                    SerializedList = kvList
                });
            }
        }
    }
    

    Razor component integration

    Add a Razor component containing a method defined with the [JSInvokable] attribute in an @code{ } block. The MyFormater.GetFormatedSerial() method is executed from the [JSInvokable] method.

    NOTE: If your adding the infrastructure for Razor components into your Razor Pages project, then it's assumed there are multiple instances in your project that leverage the purpose and benefits of Razor components versus the simple sample listed below.

    Program.cs

    Follow steps under the heading Use non-routable components in pages or views in the ASP.NET documentation to integrate Razor components into your Razor Pages project.

    // https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration?view=aspnetcore-8.0#use-non-routable-components-in-pages-or-views
    using WebApplication1.Components;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
    builder.Services.AddScoped<WebApplication2.FormaterService>();
    
    // https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration?view=aspnetcore-8.0#use-non-routable-components-in-pages-or-views
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    // https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration?view=aspnetcore-8.0#use-non-routable-components-in-pages-or-views
    app.UseAntiforgery();
    
    app.MapRazorPages();
    
    // https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration?view=aspnetcore-8.0#use-non-routable-components-in-pages-or-views
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode();
    
    // Add 'endpoints.MapControllers()' to enable Web APIs
    app.MapControllers();
    
    app.Run();
    

    Components/CallDotNet.razor

    Follow the steps under the heading Invoke a static .NET method.

    @implements IAsyncDisposable
    @inject IJSRuntime JS
    
    @code {
        private IJSObjectReference? module;
    
        protected async override Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                module = await JS.InvokeAsync<IJSObjectReference>("import",
                    "./Components/CallDotNet.razor.js");
    
                await module.InvokeVoidAsync("addHandlers");
            }
        }
    
        [JSInvokable]
        public static async Task<string?> GetFormattedSerial(string? id)
        {
            if (int.TryParse(id, out int result))
            {
                return await Task.FromResult(MyFormater.GetFormatedSerial(result));
            }
    
            string? message = "An ID was not entered.";
            return await Task.FromResult(message);
        }
    
        async ValueTask IAsyncDisposable.DisposeAsync()
        {
            if (module is not null)
            {
                await module.DisposeAsync();
            }
        }
    }
    

    Components/CallDotNet.razor.js

    // Source:
    // Call .NET methods from JavaScript functions in ASP.NET Core Blazor
    // https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-8.0
    
    export function returnArrayAsync() {
        const id = document.querySelector("#id-input").value;
        DotNet.invokeMethodAsync('WebApplication1', 'GetFormattedSerial', id)
            .then(data => {
                document.querySelector("#dot-net-result").textContent = data;
            });
    }
    
    export function addHandlers() {
        const btn = document.getElementById("btn");
        btn.addEventListener("click", returnArrayAsync);
    }
    

    Razor view sample

    @page
    @using WebApplication1.Components
    @model IndexModel
    @{
        ViewData["Title"] = "Home page";
    }
    @section Styles {
        <style>
            .result {
                font-weight: 600;
            }
        </style>
    }
    
    <h5>Use fetch() and a Web API</h5>
    <input id="serial-id" type="number" />
    <button id="get-serial-id">Format</button>
    <p>
        <span>Result: <span class="result" id="web-api-result"></span></span>
    </p>
    
    <hr />
    
    <h5>Call .NET method from JavaScript</h5>
    <input type="number" id="id-input" />
    <button id="btn">Format</button>
    <p>
        Result: <span class="result" id="dot-net-result"></span>
    </p>
    <component type="typeof(CallDotNet)" render-mode="ServerPrerendered" />
    
    <hr />
    
    <h5>Razor Component UI</h5>
    <component type="typeof(EmbeddedFormatSerial)" render-mode="ServerPrerendered" />
    
    @Html.AntiForgeryToken()
    
    @section Scripts {
        <script src="/js/format-serial.js"></script>
    }