在 foreach 循环 Blazor 中触发网络请求

Posted

技术标签:

【中文标题】在 foreach 循环 Blazor 中触发网络请求【英文标题】:Fire off a network request in the foreach loop Blazor 【发布时间】:2022-01-11 09:43:57 【问题描述】:

我有一个包含设备的 html 表。我的其中一个列名为“在线”。为了获取这个值,我们需要使用设备 ID 向服务器发出 HTTP 获取请求。

如何在 Blazor 中执行此操作,然后根据 HTTP 响应使用响应将正确的行更新为“是”或“否”?

【问题讨论】:

你也要定期更新吗? 暂时没有,除非用户刷新页面,但这可能会很好地作为第 2 步添加 您查看过此文档吗? Call a web API from ASP.NET Core Blazor 问题不在于 http 调用,它使用结果来更新表格行。 Razor 中有所有标准结构关键字,如 Razor 中的@for@foreach@if(以及更多)。见Razor syntax reference for ASP.NET Core 【参考方案1】:

为设备状态创建一个组件。在该组件中调用 OnInitializedAsync 中的 Async 方法。

<table>
  @foreach(var device in devices)
  
    <tr @key=device >
        <td> ... </td>
        <td> ... </td>
        <td> ... </td>
        <td><DeviceStatus Device=@device /></td>
    </tr>
  
</table>

DeviceStatus.razor

@IsOnline

@code 
    private bool online = false;
    private string IsOnline => online ? "Yes" : "No";

    [Parameter]
    public Device Device  get; set; 

    protected override async Task OnInitializedAsync()
    
        online = await SomeService.GetOnlineStatus(Device);
    

【讨论】:

1++。我会那样做...【参考方案2】:

在渲染器循环中进行 API 调用是一个非常糟糕的主意。

为什么?

    您无法真正控制渲染进程及其运行时间。 您让渲染器在每个渲染周期多次执行缓慢而繁琐的任务。 Renderer 进程不是为数据访问而设计的。

那你该怎么办?

    获取和管理 UI 所需数据集的 DI 服务。从 UI 中获取所有数据访问权限。如果您希望它“实时”,请使用计时器循环每 x 秒刷新一次数据。如果数据集已更改,则每次刷新都会引发DataSetChanged 事件。

    组件注入服务并连接到DataSetChanged 事件。如果有新内容要显示,事件处理程序会调用 StateHasChanged 来刷新 UI。

这是一个非常基本的演示。

服务和数据类。将服务注册为 Scoped。

using System.Timers;

namespace ***.Server;

public class RTService: IDisposable

    private System.Timers.Timer _timer = new System.Timers.Timer(3000);

    public readonly List<RTData> Data = new List<RTData>();

    public event EventHandler? DataChanged;

    public RTService()
    
        
            Data.Add(new RTData  Name = "Device 1", Live = true );
            Data.Add(new RTData  Name = "Device 2", Live = false );
            Data.Add(new RTData  Name = "Device 3", Live = true );
            Data.Add(new RTData  Name = "Device 4", Live = false );
        
        _timer.Elapsed += OnTimerElapsed;
        _timer.Start();
        _timer.AutoReset = true;
    

    private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
    
        // simulated get the data from the network
        await Task.Delay(1000);
        Data.ForEach(item => item.NewStatus = !item.Live);
        if (Data.Any(item => item.Live != item.NewStatus))
        
            Data.ForEach(item => item.Live = item.NewStatus);
            DataChanged?.Invoke(this, EventArgs.Empty);
        
    

    public void Dispose()
    
        _timer.Elapsed -= OnTimerElapsed;
    


public class RTData

    public string? Name  get; set; 

    public bool Live  get; set; 

    public bool NewStatus  get; set; 


演示页面:

@page "/"
<h3>Real-Time Device Status</h3>
<div class="container">
    @foreach (var device in this.rTService.Data)
    
        <div class="row">
            <div class="col-8">
                @device.Name
            </div>
            <div class="col-4">
                <span class="badge @this.ButtonCss(device.Live)">@this.ButtonText(device.Live)</span>
            </div>
        </div>
    
</div>

@code 
    [Inject] private RTService rTService  get; set; 

    private string ButtonCss(bool live) => live ? "bg-success" : "bg-danger";
    private string ButtonText(bool live) => live ? "On" : "Off";

    protected override Task OnInitializedAsync()
    
        this.rTService.DataChanged += this.OnDataChanged;
        return base.OnInitializedAsync();
    

    private void OnDataChanged(object? sender, EventArgs e)
        => this.InvokeAsync(StateHasChanged);

    public void Dispose()
        => this.rTService.DataChanged -= this.OnDataChanged;

【讨论】:

1++。我同意你的观点并理解你的意思。不确定OP。结合 Brian Parker 的代码提出的服务(实现状态和观察者模式)可能是一个可行的解决方案。 @HenkHolterman - 感谢代码错字编辑。【参考方案3】:

由于您的问题涉及到数据更改在显示原始数据之后,这可能会有所帮助:您可以在模型中添加一个标志以指示正在加载数据:

    List<TestUser>? Users  get; set; 

    protected override async Task OnAfterRenderAsync(bool firstRender)
    
        if (this.Users == null)
        
            this.Users = await this.GetUsersAsync();
            // Notify that properties changed
            this.StateHasChanged();
        

        var onlineTasks = new List<Task>();
        foreach (var user in this.Users)
        
            onlineTasks.Add(Task.Run(async () =>
            
                user.IsOnline = await this.GetIsOnlineAsync(user.Id);
            ));
        

        await Task.WhenAll(onlineTasks);

        // Notify that properties changed
        this.StateHasChanged();
    

    // Implement your own API call
    static readonly Random random = new Random();
    private async Task<List<TestUser>> GetUsersAsync()
    
        await Task.Delay(100);
        return new()
            
                new()  Id = 0, Name = "Foo", ,
                new()  Id = 1, Name = "Bar", ,
                new()  Id = 2, Name = "XYZ", ,
            ;
    

    // Implement your own API call using HttpClient
    private async Task<bool> GetIsOnlineAsync(int id)
    
        // Simulate a random delay
        await Task.Delay((int)(random.NextDouble() * 3000));

        // Here I just use this as example
        return id % 2 == 0;
    


    class TestUser
    
        public int Id  get; set; 
        public string Name  get; set; 
        public bool? IsOnline  get; set;  // Null = no data yet
    

显示数据取决于数据状态:

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Is Online?</th>
        </tr>
    </thead>
    <tbody>
        @if (this.Users != null)
        
            foreach (var user in this.Users)
            
                <tr>
                    <td>@(user.Name)</td>
                    <td>
                        @if (user.IsOnline == null)
                        
                            <span class="text-muted">Loading...</span>
                        
                        else if (user.IsOnline.Value)
                        
                            <span class="text-success">Online!</span>
                        
                        else
                        
                            <span class="text-danger">Offline</span>
                        
                    </td>
                </tr>
            
        
    </tbody>
</table>

您可以像在服务器端一样使用HttpClient 发出 HTTP 请求,但显然它会被转换为来自浏览器的 HTTP 请求,因此请记住存在限制。更多文档在这里:Call a web API from ASP.NET Core Blazor

首先,确保您注册了 HttpClient 服务。在默认模板中应该已经有一个,但如果没有:

builder.Services.AddScoped(sp => 
    new HttpClient
    
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    );

在您的 Blazor .razor 页面中:

// Inject the HttpClient service
@inject HttpClient Http;

// ...

// Request
List<TestUser>? users;

protected override async Task OnInitializedAsync()

    var resObj = await this.Http.GetFromJsonAsync<TestResponse>("https://reqres.in/api/users?page=2");
    this.users = resObj?.Data;


class TestResponse


    public List<TestUser> Data  get; set; 



class TestUser

    public int Id  get; set; 
    public string First_Name  get; set; 
    public string Last_Name  get; set; 


如何显示结果数据由您决定。见Razor syntax reference for ASP.NET Core。例如:

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>ID > 10?</th>
        </tr>
    </thead>
    <tbody>
        @if (this.users != null)
        
            foreach (var user in this.users)
            
                <tr>
                    <td>@(user.First_Name) @(user.Last_Name)</td>
                    <td>@(user.Id > 10 ? "Yes" : "No")</td>
                </tr>
            
        
    </tbody>
</table>

【讨论】:

【参考方案4】:

解决方案的概要如下:

    创建一个服务来检查特定 url 是在线还是离线(发出 http 请求 => 检查状态代码 => 返回特定于应用程序的响应)
public class StatusChecker

   public Task<bool> CheckAsync(string deviceId)
   
     // check status by pinging server and return the status
   

    将此服务声明为作用域或单例(取决于您的逻辑的线程安全、隐私等)

    在您的 Blazor 页面中注入此服务,为您的 deviceIds 列表中的每个调用 CheckAsync

 @inject StatusChecker Checker
     @if(IsCheckingStatus)     
         Checking.....   \else    
      <table>     <tbody>
  
    foreach(var device in DeviceIds)   
      <tr>
        <td @device </td
        <td @statuses[device] </td> </tr>         </tbody>      </table>  // if you want to manually refresh again
      <button @onclick="CheckStatuses"Refresh</button>
       
  
  
     @code     
       Dictionary<string,string statuses = new ();    
       bool IsCheckingStatus;    
       [Parameter]    
       public List<string DeviceIds get;set;
  
     protected override async Task OnInitializedAsync()    
        await CheckStatuses();    
  
     async Task CheckStatuses()    
        IsCheckingStatus = true;
        foreach(var devId in DeviceIds)
        
          var stat = await Checker.CheckAsync(devId);
          statuses[devId] = stat ? "online" : "offline";
        
        IsCheckingStatus = false;    
     

附:有人可以格式化吗? SO 根本无法在 Edge 上格式化 Blazor 代码。

【讨论】:

以上是关于在 foreach 循环 Blazor 中触发网络请求的主要内容,如果未能解决你的问题,请参考以下文章

forEach 循环中的 promise.all —— 一切立即触发

为啥这个 foreach 循环缺少类中的属性?

将数据从枚举传递到在ForEach循环中触发的sheet中。

如何在 foreach 循环之前/之后触发代码,只有在 PHP 7.4+ 中以一种有效的方式输入这个循环?

根据页面的顶部和左侧位置在页面上显示 Blazor 组件

AngularJS - ForEach 不循环