LostBeard / SpawnDev.BlazorJS

Full Blazor WebAssembly and Javascript Interop with multithreading via WebWorkers
https://blazorjs.spawndev.com
MIT License
78 stars 6 forks source link

Could not find 'blazorCulture.get' ('blazorCulture' was undefined). #15

Closed dodyg closed 8 months ago

dodyg commented 8 months ago

I encounter this error on .NET 8 RC-1

Error: Could not find 'blazorCulture.get' ('blazorCulture' was undefined).

Program.cs

services.AddBlazorJSRuntime();
services.AddWebWorkerService();
services.AddSingleton<INotificationWorker, NotificationWorker>();

On Razor


    protected override async Task OnInitializedAsync()
    {
        var webWorker = await worker.GetWebWorker();
        webWorker!.OnMessage += (sender, msg) =>
        {
            Log("Hello world", LogType.Error);
        };

        var service = webWorker!.GetService<INotificationWorker>();
        await service!.DoAsync();
    }

The interface


public interface INotificationWorker
{
    Task DoAsync();
}

public class NotificationWorker : INotificationWorker
{
    public async Task DoAsync()
    {
        await Task.CompletedTask;
    }
}

On the project


    <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.2.10" />
dodyg commented 8 months ago

image

LostBeard commented 8 months ago

Thank you for reporting your issue.

You did not state what line of code causes the error. Does the error happen when you call GetWebWorker, or when you call a method (DoAsync here) on it?

The error is due to something else in your code that is not here. Please show your full index.html file and your Program.cs file from your Blazor WASM project. The full output from the console may also help.

You may be using Javascript script that is leading to the issue (not necessarily the cause.) I need the info requested so I can get an idea of what might be going on. Thank you.

LostBeard commented 8 months ago

I did a little checking and I believe you may be following the Microsoft docs example (or similar) Dynamically set the client-side culture by user preference

If that is the case there are ways around the issue depending on whether or not you need culture localization in the WebWorker also.

For example, if you do not need localization in the WebWorker you can simply add a condition statement around the culture checking code like below.

Unmodified example code from the MS Culture example

builder.Services.AddLocalization();
var host = builder.Build();
CultureInfo culture;
var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");
if (result != null)
{
    culture = new CultureInfo(result);
}
else
{
    culture = new CultureInfo("en-US");
    await js.InvokeVoidAsync("blazorCulture.set", "en-US");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

Modified example code to detect if the app is running in a Window and only then load the culture info.

builder.Services.AddLocalization();
var host = builder.Build();
var JS = host.Services.GetRequiredService<BlazorJSRuntime>();
if (JS.IsWindow)
{
    CultureInfo culture;
    var js = host.Services.GetRequiredService<IJSRuntime>();
    var result = await js.InvokeAsync<string>("blazorCulture.get");
    if (result != null)
    {
        culture = new CultureInfo(result);
    }
    else
    {
        culture = new CultureInfo("en-US");
        await js.InvokeVoidAsync("blazorCulture.set", "en-US");
    }
    CultureInfo.DefaultThreadCurrentCulture = culture;
    CultureInfo.DefaultThreadCurrentUICulture = culture;
}

Part of the problem with the code on that page when working with the WebWorker is that it assumes the code is running in a Window global scope. It even uses localStorage to store and retrieve the culture info but localStorage does not exist in a Worker global scope. See below example code from that page.

Unmodified Javascript code snippet from MS example

<script>
  window.blazorCulture = {
    get: () => window.localStorage['BlazorCulture'],
    set: (value) => window.localStorage['BlazorCulture'] = value
  };
</script>

If you do need culture info in your WebWorker you will likely need to store it someplace other than localStorage so the WebWorker can access it.

Hope this information helps. Good luck. I am here if you need additional help.

LostBeard commented 8 months ago

Solution 2
This solution enables culture support in WebWorkers.

Upgrade your SpawnDev.BlazorJS.WebWorkers reference to 2.2.11 and change your code to use either IndexedDB or Cache, instead of localStorage, like in the example below.

Again, from the MS example in your index.html

Change the below code that will not work in a WebWorker context

<script>
  window.blazorCulture = {
    get: () => window.localStorage['BlazorCulture'],
    set: (value) => window.localStorage['BlazorCulture'] = value
  };
</script>

To this code that will work in all contexts

<script webworker-enabled>
    blazorCulture = {
        get: async () => {
            var cache = await caches.open('BlazorCulture');
            var response = await cache.match('/culture');
            return response ? await response.text() : null;
        },
        set: async (value) => {
            var cache = await caches.open('BlazorCulture');
            await cache.put('/culture', new Response(value));
        }
    };
</script>

When loading a WebWorker, SpawnDev.BlazorJS.WebWorkers ignores all <script> tags in index.html except those marked with the attribute "webworker-enabled". Version 2.2.10 and below only supported remote scripts (src attribute.) I added support for inline scripts in version 2.2.11 so that this example would work.

dodyg commented 8 months ago

I did a little checking and I believe you may be following the Microsoft docs example (or similar) Dynamically set the client-side culture by user preference

Indeed

image

dodyg commented 8 months ago

I am trying solution 2


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Taly BNPL </title>
    <base href="/" />
    <link href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" rel="stylesheet">
    <link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
    <link href="_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css" rel="stylesheet" />
    <link href="_content/Blazorise.LoadingIndicator/blazorise.loadingindicator.css" rel="stylesheet" />
    <link href="_content/Blazorise.SpinKit/blazorise.spinkit.css" rel="stylesheet" />
    <link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">

    <link href="css/app.css" rel="stylesheet" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <link href="Taly.BNPL.Admin.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">
        <svg class="loading-progress">
            <circle r="40%" cx="50%" cy="50%" />
            <circle r="40%" cx="50%" cy="50%" />
        </svg>
        <div class="loading-progress-text"></div>
    </div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/chartjs-plugin-annotation@2.2.1"></script>
    <script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

    <script src="_framework/blazor.webassembly.js"></script>
    <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
    <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <script src="/js/app.js"></script>

    <script webworker-enabled>
        blazorCulture = {
            get: async () => {
                var cache = await caches.open('BlazorCulture');
                var response = await cache.match('/culture');
                return response ? await response.text() : null;
            },
            set: async (value) => {
                var cache = await caches.open('BlazorCulture');
                await cache.put('/culture', new Response(value));
            }
        };
    </script>

    <script>
        function loadBootstrapCss(direction) {
            var head = document.getElementsByTagName('head')[0]
            if (direction === 'ltr') {
                const fileref = document.createElement("link")
                fileref.setAttribute("rel", "stylesheet")
                fileref.setAttribute("type", "text/css")
                fileref.setAttribute("href", "https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css")
                head.append(fileref)
                const html = document.getElementsByTagName('html')[0]
                html.setAttribute('lang', 'en')
                html.setAttribute('dir', 'ltr')
            }
            else {
                const fileref = document.createElement("link")
                fileref.setAttribute("rel", "stylesheet")
                fileref.setAttribute("type", "text/css")
                fileref.setAttribute("href", "https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css")
                head.append(fileref);
                const html = document.getElementsByTagName('html')[0]
                html.setAttribute('lang', 'ar')
                html.setAttribute('dir', 'rtl')
            }
        }

        if (!window.blazorCulture.get() || window.blazorCulture.get() == 'en-US')
            loadBootstrapCss('ltr');
        else
            loadBootstrapCss('rtl');
    </script>
</body>

</html>

Program.cs

CultureInfo culture;
var js = host.Services.GetRequiredService<IJSRuntime>();
var result = await js.InvokeAsync<string>("blazorCulture.get");

if (result != null)
{
    culture = new CultureInfo(result);
}
else
{
    culture = new CultureInfo("en-US");
    await js.InvokeVoidAsync("blazorCulture.set", "en-US");
}

CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;

await host.BlazorJSRunAsync();

Project

    <PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.2.11" />

Error

VM6:3 Debugging hotkey: Shift+Alt+D (when application has focus)
invoke-js.ts:176 info: System.Net.Http.HttpClient.Default.ClientHandler[101]
      Received HTTP response headers after 2305.2ms - 200
invoke-js.ts:176 info: System.Net.Http.HttpClient.Default.LogicalHandler[101]
      End processing HTTP request after 2334ms - 200
VM6:3  Error: One or more errors occurred. (Could not find 'blazorCulture.get' ('blazorCulture' was undefined).
Error: Could not find 'blazorCulture.get' ('blazorCulture' was undefined).
    at eval (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:368)
    at Array.forEach (<anonymous>)
    at l.findFunction (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:336)
    at w (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:5079)
    at eval (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:2872)
    at new Promise (<anonymous>)
    at y.beginInvokeJSFromDotNet (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:2835)
    at Object.nn [as invokeJSJson] (eval at initScriptElement (spawndev.blazorjs.webworkers.faux-env.js:236:26), <anonymous>:3:53726)
    at invoke-js.ts:233:31
    at El (invoke-js.ts:276:5))
    at Jn (marshal-to-js.ts:349:18)
    at El (marshal-to-js.ts:306:28)
    at 00b1e17a:0x1fac9
    at 00b1e17a:0x1bf8a
    at 00b1e17a:0xf171
    at 00b1e17a:0x1e7e3
    at 00b1e17a:0x1efd9
    at 00b1e17a:0xcfeb
    at 00b1e17a:0x440a0
    at e.<computed> (cwraps.ts:338:24)
callEntryPoint @ VM6:3
await in callEntryPoint (async)
Qt @ VM6:3
await in Qt (async)
ln @ VM6:3
initWebWorkerBlazor @ spawndev.blazorjs.webworkers.js?verbose=false:225
await in initWebWorkerBlazor (async)
(anonymous) @ spawndev.blazorjs.webworkers.js?verbose=false:232

image

dodyg commented 8 months ago

Solution 1 generates no errors

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Taly BNPL </title>
    <base href="/" />
    <link href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" rel="stylesheet">
    <link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
    <link href="_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css" rel="stylesheet" />
    <link href="_content/Blazorise.LoadingIndicator/blazorise.loadingindicator.css" rel="stylesheet" />
    <link href="_content/Blazorise.SpinKit/blazorise.spinkit.css" rel="stylesheet" />
    <link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">

    <link href="css/app.css" rel="stylesheet" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <link href="Taly.BNPL.Admin.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">
        <svg class="loading-progress">
            <circle r="40%" cx="50%" cy="50%" />
            <circle r="40%" cx="50%" cy="50%" />
        </svg>
        <div class="loading-progress-text"></div>
    </div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/chartjs-plugin-annotation@2.2.1"></script>
    <script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

    <script src="_framework/blazor.webassembly.js"></script>
    <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
    <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    <script src="/js/app.js"></script>
    <script>
        window.blazorCulture = {
            get: () => window.localStorage['BlazorCulture'],
            set: (value) => window.localStorage['BlazorCulture'] = value
        };

        function loadBootstrapCss(direction) {
            var head = document.getElementsByTagName('head')[0]
            if (direction === 'ltr') {
                const fileref = document.createElement("link")
                fileref.setAttribute("rel", "stylesheet")
                fileref.setAttribute("type", "text/css")
                fileref.setAttribute("href", "https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css")
                head.append(fileref)
                const html = document.getElementsByTagName('html')[0]
                html.setAttribute('lang', 'en')
                html.setAttribute('dir', 'ltr')
            }
            else {
                const fileref = document.createElement("link")
                fileref.setAttribute("rel", "stylesheet")
                fileref.setAttribute("type", "text/css")
                fileref.setAttribute("href", "https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css")
                head.append(fileref);
                const html = document.getElementsByTagName('html')[0]
                html.setAttribute('lang', 'ar')
                html.setAttribute('dir', 'rtl')
            }
        }

        if (!window.blazorCulture.get() || window.blazorCulture.get() == 'en-US')
            loadBootstrapCss('ltr');
        else
            loadBootstrapCss('rtl');
    </script>
</body>

</html>

Program.cs


var host = builder.Build();

var blazorJs = host.Services.GetRequiredService<BlazorJSRuntime>();

if (blazorJs.IsWindow)
{
    CultureInfo culture;
    var js = host.Services.GetRequiredService<IJSRuntime>();
    var result = await js.InvokeAsync<string>("blazorCulture.get");

    if (result != null)
    {
        culture = new CultureInfo(result);
    }
    else
    {
        culture = new CultureInfo("en-US");
        await js.InvokeVoidAsync("blazorCulture.set", "en-US");
    }

    CultureInfo.DefaultThreadCurrentCulture = culture;
    CultureInfo.DefaultThreadCurrentUICulture = culture;
}

await host.BlazorJSRunAsync();

image

dodyg commented 8 months ago

I have successfully used Solution 1 to execute this piece of code. Thank you for your help @LostBeard .

public interface INotificationWorker
{
    Task<Result<ListNotificationResponse>> FetchNotifications();
}

public class NotificationWorker(IJSRuntime js, IBFFAPIClient client) : INotificationWorker
{
    public async Task<Result<ListNotificationResponse>> FetchNotifications()
    {
        var request = new ListNotificationRequest();

        var response = await client.ListNotificationAsync(request);

        if (response.IsFalse)
        {
            JsConsole.Error(js, $"Error in calling {nameof(FetchNotifications)} {response.Message}");
            return Result<ListNotificationResponse>.False(response.Message);
        }

        if (response.Value.IsStatus(StatusCodes.Status200OK) is false)
        {
            JsConsole.Error(js, $"Error in {nameof(FetchNotifications)} result {response.Value.Header.Status}");
            return Result<ListNotificationResponse>.False(response.Value.Header.Status.ToString());
        }

        return Result<ListNotificationResponse>.True(response.Value.Body!);
    }
}
LostBeard commented 8 months ago

I am trying solution 2

Solution 2 works just fine. You got an error because the WebWorker used a cached version of your index.html. Same thing happened to me on the first test of the code. Simply disabling caching via the DevTools and refreshing allowed the new index.html to be loaded.

Glad you found a usable solution. 👍