2

在我的 razor 组件中,我使用了一个Virtualize组件(此处的文档)ItemsProviderDelegate该组件被实现为一种async从 API 批量加载数据传输对象 (DTO) 的方法。该方法看起来像这样:

private async ValueTask<ItemsProviderResult<Dto>> LoadDtosAsync(ItemsProviderRequest request)
{
    // Massage parameters and dispatch action
    // _stateFacade is essentially a wrapper around 
    // Dispatcher.Dispatch(new LoadDtosAction())

    _stateFacade.LoadDtos(request.StartIndex, request.Count);

    // I make the assumption here that 'IsLoading' is immediately 
    // 'true' after the LoadDtosAction is dispatched. I think
    // that this is probably a bad assumption because
    // Dispatcher.Dispatch() is probably not synchronous
    // under-the-hood.

    // dtoState is an IState<Dto>
    while(_dtoState.Value.IsLoading)
    {
        // My current 'solution' to wait for the data to be loaded.
        await Task.Delay(100);
    }

    // Provide the items to the Virtualize component for rendering...
    return new ItemsProviderResult<Dto>(
        _dtoState.Value.VirtualizedDtos ?? new List<Dto>(),
        _dtoState.Value.DtosServerCount ?? 0
    );

}

这已被证明是一种有效的方法来渲染来自后端模型集合的批量数据,这些数据可能非常大,同时保持请求大小很小。客户端应用程序一次只需要从 API 请求少量对象,而 UI 不需要愚蠢的“页面”控件,因为用户可以直观地滚动浏览显示数据的组件。

Fluxor 用于管理客户端应用程序的状态,包括组件已请求的当前 DTO Virtualize。这抽象了从 API 请求批量 DTO 的逻辑,并允许根据哪个组件调度操作来触发副作用。

应用程序中的许多Action类型都有一个object? Sender属性,其中包含对调度操作的组件的引用。当组件中发送所需操作的原始方法不需要从操作返回的结果状态时,此方法有效。然后效果可以根据发送动作的组件的类型调用回调方法,例如:

public class UpdateDtoEffect : Effect<UpdateDtoSuccessAction>
{
    protected override async Task HandleAsync(UpdateDtoSuccessAction action, IDispatcher dispatcher)
    {
        var updateDtoForm = action.Sender as UpdateDtoForm;
        if (updateDtoForm is not null)
        {
            await updateDtoForm.OnSuccessfulUpdate.InvokeAsync();
        }
    }
}

OnSuccessfulUpdate被上述效果调用时,此操作的减速器将更新状态,因此回调方法可以依赖最新的状态信息。

AnItemsProviderDelegate对这种方法提出了一个有趣的例外。为了正确实现委托,我们需要返回项目列表和服务器上可用项目的数量。LoadDtosAction此信息存储在此功能的状态中,当成功时由减速器更新。在当前的实现中(上面一般表示),该LoadDtosAsync方法做了两个我不喜欢的假设:

  1. 状态值在被分派isLoading时立即设置为 true 。LoadDtosAction我不认为这总是正确的,因此组件有时会立即询问状态值以更新自身(这将导致显示先前的状态而不是结果状态)。

  2. 生成的 action-reducer-effect 链最终会将状态isLoading值更新为false

是否有一种方法可以让ItemsProviderDelegate实现分派LoadDtosAction并“等待”操作的结果以返回ItemsProviderResult

  • 编辑 - 动作流程如下所示:
LoadDtosAction => 
LoadDtosActionReducer (new state, 'isLoading':true) =>
LoadDtosActionEffect (performs asynchronous API call) =>
LoadDtosSuccessAction =>
LoadDtosSuccessActionReducer (new state, 'VirtualizedDtos':{IEnumerable<Dto>}, 'DtosServerCount':{int})
LoadDtosSuccessEffect (perform optional asynchronous callbacks to 'Sender')
4

1 回答 1

1

For future archeologists, the best solution I could come up with was to add Fluxor's IActionSubscriber via DI into my component (with the virtualized list which is managed by a redux state) and subscribe to the success/failure actions dispatched when the LoadDtosActionEffect attempts to talk to the API to retrieve the DTOs. The component declares a simple boolean flag which is immediately set to true in LoadDtosAsync, the actions registered with the action subscriber simply set this flag to false when the success/failure actions are dispatched.

I suspect, since Blazor WASM is single-threaded, this flag should not be concurrently modified. I discovered this when I tried to use System.Threading.EventWaitHandle to block while waiting for the DTOs to load.

Pro-tip: don't block in Blazor WASM, you will only achieve deadlock in your application.

The biggest note here is to add a timeout to this code, if some future modification breaks the chain of actions/effects upon which the action subscriptions rely, the loop will still exit and simply use the incorrect state. This result is more desirable than slowly building up tons of concurrent "threads" (which aren't really threads) in the async/await schedule which will just end up eating cycles and killing performance.

The resulting code to wait for an action (or subsequent effects/actions dispatched) to complete:

// Private class member declaration
private bool _refreshingDtos;

// Called in OnInitialized() or OnInitializedAsync()
private void SubscribeToActions()
{
    _actionSubscriber.SubscribeToAction<LoadDtosSuccessAction>(this, action =>
    {
        _refreshingDtos = false;
    });
    _actionSubscriber.SubscribeToAction<LoadDtosFailureAction>(this, action =>
    {
        _refreshingDtos = false;
    });
}

private async ValueTask<ItemsProviderResult<Dto>> LoadDtosAsync(ItemsProviderRequest request)
{
    _stateFacade.LoadDtos(request.StartIndex, request.Count);
    _refreshingDtos = true;

    var delay = 100;
    var timeout = 0;
    while(_refreshingDtos && timeout < 30000)
    {
        await Task.Delay(delay);
        timeout += delay;
    }

    return new ItemsProviderResult<Dto>(
        _dtoState.Value.VirtualizedDtos ?? new List<Dto>(), _dtoState.Value.DtoServerCount ?? 0
    );
}


// Component class implements IDisposable
// to make sure the component is unsubscribed (avoid leaking reference)
void IDisposable.Dispose()
{
    if (_actionSubscriber is not null)
    {
        _actionSubscriber.UnsubscribeFromAllActions(this);
    }
}
于 2021-02-11T21:44:12.663 回答