Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible regression in JavaScript [JSImport]/[JSExport] interop in a Blazor app (.NET 8 Preview 5) #87690

Closed
guardrex opened this issue Jun 16, 2023 · 12 comments

Comments

@guardrex
Copy link

Description

Pavel, I'm working on doc updates for the Webcil (.wasm) .NET assembly packing format for .NET 8 preview docs.

One of the JS import/export examples calls getAssemblyExports on the app's assembly, so I was checking that example. While doing so, I noticed an error when running the example in a Pre5 test app.

The section is located at ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop?view=aspnetcore-7.0#call-net-from-javascript

When the component is accessed, it works as described ...

image

When that error is checked, it appears as ...

blazor.webassembly.js:1 Error: Assert failed: ES6 module CallDotNet1 was not imported yet, please call JSHost.ImportAsync() first.

JSHost.ImportAsync() is called (and ultimately works) ...

protected override async Task OnInitializedAsync()
{
    await JSHost.ImportAsync("CallDotNet1",
        "../Pages/CallDotNet1.razor.js");
}

Cross-reference: https://github.com/guardrex/BlazorWASM80Pre5WebcilCheck/blob/main/Pages/CallDotNet1.razor#L16-L17

Reproduction Steps

I put up a sample app with that example ...

https://github.com/guardrex/BlazorWASM80Pre5WebcilCheck

Expected behavior

No module load error in the console.

Actual behavior

image

Regression?

AFAIK, there was no error when we put the guidance up.

Known Workarounds

None

Configuration

  • .NET 8 Preview 5
  • Windows 11 Pro
  • x64
  • Don't know if it's specific to this config.
  • MS Edge Version 114.0.1823.43 (Official build) (64-bit)

Other information

None

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jun 16, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jun 16, 2023
@teo-tsirpanis teo-tsirpanis added area-System.Runtime.InteropServices.JavaScript and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jun 16, 2023
@lambdageek
Copy link
Member

lambdageek commented Jun 16, 2023

This seems like maybe something in the Blazor render logic has changed or maybe ImportAsync became lazier

If I change the CallDotNet1.razor page like this:

    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine ("Before ImportAsync");
        await JSHost.ImportAsync("CallDotNet1",
            "../Pages/CallDotNet1.razor.js");
        Console.WriteLine ("ImportAsync done");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine ($"In OnAfterRender first:{firstRender}");
        SetWelcomeMessage();
    }

I see the following in the browser console:

Before ImportAsync
blazor.webassembly.js:1 In OnAfterRender first:True
blazor.webassembly.js:1 Error: Assert failed: ES6 module CallDotNet1 was not imported yet, please call JSHost.ImportAsync() first.
At @ blazor.webassembly.js:1
blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Error: Assert failed: ES6 module CallDotNet1 was not imported yet, please call JSHost.ImportAsync() first.
          at http://localhost:5043/_framework/dotnet.runtime.8.0.0-preview.5.23280.8.ezmmp7xb16.js:3:47340
          at zr (http://localhost:5043/_framework/dotnet.runtime.8.0.0-preview.5.23280.8.ezmmp7xb16.js:3:47805)
          at wasm://wasm/00afa88e:wasm-function[339]:0x21835
          at wasm://wasm/00afa88e:wasm-function[236]:0x1defe
          at wasm://wasm/00afa88e:wasm-function[229]:0x111d9
          at wasm://wasm/00afa88e:wasm-function[263]:0x1f045
          at wasm://wasm/00afa88e:wasm-function[3213]:0xe7784
          at wasm://wasm/00afa88e:wasm-function[2539]:0xbe496
          at wasm://wasm/00afa88e:wasm-function[2538]:0xbe426
          at wasm://wasm/00afa88e:wasm-function[1920]:0x9a9cd
Error: Assert failed: ES6 module CallDotNet1 was not imported yet, please call JSHost.ImportAsync() first.
    at http://localhost:5043/_framework/dotnet.runtime.8.0.0-preview.5.23280.8.ezmmp7xb16.js:3:47340
    at zr (http://localhost:5043/_framework/dotnet.runtime.8.0.0-preview.5.23280.8.ezmmp7xb16.js:3:47805)
    at wasm://wasm/00afa88e:wasm-function[339]:0x21835
    at wasm://wasm/00afa88e:wasm-function[236]:0x1defe
    at wasm://wasm/00afa88e:wasm-function[229]:0x111d9
    at wasm://wasm/00afa88e:wasm-function[263]:0x1f045
    at wasm://wasm/00afa88e:wasm-function[3213]:0xe7784
    at wasm://wasm/00afa88e:wasm-function[2539]:0xbe496
    at wasm://wasm/00afa88e:wasm-function[2538]:0xbe426
    at wasm://wasm/00afa88e:wasm-function[1920]:0x9a9cd
At @ blazor.webassembly.js:1
blazor.webassembly.js:1 ImportAsync done
blazor.webassembly.js:1 In OnAfterRender first:False

So when we hit the await in OnInitializedAsync we immediately proceed to the first render. and only after that does the import complete.

I suspect previously ImportAsync returned an already-completed Task, so OnInitializeAsync finished completely before the first render.

@lambdageek lambdageek added the arch-wasm WebAssembly architecture label Jun 16, 2023
@ghost
Copy link

ghost commented Jun 16, 2023

Tagging subscribers to 'arch-wasm': @lewing
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Pavel, I'm working on doc updates for the Webcil (.wasm) .NET assembly packing format for .NET 8 preview docs.

One of the JS import/export examples calls getAssemblyExports on the app's assembly, so I was checking that example. While doing so, I noticed an error when running the example in a Pre5 test app.

The section is located at ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop?view=aspnetcore-7.0#call-net-from-javascript

When the component is accessed, it works as described ...

image

When that error is checked, it appears as ...

blazor.webassembly.js:1 Error: Assert failed: ES6 module CallDotNet1 was not imported yet, please call JSHost.ImportAsync() first.

JSHost.ImportAsync() is called (and ultimately works) ...

protected override async Task OnInitializedAsync()
{
    await JSHost.ImportAsync("CallDotNet1",
        "../Pages/CallDotNet1.razor.js");
}

Cross-reference: https://github.com/guardrex/BlazorWASM80Pre5WebcilCheck/blob/main/Pages/CallDotNet1.razor#L16-L17

Reproduction Steps

I put up a sample app with that example ...

https://github.com/guardrex/BlazorWASM80Pre5WebcilCheck

Expected behavior

No module load error in the console.

Actual behavior

image

Regression?

AFAIK, there was no error when we put the guidance up.

Known Workarounds

None

Configuration

  • .NET 8 Preview 5
  • Windows 11 Pro
  • x64
  • Don't know if it's specific to this config.
  • MS Edge Version 114.0.1823.43 (Official build) (64-bit)

Other information

None

Author: guardrex
Assignees: -
Labels:

arch-wasm, untriaged, area-System.Runtime.InteropServices.JavaScript

Milestone: -

@lambdageek lambdageek added this to the 8.0.0 milestone Jun 16, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jun 16, 2023
@lambdageek
Copy link
Member

Doing something like this will resolve the error, but I'm not sure if that's the right thing or if this needs to be fixed in Blazor.

    private bool importDone;

    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine ("Before ImportAsync");
        await JSHost.ImportAsync("CallDotNet1",
            "../Pages/CallDotNet1.razor.js");
        importDone = true;
        StateHasChanged();
        Console.WriteLine ("ImportAsync done");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine ($"In OnAfterRender first:{firstRender}");
        if (importDone)
            SetWelcomeMessage();
    }

@lambdageek
Copy link
Member

lambdageek commented Jun 16, 2023

cc @maraf @javiercn @MackinnonBuck - is it expected that blazor pages will call OnAfterRender (firstRender: true) even before OnInitializedAsync is completely completed?

@javiercn
Copy link
Member

javiercn commented Jun 16, 2023

@lambdageek yes.

ComponentBase renders twice, once in the middle of the method when the synchronous work finishes, and another time after the async work completes.

We call OnInitializedAsync, check if the returned task is not completed, render, and then queue a continuation once the task completes.

The relevant code is here

@lambdageek
Copy link
Member

ComponentBase renders twice, once in the middle of the method when the synchronous work finishes, and another time after the async work completes.

We call OnInitializedAsync, check if the returned task is not completed, render, and then queue a continuation once the task completes.

@guardrex sounds like the sample should be updated to something like #87690 (comment) so that it doesn't attempt to call the imported javascript method until the async import has completed. (seems like you can remove the call to StateHasChanged in OnInitializeAsync, however. And the writelines)

The fact that it happened to work on earlier runtimes is probably just a happy accident, but it could break there, too, if you had other awaits in the component initialization before the async import, for example.

@guardrex
Copy link
Author

guardrex commented Jun 16, 2023

In one of Steve's examples, he loads his modules in OnAfterRenderAsync, not OnInitializedAsync. I'd like to give that a shot with this type of JS interop to see how that works ... unless anyone knows that it will 💥. I'll test it now and report back.

@guardrex
Copy link
Author

Ok, so this ... based on a Steve example in one of our regular JS interop articles ...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JSHost.ImportAsync("CallDotNet1",
        "../Pages/CallDotNet1.razor.js");

        SetWelcomeMessage();
    }
}

... seems to ✨ Just Work!™ ✨ without incident. Any objections to using that approach with import/export JS interop?

@guardrex
Copy link
Author

btw - Is @pavelsavara out right now? He and I were working on these examples, so I was hoping to have him run an 👁️ over this to make sure he's cool with whatever we do.

@lambdageek
Copy link
Member

@guardrex Pavel should be back early next week

FWIW I think doing it from OnAfterRenderAsync is better. Keeps the code simple, with the import and call near each other. Probably if someone needs more a more sophisticated lifecycle strategy, they could look at the Razor component lifecycle docs and adopt the simple sample to their needs

@guardrex
Copy link
Author

guardrex commented Jun 16, 2023

I agree, and I've used Steve's approach elsewhere in the docs. I'm just not aware if Pavel sent it over that way intentionally. I'll make a note to email him next week and confirm offline that the choice is fine with him. I'll take this up now on a docs issue.

Thanks all and have a great weekend! 🍻

@pavelsavara
Copy link
Member

I don't have strong opinion and I'm OK with the outcome :)

@ghost ghost locked as resolved and limited conversation to collaborators Jul 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants