.Net 8 Blazor (Server and Client in auto mode) JSInterop isolation not finding js function

136 Views Asked by At

I'm trying to get jsinterop to work in my .NET 8 Blazor application using both Client and Server. I'm having a heck of a time figuring out what is wrong. Sometimes it works, but as soon as I navigate to a new page, interop breaks.

I have all my JS files at the bottom of my Layout like so:

<script src="_framework/blazor.web.js"></script>

<!-- ########## External libraries below this line ##########-->

<!-- sidebar JS -->
<script src="/assets/js/defaultmenu.js"></script>

<!-- Notifications JS -->
<script src="/scripts/vendor/awesomenotifications/index.var.min.js"></script>
<script src="/assets/js/notifications.js"></script>

The two actions I am trying to do involve a large js function that slides the sidebar open or closed and the other is to show an error toast using awesome-notifications.

The js in defaultmenu.js is like this:

function toggleSidemenu() {
// many lines of code to do the slide and animations
}

The js in notifications,js looks like this:

function showErrorToast(message) {
    new AWN({}).alert(message);
}

I've tried adding the functions to window. I've tried using export. No matter what I do, I get the following error in my UI:

Error: Microsoft.JSInterop.JSException: Could not find 'showErrorToast' ('showErrorToast' was undefined). Error: Could not find 'showErrorToast' ('showErrorToast' was undefined).

So, now onto my code. I wanted to wrap the interop call into a service that could be used by injecting it into a component. Here's roughly how that looks (exists in separate project but called from component in client project):

public class NotificationsJsInteropService(IJSRuntime js) : INotificationsInteropService
{
    public async Task ShowSuccessToastAsync(string message)
    {
        try
        {
            await using var module = await LoadNotificationsJs();

            await module.InvokeVoidAsync("showSuccessToast", message);
        }
        catch (JSDisconnectedException ex)
        {
            // Ignore
        }
    }

    public async Task ShowWarningToastAsync(string message)
    {
        try
        {
            await using var module = await LoadNotificationsJs();

            await module.InvokeVoidAsync("showWarningToast", message);
        }
        catch (JSDisconnectedException ex)
        {
            // Ignore
        }
    }

    public async Task ShowErrorToastAsync(string message)
    {
        try
        {
            await using var module = await LoadNotificationsJs();

            await module.InvokeVoidAsync("showErrorToast", message);
        }
        catch (JSDisconnectedException ex)
        {
            // Ignore
        }
    }

    private ValueTask<IJSObjectReference> LoadNotificationsJs()
    {
        return js.InvokeAsync<IJSObjectReference>("import", "/scripts/vendor/awesomenotifications/index.var.js", "/assets/js/notifications.js");
    }
}

The code above is attempting to use interop isolation. I cannot, no matter what I try, get interop to work for my application. I have tried:

  • moving the function into the layout at the end of the body and assigning it into window
  • assigning the function to a variable
  • loading the js files in the body
  • only loading the notifications.js in the isolation code and the awesome-notifications package in the body

And here is my usage of the service (in client project):

public abstract class MyBasePage : AuthedBasePage
{
    [Inject]
    public INotificationsInteropService InteropService { get; set; }

    [Inject]
    public IStateContainer<MyModel> StateContainer { get; set; }

    [Inject]
    public IMyModelService MyModelService{ get; set; }

    protected MyModel Model { get; set; } = new();

    private bool ErrorOnDataLoad { get; set; }

    protected async Task LoadDataAsync()
    {
        StateContainer.OnStateChange += DataStateHasChangedAsync;
        StateContainer.OnInitialized += DataStateHasChangedAsync;

        await StateContainer.GetOrInitializeValueAsync(LoadDataFromApiAsync);
    }

    private Task DataStateHasChangedAsync(MyModel ? arg)
    {
        Model = arg;

        StateHasChanged();
        return Task.CompletedTask;
    }

    private async Task<MyModel> LoadDataFromApiAsync()
    {
        try
        {
            return await MyModelService.GetModelAsync();
        }
        catch (Exception ex)
        {
            ErrorOnDataLoad = true;
            return new MyModel();
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (ErrorOnDataLoad)
        {
            await InteropService.ShowErrorToastAsync("Error while retrieving intake form");
        }
    }
}

If the data fails to load, I'm setting a flag so I can make sure I only call the interop after render (as I've seen in most of the examples).

The other interop function I'm using is being called directly from a component on click (in client project):

public partial class NavigationToggleComponent
{
    [Inject] public IJSRuntime Js { get; set; }

    public async Task ToggleSidebar()
    {
        await Js.InvokeVoidAsync("toggleSidemenu");

        StateHasChanged();
    }
}

If I reload the page, this call works and the sidebar will open and close. As soon as I use any navigation through the site, this interop call breaks. The first shown interop can only be accessed after navigation has happened, so that is why I think my navigation might be breaking something.

Here is App.razor (in server project):

<!DOCTYPE html>
<html lang="en" dir="ltr" data-nav-layout="vertical" class="light" data-header-styles="light" data-menu-styles="dark">

    <head>
        <base href="/">
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        
        <!-- Favicon -->
        <link rel="icon" type="/image/png" href="favicon.png">

        <HeadOutlet />
        
        <!-- fontawesome JS -->
        <script src="/scripts/fontawesome-6.5.1-pro.min.js"></script>

        <!-- Tailwind css -->
        <link href="/app.css" rel="stylesheet">
    </head>

    <Routes />
    
</html> 

The reason it looks like this is I have two completely different layouts (one for unauthenticated users and another after auth) for my site that involve different attributes on <body>

And Routes.razor (in server project):

<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

Here is my MainLayout (in client project):

@inherits LayoutComponentBase

<HeadContent>
    <!-- Main JS -->
    <script src="/assets/js/main.js"></script>

    <!-- Style Css -->
    <link rel="stylesheet" href="/assets/css/style.css">

</HeadContent>

<body class="">

<!-- page template omitted for brevity  -->

<div class="main-content">
    @Body
</div>

<!-- page template omitted for brevity  -->

<!-- blazor app -->
<script src="_framework/blazor.web.js"></script>

<!-- External js libraries below this line -->

</body>

And just for clarity, here's what a nav item looks like that seems to break interop (in client project):

<li class="@ActiveClass("slide")">
    <NavLink href="@Url" class="side-menu__item navigation-item">@Text</NavLink>
</li>

EDIT

I found another post suggesting a module export was expected. This is what I changed notifications.js to and STILL get an error that it cannot find showErrorToast

export function showSuccessToast(message) {
    new AWN({}).success(message);
}

export function showWarningToast(message) {
    new AWN({}).warning(message);
}

export function showErrorToast(message) {
    new AWN({}).alert(message);
}
    
export function showConfirm(message) {
    new AWN({}).confirm(message);
}
export function showInformation(message) {
    new AWN({}).info(message);
}
0

There are 0 best solutions below