1

我正在使用最新的Blazor WebAssembly 3.2.0 候选版本

从 vanilla Blazor WASM 模板中,我进行了以下调整以测试/调试问题:

Program.cs中,我添加了以下将注入所有页面的单例:

builder.Services.AddSingleton<ApplicationState>();

该类看起来像:

public class ApplicationState
{
    public bool BoolValue { get; set; }
    public string StringValue { get; set; }

    public ApplicationState()
    {
        BoolValue = false;
        StringValue = "Default value";
    }

}

并与JSRuntime一起注入到_Imports.razor中,因此它们可用于所有页面:

@inject IJSRuntime JSRuntime
@inject BlazorApp1.State.ApplicationState AppState;

为了测试 ApplicationState 对象是否正确地通过了应用程序,我在以下 2 个位置使用了它:

NavManu.razor中,我将其中一个菜单项包裹在BoolValue周围以显示/隐藏该项目:

@if (AppState.BoolValue)
{
    <li class="nav-item px-3">
        <NavLink class="nav-link" href="counter">
            <span class="oi oi-plus" aria-hidden="true"></span> Counter
        </NavLink>
    </li>
}

Index.razor中,我在默认的 Hello World 标头下添加了一个 H2 标记以显示StringValue

<h1>Hello, world!</h1>
<h2>@AppState.StringValue</h2>

我还有一个位于wwwroot/js/extenstions.js的 javascript 文件,它有一个用于获取 cookie 数据的函数:

function getCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
};

并且作为脚本包含在_framework/blazor.webassembly.js脚本下方的wwwroot/index.html中:

<script src="_framework/blazor.webassembly.js"></script>
<script src="js/extensions.js"></script>

App.razor我有以下@code块:

@code
{
    protected override async Task OnInitializedAsync()
    {
        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
    }
}

一切都按预期工作,菜单项以及 H2 标记中的字符串值都是可见的。

但是,如果我通过 JSRuntime 添加对我的 Javasript 函数的调用,如下所示:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
    }
}

javascript运行,AppState值被更新(我添加了一个控制台日志来测试这个)但是导航没有更新以显示菜单项并且H2标签没有显示新的字符串值......

如果您导航到菜单中的页面,状态将更新,但如果您进行硬刷新,则不会再次更新......

我尝试添加StateHasChanged()调用,但这不起作用:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";
        StateHasChanged(); // Does not work!
    }
}

由于导航后状态似乎更新了,我也尝试强制导航事件,也无济于事:

@code
{
    protected override async Task OnInitializedAsync()
    {
        var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");

        AppState.BoolValue = true;
        AppState.StringValue = "Testing 123...";

        var uri = new Uri(NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
        NavigationManager.NavigateTo(uri);
    }
}

我正在继续尝试其他一些事情,但似乎没有任何效果。在我的理解中, StateHasChanged()确实应该是解决此类问题的方法。还有什么我应该尝试的吗?

更新

正如 enet 指出的那样,当状态发生变化时,我需要通知下游组件。引起问题的不是 JSRuntime,而是它在 App.razor 中创建的延迟导致对象状态在硬刷新时似乎有所不同。在第一个场景中,对象在其他组件开始渲染之前更新,因此它们使用已经更新的状态。但是一旦出现任何类型的延迟(例如调用 JSRuntime),他们就需要通知来更新他们的状态,因为他们有机会在延迟之前使用旧状态进行渲染。我用几毫秒的 Task.Delay() 替换了 JSRuntime 调用,这导致了同样的问题!向事件添加触发器解决了这个问题。

4

1 回答 1

2

消息服务.cs

 public class MessageService
{
    private string message;
    public string Message
    {
        get => message;
        set
        {
            if (message != value)
            {
                message = value;
                if (Notify != null)
                {
                     Notify?.Invoke();
                }

            }
        }
    }

    public event Action Notify;
}

注意:该服务是一个普通类...它为其他对象提供服务,并且应该在 Startup.ConfigureServices 方法中将其添加到 DI 容器中,以使其可供请求的客户端使用。在 ConfigureServices 方法中添加:

 services.AddScoped<MessageService>();

注意:如您所见,我定义了一个 Action 类型的事件委托,当用户在 Component3 中的文本框中键入文本时,它会从属性的 set 访问器调用。触发此委托会导致 Components3 输入的文本显示在 Index 组件中,该组件是 Component2 的父级(请参见下面的代码)。

索引.razor

  @page "/"

  @inject MessageService MessageService
  @implements IDisposable
  @inject IJSRuntime  JSRuntime

  <p>
   I'm the parent of Component2. I've got a message from my grand 
   child: @MessageService.Message
  </p>

  <Component2 />
  <p>@jwtToken</p>

  @code {
    private string jwtToken;

    protected override async Task OnInitializedAsync()
   {
       // await SetTokenAsync("my token");
       jwtToken = await GetTokenAsync();
       MessageService.Notify += OnNotify;
       MessageService.Message = "A message from Index";
    }

    public void OnNotify()
    {
    InvokeAsync(() =>
    {
        StateHasChanged();
    });
}

public void Dispose()
{
    MessageService.Notify -= OnNotify;
}

public async Task<string> GetTokenAsync()
       => await JSRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");

public async Task SetTokenAsync(string token)
{
    if (token == null)
    {
        await JSRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
    }
    else
    {
        await JSRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
    }


 }
}

注意我们直接绑定到 MessageService.Message 属性,但是必须调用 StateHasChanged 方法来刷新文本的显示。

Component2.razor

<h3>Component2: I'm the parent of component three</h3>
<Component3/>

@code {

}

Component3.razor

 @inject MessageService MessageService

 <p>This is component3. Please type a message to my grand parent</p>
 <input placeholder="Type a message to grandpa..." type="text"
   @bind="@MessageService.Message" @bind:event="oninput" />

 @code {

     protected override void OnInitialized()
     {

        MessageService.Notify += () => InvokeAsync(() =>
        {
           StateHasChanged();
        });

     }
 }

请注意,在 Component3 中,我们将 MessageService.Message 绑定到一个文本框,并且每次按下键盘时都会发生绑定(输入事件与更改事件)。

就是这样,希望这会有所帮助,不要犹豫,提出任何问题。

于 2020-05-08T18:35:54.453 回答