Blazor OnAfterRenderAsync Javascript

Blazor OnAfterRenderAsync Javascript

所以我一直在研究 Blazor WebAssembly,但我不知道如何解决这个问题。基本上,我有一个 NavMenu.razor 页面,它将从 JSON 文件异步获取 NavMenuItem 数组。那很好用。现在,我要做的是为每个锚点添加一个事件监听器,这些锚点是从下面的 foreach 循环中的 <NavLink> 标签生成的。

该循环之外的 NavLink(未由异步函数填充的那个)成功地正确应用了 addSmoothScrollingToNavLinks() 函数。其他 NavLinks 没有。好像他们还没有在 DOM.

我猜有一些奇怪的竞争条件,但我不知道如何解决它。关于如何解决这个问题有什么想法吗?

NavMenu.razor

@inject HttpClient Http
@inject IJSRuntime jsRuntime

@if (navMenuItems == null)
{
    <div></div>
}
else
{
    @foreach (var navMenuItem in navMenuItems)
    {
        <NavLink class="nav-link" href="@navMenuItem.URL" Match="NavLinkMatch.All">
            <div class="d-flex flex-column align-items-center">
                <div>
                    <i class="@navMenuItem.CssClass fa-2x"></i>
                </div>
                <div class="mt-1">
                    <span>@navMenuItem.Text</span>
                </div>
            </div>
        </NavLink>
    }
}

<NavLink class="nav-link" href="#" Match="NavLinkMatch.All">
    <div class="d-flex flex-column align-items-center">
        This works!
    </div>
</NavLink>

@code {
    private NavMenuItem[] navMenuItems;

    protected override async Task OnInitializedAsync()
    {
        navMenuItems = await Http.GetJsonAsync<NavMenuItem[]>("sample-data/navmenuitems.json");
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
        }
    }

    public class NavMenuItem
    {
        public string Text { get; set; }

        public string URL { get; set; }

        public string CssClass { get; set; }
    }
}

index.html(在 webassembly.js 脚本标签下)

 <script>
        function scrollToTop() {
            window.scroll({
                behavior: 'smooth',
                left: 0,
                top: 0
            });
        }

        window.MyFunctions = {
            addSmoothScrollingToNavLinks: function () {
                let links = document.querySelectorAll("a.nav-link");
                console.log(links);
                // links only has 1 item in it here. The one not generated from the async method
                for (const link of links) {
                    link.addEventListener('click', function (event) {
                        event.preventDefault();
                        scrollToTop();
                    })
                }
            }
        }
    </script>

这是因为 Blazor 不会 等待 OnInitializedAsync() 完成,并且会在 OnInitializedAsync 启动后开始渲染视图。见 source code on GitHub:

private async Task RunInitAndSetParametersAsync()
{
    OnInitialized();
    var task = OnInitializedAsync();       // NO await here!

    if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
    {
        // Call state has changed here so that we render after the sync part of OnInitAsync has run
        // and wait for it to finish before we continue. If no async work has been done yet, we want
        // to defer calling StateHasChanged up until the first bit of async code happens or until
        // the end. Additionally, we want to avoid calling StateHasChanged if no
        // async work is to be performed.
        StateHasChanged();                  // Notify here! (happens before await)

        try
        {
            await task;                     // await the OnInitializedAsync to complete!
        }
        ...

如您所见,StateHasChanged() 可能会在 OnInitializedAsync() 完成之前开始。由于您在 OnInitializedAsync() 方法中发送 HTTP 请求,组件会在您从 sample-data/navmenuitems.json 端点获得响应之前呈现。

如何修复

您可以在 Blazor(而不是 js)中绑定事件,并触发 JavaScript 编写的处理程序。例如,如果您使用 ASP.NET Core 3.1.x(不适用于 3.0,请参阅 PR#14509):

@foreach (var navMenuItem in navMenuItems)
{
<a class="nav-link" href="@navMenuItem.URL" @onclick="OnClickNavLink" @onclick:preventDefault>
    <div class="d-flex flex-column align-items-center">
        <div>
            <i class="@navMenuItem.CssClass fa-2x"></i>
        </div>
        <div class="mt-1">
            <span>@navMenuItem.Text</span>
        </div>
    </div>
</a>
}

@code{
    ...


    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        //if (firstRender)
        //{
        //    await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
        //}
    }
    private async Task OnClickNavLink(MouseEventArgs e)
    {
        Console.WriteLine("click link!");
        await jsRuntime.InvokeVoidAsync("MyFunctions.smoothScrolling");
    }

}

其中 MyFunctions.addSmoothScrollingToNavLinks 是:

smoothScrolling:function(){
    scrollToTop();
}

我也在努力使这种模式正确,在 OnInitializedAsync 中调用一些异步服务,然后在 OnAfterRenderAsync 中调用一些 javascript。问题是 javascript 调用是用来执行一些 js 的,这些 js 取决于服务返回的数据(数据必须在那里才能使所需的行为生效)。

发生的事情是 js 调用是 运行 在数据到达之前,有没有办法在来自 OnInitializedAsyncOnAfterRenderAsync 的数据之间实现这个 "dependency" ?

不完美,但对我有用, 让你等待下载

private bool load = false;
protected override async Task OnInitializedAsync()
{
    navMenuItems = await Http.GetJsonAsync<NavMenuItem[]>("sample-data/navmenuitems.json");
    load = true;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (load)
    {
        await jsRuntime.InvokeVoidAsync("MyFunctions.addSmoothScrollingToNavLinks");
        load = false;
    }
}