Componente de barra de carregamento reutilizável no Blazor

Imagem de capa Componente de barra de carregamento reutilizável no Blazor

Componente de barra de carregamento reutilizável no Blazor

Às vezes temos uma tarefa que demora um pouco mais. Imagine que você deseja obter uma grande quantidade de dados, processar esses dados e torná-los visíveis para o usuário. Se isso demorar um pouco, nada realmente acontece aos olhos do usuário. Então vamos criar um componente de barra de carregamento reutilizável no Blazor.

O componente da barra de carregamento

Vamos mantê-lo simples para o início. Sinta-se à vontade para ajustar tudo o que quiser. Como o Blazor vem com o Bootstrap, usaremos o Bootstrap para nossa própria barra de carregamento. Então, vamos criar um novo componente razor de carregamento:

<!-- This is our loading component -->
<div>
    <p>Some description here</p>

    <div  class="progress">
        <div  class="progress-bar progress-bar-striped"  role="progressbar"
          style="width: 0%"
          aria-valuenow="0"
          aria-valuemin="0"
          aria-valuemax="0"></div>
     </div>
 </div>


<div>
    <!-- This is our real component which we will show once the loading is done -->
</div>
}

Precisamos de algumas coisas aqui para fazer:

  1. Temos que mudar a visibilidade do nosso componente da barra de carregamento e do nosso componente renderizado "real". Como disse, vamos mantê-lo muito simples. Você pode ajustar isso às suas necessidades.
  2. Nós não queremos conhecer o componente "real" dos olhos do LoadingComponent caso contrário não seria realmente reutilizável.
  3. O componente "real" deve definir o título, o total de etapas e a etapa atual de dentro de seu componente.

O primeiro passo é que precisamos de um container que modele alguns dos requisitos. Esse contêiner é hospedado pelo LoadingComponent e é transmitido para o componente real. O LoadingComponent também precisa ouvir as alterações quando o contêiner é alterado. Para todos vocês que trabalharam com WPF ou UWP vão se sentir "em casa". Usaremos a interface INotifyPropertyChanged.

Este é o nosso modelo:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace  BlazorLoadingComponent.Shared;

public  class  LoadingConfiguration : INotifyPropertyChanged
{
     private  string _title;
     private  int _currentStep;
     private  int _totalSteps;
     private  bool _isLoading;

     public  string Title
     {
         get => _title;
         set
         {
            _title = value;
            OnPropertyChanged();
         }
     }

     public  int CurrentStep
     {
         get => _currentStep;
         set
         {
              _currentStep = value;
              OnPropertyChanged();
         }
     }

     public  int TotalSteps
     {
         get => _totalSteps;
         set
         {
              _totalSteps = value;
              OnPropertyChanged();
         }
     }

     public  bool IsLoading
     {
         get => _isLoading;
         set
         {
             _isLoading = value;
             OnPropertyChanged();
         }
     }

     public  int  GetPercentage()
     {
         return TotalSteps > 0 ? (int)((double)CurrentStep / (double)TotalSteps * 100) : 0;
     }

     public  event PropertyChangedEventHandler? PropertyChanged;

     protected  virtual  void  OnPropertyChanged([CallerMemberName] string? propertyName = null)
     {
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }
}                

Temos os 3 valores que precisamos para o LoadingComponent: Title, CurrentStep e TotalSteps. Se um deles for alterado, invocaremos o evento OnProperyChanged, que pode ser selecionado pelo nosso LoadingComponent para atualizar o estado.

Com essas informações podemos modelar:

<div  style="display: @DisplayClassLoading">
     <p>@LoadingConfiguration.Title</p>

     <div  class="progress">
         <div  class="progress-bar progress-bar-striped"  role="progressbar"
               style="width: @LoadingConfiguration.GetPercentage()%"
               aria-valuenow="@LoadingConfiguration.CurrentStep"
               aria-valuemin="0"
               aria-valuemax="@LoadingConfiguration.TotalSteps"></div>
    </div>
</div>

<div  style="display: @DisplayClassChildContent">
</div>

@code {
    private LoadingConfiguration LoadingConfiguration { get; set; } = new();

    private string DisplayClassLoading => LoadingConfiguration.IsLoading ? "initial" : "none";
    private string DisplayClassChildContent => LoadingConfiguration.IsLoading ? "none" : "initial";

    protected override void OnParametersSet()
    {
        // Everytime any property changes we update our UI
        LoadingConfiguration.PropertyChanged += (_, _) => StateHasChanged();
     }
}

O que ele faz é basicamente tornar a barra de carregamento visível quando o sinalizador IsLoading estiver definido como verdadeiro e mostrar todas as informações para modelar a própria barra de carregamento, incluindo o título, etapas e assim por diante. Agora você pode se perguntar por que eu não uso algo como @if(LoadingConfiguration.IsLoading) { } else { }. A razão é simples. Se definirmos IsLoading como verdade, ele mostrará nossa barra de carregamento, o que é bom, mas também removerá nosso componente "real" da árvore de renderização. É assim que o Blazor funciona. Portanto, ambos os componentes devem estar na árvore de renderização o tempo todo, mas não devem estar visíveis ao mesmo tempo.

Agora, a última parte: adicione o espaço reservado do componente "real":

<div style="display: @DisplayClassChildContent">
    @ChildCoontent
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
    
    private LoadingConfiguration LoadingConfiguration { get; set; } = new();

    private  string DisplayClassLoading => LoadingConfiguration.IsLoading ? "initial" : "none";
    private  string DisplayClassChildContent => LoadingConfiguration.IsLoading ? "none" : "initial";
    
    protected  override  void  OnParametersSet()
    {
         // Everytime any property changes we update our UI
         LoadingConfiguration.PropertyChanged += (_, _) => StateHasChanged();
    }
}

Isso é bom. Agora podemos usar nosso componente. Mas espere, está faltando uma coisa! Precisamos passar nosso LoadingConfiguration para ChildContent. Para fazer isso, vamos aproveitar o CascadingValue:

<div  style="display: @DisplayClassChildContent">
    <CascadingValue  Value="@LoadingConfiguration">
         @ChildContent
    </CascadingValue>
</div>

Isso é tudo! Terminamos a nossa barra de carregamento. Agora podemos usá-lo em ação. Vamos criar um novo HeavyLoadComponent que faz algumas coisas e propaga suas mudanças através do LoadingConfiguration para seu pai.

<h3>Roles:</h3>
<ul>
   @foreach (var role in _roles)
   {
       <li>@role</li>
    }
 </ul>

@code {

   [CascadingParameter]
   public LoadingConfiguration LoadingConfiguration { get; set; } = default!;
   
   private  string[] _roles = Array.Empty<string>();
   
   protected  override  async Task OnInitializedAsync()
   {
        // Here we will simulate some heavy work via Task.Delay
        LoadingConfiguration.TotalSteps = 3;
        LoadingConfiguration.Title = "Getting Data";
        LoadingConfiguration.IsLoading = true;
        
        await Task.Delay(1000);
        _roles = new[] { "Admin", "Co-Admin", "User" };
        
        LoadingConfiguration.CurrentStep++;
        LoadingConfiguration.Title = "Filter Data";
        
        await Task.Delay(1000);
        _roles = _roles.Where(s => s.Contains("Admin")).ToArray();
        
        LoadingConfiguration.CurrentStep++;
        LoadingConfiguration.Title = "Another step to prepare the matrix...";
   
        await Task.Delay(1000);
        
        LoadingConfiguration.CurrentStep++;
        LoadingConfiguration.Title = "Almost there...";
        
        await Task.Delay(1000);
        
        LoadingConfiguration.IsLoading = false;
    }
}

O LoadingConfiguration é selecionado pelo componente filho por meio de CascadingParameterAttribute. Agora, toda vez que fizermos uma alteração, o LoadingComponent verá a alteração por meio do evento e, portanto, atualizará a interface do usuário para os novos valores definidos!

O uso é super simples:

<LoadingComponent>
    <HeavyWorkComponent></HeavyWorkComponent>
</LoadingComponent>

O resultado:

Impressionante!

Conclusão

Nós escrevemos um bom componente que carrega coisas de barra para nós sem conhecer o componente filho real. Além disso, o componente filho não tem ideia sobre seu pai.

Recursos