Blazor 子组件渲染和参数更改

Blazor Child Component Rendering and Parameter Changes

我有一个接受 int 参数的组件 - 该组件使用该参数进行 API 调用以检索一些数据。此逻辑当前在组件的 OnParametersSetAsync().

此组件还有一个复杂类型的参数。

当此组件被父组件使用并出于各种原因重新呈现自身时,将在该子组件上调用 OnParametersSetAsync() - 即使其参数 none 已更改。我的理解是,这是因为复杂类型的参数(blazor 无法判断它是否发生了变化,所以它假定它发生了变化)。

这导致 API 调用不必要地重新触发(实际的 int 参数没有改变)。

像这样进行数据检索不适合 OnParametersSetAsync() 吗?如果是这样,我应该如何更改我的组件以使用 Blazor 框架?

父组件

调用 ChangeName() 触发父级的重新渲染

<div>
    <EditForm Model="favoriteNumber">
        <InputSelect @bind-Value="favoriteNumber">
            <option value="0">zero</option>
            <option value="1">one</option>
            <option value="2">two</option>
            <option value="3">three</option>
        </InputSelect>
    </EditForm>
    
    @* This is the child-component in question *@
    <TestComponent FavoriteNumber="favoriteNumber" FavoriteBook="favoriteBook" />
    <br />

    <EditForm Model="person">
        First Name:
        <InputText @bind-Value="person.FirstName" />
        <br />
        Last Name:
        <InputText @bind-Value="person.LastName" />
    </EditForm>

    <button @onclick="ChangeName">Change Name</button>
</div>

@code {
    private int favoriteNumber = 0;
    private Book favoriteBook = new();
    private Person person = new() { FirstName = "Joe", LastName = "Smith" };

    private void ChangeName()
    {
        person.FirstName = person.FirstName == "Susan" ? "Joe" : "Susan";
        person.LastName = person.LastName == "Smith" ? "Williams" : "Smith";
    }
}

子组件

<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    protected override async Task OnParametersSetAsync()
    {
        infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
    }
}

您可以使用私有 int 实现自己的状态逻辑。
比再次调用 API 便宜很多。

<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    private int currentNumber = -1; // some invalid value

    protected override async Task OnParametersSetAsync()
    {
      if (currentNumber != FavoriteNumber)
      {
        currentNumber = FavoriteNumber;
        infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
      }
    }
}

我认为将此逻辑放入 OnParametersSetAsync() 方法中不一定是不好的做法。但是有一种方法可以防止它进行如此多的 API 调用。我会创建一个私有变量来存储 public 参数的值,然后每次调用 OnParametersSetAsync() 方法时比较这两个变量,如果它们相同,则不进行API 调用,如果它们不同,则进行 API 调用,完成后,将私有变量分配给 public 参数的值。考虑到组件第一次调用该方法,我可能会将私有变量默认分配给 -1,因为通常 ID 值不是负数。但基本上我会将它分配给一个永远不会等于作为参数传递的任何值的值。否则第一次调用你的 API 可能不会真正被调用。这是一个例子:

<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }
    private int CurrentFavoriteNumber  { get; set; } = -1; 

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    protected override async Task OnParametersSetAsync()
    {
        if (FavoriteNumber != CurrentFavoriteNumber)
        {
            infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
            CurrentFavoriteNumber = FavoriteNumber;
        }
    }
}

which the component uses to make an API call to retrieve some data.

您的子组件不应执行任何 API 调用。父组件应该管理父组件本身及其子组件的状态,downstreaming 数据。如果事情变得复杂,那么您将不得不实施处理状态的服务。 @Peter Morris 肯定会建议您使用 Blazor State Management Using Fluxor.

不确定为什么要使用两个 EditForm 组件,而实际上应该使用 none。意识到组件非常昂贵,它们会使您的代码变慢。所以明智地使用它

回答您的问题:

在子组件中定义一个本地字段来保存 FavoriteNumber 参数 属性 的值,如下所示:

 @code 
  {
       [Parameter]
       public int FavoriteNumber { get; set; }
       private int FavoriteNumberLocal = -1;
  }

注意:FavoriteNumberLocal 变量存储从父组件传递的值。它允许您在本地存储并检查其值是否已更改,并据此决定是否调用 Web Api 端点(同样,您不应该这样做)

protected override async Task OnParametersSetAsync()
{
    if( FavoriteNumberLocal != FavoriteNumber)
    { 
         FavoriteNumberLocal = FavoriteNumber;

         infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: 
         FavoriteNumberLocal.ToString());
    }
}  

阅读最后两条评论 question

你可以引入本地字段并像其他建议的那样比较它的值,或者在它改变之前捕获旧值SetParametersAsync,它会在基本场景中工作。

但是,如果:

  • 参数变化太快?您将收到并发请求,并且响应的顺序可能有误。
  • 您离开页面后才收到回复?
  • 您想延迟或限制参数更改,例如当参数绑定到用户输入时。

Reactive Extensions (IObservable) 就是专门为处理这种情况而设计的。在 Angular(与 Blazor 非常相似)中,RxJS 是第一个 class 公民。

在Blazor中,只需将参数转成IObservable,使用RX Operators即可处理,无需引入自己的局部变量

readonly Subject<Unit> _parametersSet = new ();

protected override Task OnParametersSetAsync()
{
    _parametersSet.OnNext(Unit.Default); //turn OnParametersSetAsync into Observable stream
     return base.OnParametersSetAsync();
}


[Parameter] public int FavoriteNumber { get; set; }

protected override void OnInitialized()
{
    _parametersSet.Select(_ => FavoriteNumber) //turn parameter into Observable
        .DistinctUntilChanged() //detect changes
        .Select(value => Observable.FromAsync(cancellationToken => 
        {
            Console.WriteLine($"FavoriteNumber has changed: {value}");
            infoAboutFavoriteNumber = await ApiService.GetAsync(value, cancellationToken);
        })
        .Switch() //take care of concurrency
        .Subscribe();
}

它的好处是,您可以使用所有样板文件创建可重用的 class 或辅助方法。您只需指定一个参数和异步方法,例如:

Loader.Create(ObserveParameter(() => FavoriteNumber), LoadAsync);

如需更多阅读,请查看:

您面临一个常见问题:在 UI 中进行数据和数据访问 activity。事情往往会变得一团糟!在这个答案中,我将数据与组件分开。数据和数据访问驻留在依赖注入服务中。

我也取消了 EditForm 因为你实际上并没有使用它,并且将 Select 更改为简单的 select 这样我们就可以捕获更新,更新建模并触发服务中的数据检索。这也意味着组件在模型更新后获得 re-rendered。 OnChanged 事件的 Blazor UI 事件处理程序在调用 NumberChanged.

之后调用 StateHasChanged

首先 class 我们的收藏夹数据。

public class MyFavourites
{
    public int FavouriteNumber { get; set; }    

    public string FavouriteNumberInfo { get; set; } = string.Empty;
}

第二个 DI 服务来保存我们的收藏夹数据和数据存储操作。

namespace Server;

public class MyFavouritesViewService
{
    public MyFavourites Favourites { get; private set; } = new MyFavourites();

    public async Task GetFavourites()
    {
        // Emulate a database get
        await Task.Delay(100);
        Favourites = new MyFavourites { FavouriteNumber = 2, FavouriteNumberInfo = "The number is 2" };
    }

    public async Task SaveFavourites()
    {
        // Emulate a database save
        await Task.Delay(100);
        // Save code here
    }

    public async Task GetNewNumberInfo(int number)
    {
        if (number != Favourites.FavouriteNumber)
        {
            // Emulate a database get
            await Task.Delay(100);
            Favourites.FavouriteNumberInfo = $"The number is {number}";
            Favourites.FavouriteNumber = number;
        }
    }
}

接下来在程序中注册服务:

builder.Services.AddScoped<MyFavouritesViewService>();

组件:

<h3>MyFavouriteNumber is @this.Favourites.FavouriteNumber</h3>

<h3>MyFavouriteNumber info is @this.Favourites.FavouriteNumberInfo</h3>

@code {
    [Parameter][EditorRequired] public MyFavourites Favourites { get; set; } = new MyFavourites();
}

最后是页面。注意我使用 OwningComponentBaseMyFavouritesViewService 的范围与组件生命周期联系起来。

@page "/favourites"
@page "/"
@inherits OwningComponentBase<MyFavouritesViewService>

@namespace Server

<h3>Favourite Number</h3>

<div class="p-5">
        <select class="form-select" @onchange="NumberChanged">
            @foreach (var option in options)
        {
            if (option.Key == this.Service.Favourites.FavouriteNumber)
            {
                <option selected value="@option.Key">@option.Value</option>
            }
            else
            {
                <option value="@option.Key">@option.Value</option>
            }
        }
        </select>
<div>
    <button class="btn btn-success" @onclick="SaveFavourites">Save</button>
</div>
</div>
<MyFavouriteNumber Favourites=this.Service.Favourites />

@code {
    
    private Dictionary<int, string> options = new Dictionary<int, string>
    {
        {0, "Zero"},
        {1, "One"},
        {2, "Two"},
        {3, "Three"},
    };

    //  Use OnInitializedAsync to get the original values from the data store
    protected async override Task OnInitializedAsync()
    =>  await this.Service.GetFavourites();

    // Demo to show saving
    private async Task SaveFavourites()
        => await this.Service.SaveFavourites();


    // Async setup ensures GetNewNumberInfo runs to completion
    // before StatehasChanged is called by the Handler
    // Renderer the checks what's changed and calls SetParamaterAsync 
    // on MyFavouriteNumber because FavouriteNumber has changed
    private async Task NumberChanged(ChangeEventArgs e)
    {
        if (int.TryParse(e.Value?.ToString(), out int value))
            await this.Service.GetNewNumberInfo(value);
    }
}