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);
}