Blazor - 在 API 调用时显示等待或微调器

Posted

技术标签:

【中文标题】Blazor - 在 API 调用时显示等待或微调器【英文标题】:Blazor - Display wait or spinner on API call 【发布时间】:2019-10-29 11:42:06 【问题描述】:

在我的 Blazor 应用程序中,我正在对后端服务器进行 API 调用,这可能需要一些时间。我需要向用户显示反馈、等待光标或“微调器”图像。这是如何在 Blazor 中完成的?

我尝试使用 CSS 并打开和关闭 CSS,但在调用完成之前页面不会刷新。任何建议将不胜感激。

@functions 
    UserModel userModel = new UserModel();
    Response response = new Response();
    string errorCss = "errorOff";
    string cursorCSS = "cursorSpinOff";

    protected void Submit()
    
        //Show Sending...
        cursorCSS = "";
        this.StateHasChanged();
        response = Service.Post(userModel);
        if (response.Errors.Any())
        
            errorCss = "errorOn";
        
        //turn sending off
        cursorCSS = "cursorSpinOff";
        this.StateHasChanged();
    

【问题讨论】:

【参考方案1】:

选项 1:使用 Task.Delay(1)

使用异步方法。 使用await Task.Delay(1)await Task.Yield(); 刷新更改
private async Task AsyncLongFunc()    // this is an async task

    spinning=true;
    await Task.Delay(1);      // flushing changes. The trick!!
    LongFunc();               // non-async code
    currentCount++;
    spinning=false;
    await Task.Delay(1);      // changes are flushed again    

选项 1 是一个简单的解决方案,运行良好,但看起来像个技巧。

选项 2:使用 Task.Run()(不适用于 WebAssembly)

2020 年 1 月。 @Ed Charbeneau 发布 BlazorPro.Spinkit project 将长进程包含在任务中以不阻塞线程:

确保您的LongOperation()Task,如果不是,请将其包含在Task 中并等待它:

async Task AsyncLongOperation()    // this is an async task

    spinning=true;
    await Task.Run(()=> LongOperation());  //<--here!
    currentCount++;
    spinning=false;


效果

微调器和服务器端预渲染

由于 Blazor Server 应用使用预渲染,微调器不会出现,要显示微调器,必须在 OnAfterRender 中完成长时间操作。

在 OnInitializeAsync 上使用 OnAfterRenderAsync 以避免延迟的服务器端渲染

    // Don't do this
    //protected override async Task OnInitializedAsync()
    //
    //    await LongOperation();
    //

    protected override async Task OnAfterRenderAsync(bool firstRender)
    
        if (firstRender)
                    
            await Task.Run(()=> LongOperation());//<--or Task.Delay(0) without Task.Run
            StateHasChanged();
        
    

更多示例

了解更多关于如何编写漂亮的微调器的信息,您可以从开源项目BlazorPro.Spinkit 中学习,它包含巧妙的示例。

更多信息

请参阅 Henk Holterman's answer 以及 blazor 内部解释。

【讨论】:

谢谢,谢谢,谢谢。很高兴我找到了这个。 我已经搜索了几个小时,Task.Run 是我在异步函数中从实体框架 DBContext 返回数据时丢失的内容。谢谢! 您自己的最后一次编辑使其具有两个选项会好很多。我会做一些小的修饰。 很好的答案。尤其是服务器端 Blazor 项目中关于 OnAfterRender 的部分。非常感谢。 你不知道我有多高兴找到这个...【参考方案2】:

除了@dani 在这里的回答,我想指出这里有两个独立的问题,将它们分开是值得的。

    何时致电StateHasChanged()

Blazor 将(在概念上)在初始化之后以及事件之前和之后调用 StateHasChanged()。这意味着您通常不需要调用它,只有当您的方法有几个不同的步骤并且您想要在中间更新 UI 时才需要调用它。微调器就是这种情况。

当您使用即发即弃 (async void) 或来自不同来源的更改(例如计时器或程序中其他层的事件)时,您确实需要调用它。

    调用StateHasChanged()后如何确保UI更新

StateHasChanged() 本身不会更新 UI。它只是对渲染操作进行排队。您可以将其视为设置“脏标志”。

一旦渲染引擎再次在其线程上运行,Blazor 就会更新 UI。就像任何其他 UI 框架一样,所有 UI 操作都必须在主线程上完成。但是您的事件也(最初)在同一个线程上运行,阻塞了渲染器。

要解决此问题,请通过返回 async Task 确保您的事件是异步的。 Blazor 完全支持这一点。不要不要使用async void。仅当您不需要异步行为时才使用 void

2.1 使用异步操作

当您的方法在 StateHasChanged() 之后快速等待异步 I/O 操作时,您就完成了。控制权将返回渲染引擎,您的 UI 将更新。

 statusMessage = "Busy...";
 StateHasChanged();
 response = await SomeLongCodeAsync();  // show Busy
 statusMessage = "Done.";

2.2 插入一个小的异步操作

当您的代码占用大量 CPU 时,它不会快速释放主线程。当你调用一些外部代码时,你并不总是知道它到底有多异步。所以我们有一个流行的技巧:

 statusMessage = "Busy...";
 StateHasChanged();
 await Task.Delay(1);      // flush changes - show Busy
 SomeLongSynchronousCode();
 statusMessage = "Done.";

更合乎逻辑的版本是使用 Task.Yield(),但这在 WebAssembly 上往往会失败。

2.3 使用Task.Run() 使用额外的线程

当您的事件处理程序需要调用一些非异步代码(例如受 CPU 限制的工作)并且您在 Blazor-Server 上时,您可以使用 Task.Run() 争取一个额外的池线程:

 statusMessage = "Busy...";
 StateHasChanged();
 await Task.Run( _ => SomeLongSynchronousCode());  // run on other thread
 statusMessage = "Done.";

当你在 Blazor-WebAssembly 上运行它时,它没有任何效果。浏览器环境中没有可用的“额外线程”。

当您在 Blazor-Server 上运行此程序时,您应该意识到使用更多线程可能会损害您的可伸缩性。如果您计划在服务器上运行尽可能多的并发客户端,那么这是一种去优化。

当你想试验时:

void SomeLongSynchronousCode()
 
   Thread.Sleep(3000);


Task SomeLongCodeAsync()
 
   return Task.Delay(3000);

【讨论】:

【参考方案3】:

围绕 StateHasChanged() 进行了很多精彩的讨论,但为了回答 OP 的问题,这里有另一种实现微调器的方法,通用,用于 HttpClient 调用后端 API。

此代码来自 Blazor Webassembly 应用程序...

Program.cs

public static async Task Main(string[] args)

    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");
    
    builder.Services.AddScoped(sp => new HttpClient  BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) );
    builder.Services.AddScoped<SpinnerService>();
    builder.Services.AddScoped<SpinnerHandler>();
    builder.Services.AddScoped(s =>
    
        SpinnerHandler spinHandler = s.GetRequiredService<SpinnerHandler>();
        spinHandler.InnerHandler = new HttpClientHandler();
        NavigationManager navManager = s.GetRequiredService<NavigationManager>();
        return new HttpClient(spinHandler)
        
            BaseAddress = new Uri(navManager.BaseUri)
        ;
    );

    await builder.Build().RunAsync();

SpinnerHandler.cs 注意:记得取消注释人工延迟。如果您在 Visual Studio 中使用开箱即用的 Webassembly 模板,请单击 天气预报 以查看运行中的微调器演示。

public class SpinnerHandler : DelegatingHandler

    private readonly SpinnerService _spinnerService;

    public SpinnerHandler(SpinnerService spinnerService)
    
        _spinnerService = spinnerService;
    

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    
        _spinnerService.Show();
        //await Task.Delay(3000); // artificial delay for testing
        var response = await base.SendAsync(request, cancellationToken);
        _spinnerService.Hide();
        return response;
    

SpinnerService.cs

public class SpinnerService

    public event Action OnShow;
    public event Action OnHide;

    public void Show()
    
        OnShow?.Invoke();
    

    public void Hide()
    
        OnHide?.Invoke();
    

MainLayout.razor

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="top-row px-4">
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>

        <div class="content px-4">
            @Body
            <Spinner />
        </div>
    </div>
</div>

Spinner.razor 注意:要添加一些变化,您可以在 OnIntialized() 方法中生成一个随机数,并在div 选择随机微调器类型。在此方法中,对于每个 HttpClient 请求,最终用户将观察到随机微调器类型。为简洁起见,此示例已被精简为仅一种类型的微调器。

@inject SpinnerService SpinnerService

@if (isVisible)

    <div class="spinner-container">
        <Spinner_Wave />
    </div>


@code

    protected bool isVisible  get; set; 

    protected override void OnInitialized()
    
        SpinnerService.OnShow += ShowSpinner;
        SpinnerService.OnHide += HideSpinner;
    

    public void ShowSpinner()
    
        isVisible = true;
        StateHasChanged();
    

    public void HideSpinner()
    
        isVisible = false;
        StateHasChanged();
    

Spinner-Wave.razor 归功于:https://tobiasahlin.com/spinkit/ 注意:这个旋转套件有一个 Nuget 包。 Nuget 包的缺点是您无法直接访问 CSS 进行调整。在这里,我调整了微调器的大小,并将背景颜色设置为与网站的原色相匹配,如果您在整个网站中使用 CSS 主题(或者可能是多个 CSS 主题),这将很有帮助

@* Credit: https://tobiasahlin.com/spinkit/ *@

<div class="spin-wave">
    <div class="spin-rect spin-rect1"></div>
    <div class="spin-rect spin-rect2"></div>
    <div class="spin-rect spin-rect3"></div>
    <div class="spin-rect spin-rect4"></div>
    <div class="spin-rect spin-rect5"></div>
</div>
<div class="h3 text-center">
    <strong>Loading...</strong>
</div>

<style>
    .spin-wave 
        margin: 10px auto;
        width: 200px;
        height: 160px;
        text-align: center;
        font-size: 10px;
    

        .spin-wave .spin-rect 
            background-color: var(--primary);
            height: 100%;
            width: 20px;
            display: inline-block;
            -webkit-animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
            animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
        

        .spin-wave .spin-rect1 
            -webkit-animation-delay: -1.2s;
            animation-delay: -1.2s;
        

        .spin-wave .spin-rect2 
            -webkit-animation-delay: -1.1s;
            animation-delay: -1.1s;
        

        .spin-wave .spin-rect3 
            -webkit-animation-delay: -1s;
            animation-delay: -1s;
        

        .spin-wave .spin-rect4 
            -webkit-animation-delay: -0.9s;
            animation-delay: -0.9s;
        

        .spin-wave .spin-rect5 
            -webkit-animation-delay: -0.8s;
            animation-delay: -0.8s;
        

    @@-webkit-keyframes spin-waveStretchDelay 
        0%, 40%, 100% 
            -webkit-transform: scaleY(0.4);
            transform: scaleY(0.4);
        

        20% 
            -webkit-transform: scaleY(1);
            transform: scaleY(1);
        
    

    @@keyframes spin-waveStretchDelay 
        0%, 40%, 100% 
            -webkit-transform: scaleY(0.4);
            transform: scaleY(0.4);
        

        20% 
            -webkit-transform: scaleY(1);
            transform: scaleY(1);
        
    
</style>

很漂亮

【讨论】:

很棒的帖子!这几乎正​​是我想要的,因为我想向现有项目添加“加载/处理”指示器,但不想修改每个页面/组件来添加“微调器”代码。我似乎遇到的唯一问题是当有多个同时调用后端 API 并且某些调用比其他调用花费更长的时间时。使用此代码,只要第一次调用完成,微调器就会被隐藏。我想我可以稍微调整一下以“知道”在隐藏之前是否仍在处理呼叫。再次感谢! @Redwing19,很高兴我能提供帮助并感谢您的热情。关于多个并行 API 请求,它取决于具体情况。我淡化了这个答案,以便人们可以根据需要进行定制。对于我的使用,我通过向 SpinnerService 添加一个布尔完整属性来解决多个请求问题,以作为在调用 Hide() 时不关闭微调器的替代。加载完数据后,我的组件调用属性的 set 方法,该方法将调用 Hide() 然后关闭微调器。但这只是一个例子。如果你想出更通用的东西,请分享。 感谢您的信息!我最终向 SpinnerHandler 类添加了一个静态 int _callCounter 成员变量。它在调用 SendAsync 之前递增,之后递减。然后,如果 _callCounter 为 0,我只调用 _spinnerService.Hide()。这并不完美,因为如果连续完成多个调用并且第一个调用在第二个调用开始之前完成,它会闪烁一点,但它可以工作非常适合并发通话并很好地满足我的需求。【参考方案4】:

为了回答@daniherrera's solution中的通知,提出了三个更优雅的解决方案here。

简而言之:

实现 INotifyPropertyChanged 模型并调用 StateHasChanged() PropertyChangedEventHandler 模型中的事件属性。 使用委托在模型上调用 StateHasChanged() 添加一个 EventCallBack&lt;T&gt; 参数到视图的组件或页面,并将其分配给应该更改组件及其父级渲染的函数。 ( StateHasChanged()这个没必要`)

最后一个选项是最简单、最灵活和***的选项,但请根据您的方便选择。

总的来说,如果您担心应用的安全性,我建议您使用其中一种解决方案,而不是 await Task.Delay(1); 一种。

编辑:经过更多阅读,this link 提供了关于如何在 C# 中处理事件的有力解释,主要是 EventCallBack

【讨论】:

“总的来说,我建议使用其中一种解决方案,而不是 await Task.Delay(1);如果您的应用程序的安全性受到关注,则使用一种。”你的意思是?这些渲染器机制在安全方面有何不同?【参考方案5】:

不要犯我使用 Thread.Sleep(n) 测试等待微调器时犯的同样错误。

protected override async Task OnInitializedAsync()

    // Thread.Sleep(3000); // By suspending current thread the browser will freeze.
    await Task.Delay(3000); // This is your friend as dani herrera pointed out. 
                      // It creates a new task that completes 
                      // after a specified number of milliseconds.

    forecasts = await ForecastService.GetForecastAsync(DateTime.Now);

【讨论】:

它似乎不起作用。我收到警告“由于未等待此调用,因此在调用完成之前继续执行当前方法。考虑将 'await' 运算符应用于调用结果。” 嗨,我怀疑你没有等待电话 --> await SomethingToAwait.SomeAsyncMethod() @rugbrød,警告提示您需要等待“Task.Delay(3000)”,否则不会发生延迟。我编辑了示例以修复此警告。【参考方案6】:

Blazor 服务器端 - 我需要调用 StateHasChanged() 来强制更新前端,以便在代码移动到 ajax 调用之前显示微调器。

/* Show spinner */
carForm.ShowSpinner = true;

/* Force update of front end */
StateHasChanged();

/* Start long running API/Db call */
await _carRepository.Update(item);

【讨论】:

在 Blazor Server 中对我不起作用。我需要 dani 的选项 1。这就像 StateHasChanged 在异步操作期间没有注册。 如果调用StateHasChanged(“强制”可能是错误的术语,它本质上表示/表示“脏”状态)是这里的一个考虑因素,那么你的答案很可能不相关对于这个问题的情景或每个人的担忧。您可能需要调用StateHasChanged,因为_carRepository.Update 不是此范围内完成的第一个或最后一个任务。参考Multiple Asynchronous Phases in an Asynchronous Handler【参考方案7】:

使用 InvokeAsync(StateHasChanged),希望它会起作用。

protected async void Submit()
    
        //Show Sending...
        cursorCSS = "";
        this.StateHasChanged();
        response = Service.Post(userModel);
        if (response.Errors.Any())
        
            errorCss = "errorOn";
        
        //turn sending off
        cursorCSS = "cursorSpinOff";
        await InvokeAsync(StateHasChanged);
    

【讨论】:

不要使用async void。这里不需要。

以上是关于Blazor - 在 API 调用时显示等待或微调器的主要内容,如果未能解决你的问题,请参考以下文章

Bootstrap 3:在等待模式内容加载时显示微调器+淡入淡出背景

如何在 Flutter 中获取当前位置时显示加载微调器?

Flutter 在登录时显示微调器

在 DataTables 表加载 Ruby on Rails 时显示微调器

在Swift中加载UICollectionView时的加载器/微调器

当用户尝试对数据进行排序时显示加载微调器