Рубрики
Blazor

Упрощаем Lifetime Blazor приложения

Исходя из проблематики предыдующей статьи, у меня появились следующие наработки.

30% моих компонентов инициализируются через метод OnInitialized(), а все остальные 70% через оверрайдинг метода OnComponentMountAsync(). Такого метода не существует, но когда я поддержал его в своей компонентной базе, то перестал задумываться о проблемах с построением асинхронных кейсов внутри приложения.

В качестве основы я создал интерфейс IAsyncComponentBase.cs, который оговаривает, что компоненты, наследующие данный интерфейс имеет доступ к специальному делегату. Его вызов приводит к попытке перерендеривания страницы.

using System.Threading.Tasks;

namespace blazor_lifetime.Models
{
    public delegate Task RenderHandleAsync();
    public interface IAsyncContentRenderer
    {
        RenderHandleAsync RenderChildContentAsync { get; set; }
    }
}

Для того чтобы обеспечить прозрачную поддержку асинхронного рендеринга компонента, необходимо создать вспомогательный компонент AsyncContentSupport.razor. Он будет оберткой над контентом асинхронных компонентов.

@using blazor_lifetime.Models

@if (_isContentVisible)
{
    @ChildContent
}
else
{
    @if (LoadingContent != null)
    {
        @LoadingContent
    }
}

@code{
    private bool _isContentVisible = false;

    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public RenderFragment LoadingContent { get; set; }
    [Parameter] public IAsyncComponentBase Component { get; set; }

    protected override void OnInitialized()
    {
        if (Component == null)
        {
            throw new Exception(nameof(Component));
        }
        Component.RenderChildContentAsync += OnRenderChildContentAsync;
    }

    protected async Task OnRenderChildContentAsync()
    {
        _isContentVisible = true;
        await InvokeAsync(StateHasChanged);
    }
}

Далее опишем замену базовому классу компонента. Сначала я использовал наследование от ComponentBase, а затем, когда появился OwningComponentBase, я начал использовать его.

Кстати, использование OwningComponentBase очень полезно при проектировании приложений на базе Blazor Server Side, потому что у вас всегда есть IDisposable интерфейс.

И да, еще одно мелкое замечание. ComponentBase выгружается не сразу, что не иногда не очень то и удобно. А вот Owning… версия крайне позитивна в данном вопросе. Как только пользователь закрывает страницу с компонентом, то у него сразу происходит вызов Dispose() и выгрузка самого компонента.

Я назвал этот класс AdvancedComponentBase и именно он должен имплемнтировать ранее указанный мною интерфейс IAsyncComponentBase.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using blazor_lifetime.Models;

namespace blazor_lifetime.Shared
{
    public class AdvancedComponentBase : OwningComponentBase, IAsyncComponentBase
    {
        [Parameter] public virtual RenderFragment ChildContent { get; set; }
        public RenderHandleAsync RenderChildContentAsync { get; set; }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                await OnComponentMountAsync().ConfigureAwait(false);
                if (RenderChildContentAsync != null)
                {
                    await RenderChildContentAsync();
                }
            }
            else
            {
                await Task.CompletedTask;
            }
        }

        protected virtual Task OnComponentMountAsync()
            => Task.CompletedTask;
    }
}

А теперь на красочном примере компонента я хочу объяснить, как же теперь пользоваться этим компонентом поддержки и как он работает внутри.

Как только страница впервые начинает отрисовываться, то наш вспомогательный компонент отрисовывает либо LoadingContent либо не отрисовывает вообще ничего. Также он передает ручку управления собой в основной компонент (RenderHandler=@this) и вызывает у основного компонента OnComponentMountAsync().

Завершение выполенения OnComponentMountAsync() приводит к тому, что наш основной компонент пытается теперь вызывать отрисовку ChildContent в вспомогательном интерфейсе, в котором и расположена та часть кода, которая крайне зависима от асинхронности.

@inherits FirstLevelAdvancedComponentAsyncModel

<AsyncContentSupport RenderHandler="@this">
    <LoadingContent>
        Please wait... Content is loading...
    </LoadingContent>
    <ChildContent>
        <div>First Component Start</div>
        <SecondLevelComponentAsync Parameter=@AsyncParameter />
        <div>First Component End</div>
    </ChildContent>
</AsyncContentSupport>

Такой подход конечно требует некоторое время на разбор и применение в работе. Однако, дальнейшее его использование открывает самые сильные стороны этого подхода.

  • Внутренние компоненты всегда будут получать параметры, которые появляются в ходе выполнения асинхронного кода
  • Теперь в компонент невероятно просто встроить статический контент, который будет показываться при подгрузке — это может быть и обычный прогресс бар или же аккуратные заглушки
  • При данном подходе, первый рендер уже закончился, а значит обращение к JsInvoke внутри OnComponentMountAsync() не будет выбрасывать исключений

В качестве примера, все виды подходов к рендерингу были опубликованы в репозитории: https://git.defend.pw/defend/blazor-lifetime

1 ответ к “Упрощаем Lifetime Blazor приложения”

Если по окончании вызова асинхронного метода добавить строку
StateHasChanged();
не тоже самое получается?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *