Sometimes we have a task which takes a bit longer. Just imagine you want to grab a big amount of data, process this data and then make it visible to the user. If this takes a bit of time nothing really happens from the eyes of the user. So lets create a reusable loading bar component in Blazor.
The loading bar component
We will keep it simple for the beginning. Feel free to tweak everything you want. As Blazor comes with Bootstrap, we will use Bootstrap for our loading bar itself. So let's create a new Loading
razor component:
<!-- 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>
}
We need a few things here to do:
- We have to switch the visibility of our loading bar component and of our "real" rendered component. As said we will keep it very simple. You can adjust this to your needs.
- We don't want to know the "real" component from the eyes of the
LoadingComponent
otherwise it wouldn't be really reusable. - The "real" component has to set the title, total steps and the current step from within its component.
The first step is we need a container which models some of the requirements. This container is hosted by the LoadingComponent
and gets passed down to the real component. The LoadingComponent
also has to listen to changes when the container changes. For all of you which worked with WPF or UWP you will feel "at home". We will use the INotifyPropertyChanged
interface.
This is our model:
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));
}
}
We have the 3 values which we need for the LoadingComponent
: Title
, CurrentStep
and TotalSteps
. If one of those is changed we will invoke the OnProperyChanged
event which then can be picked up by our LoadingComponent
to refresh the state.
With this information we can model:
<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();
}
}
What it does is basically, we will make the loading bar visible when the IsLoading
flag is set to true
and show all the information to model the loading bar itself including the title, steps and so on.
Now you might wonder why I don't take something like @if(LoadingConfiguration.IsLoading) { } else { }
. The reason is simple. If we set IsLoading
to true it will show our loading bar, that is nice, but it will also remove our "real" component from the render tree. This is just how Blazor works. Therefore both components should be in the render tree all the time but both should not be visible at the same time.
Now the last bit: Add the "real" component placeholder:
<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();
}
}
That is nice. Now we could use our component. But wait there is one thing missing! We need to pass down our LoadingConfiguration
down to the ChildContent
. To do so we will leverage CascadingValue
:
<div style="display: @DisplayClassChildContent">
<CascadingValue Value="@LoadingConfiguration">
@ChildContent
</CascadingValue>
</div>
That is all! We are done with our loading bar. Now we can use it in action. Lets create a new HeavyLoadComponent
which does some stuff and propgates it changes via the LoadingConfiguration
to its parent.
<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;
}
}
The LoadingConfiguration
gets picked up by the child component via CascadingParameterAttribute
. Now everytime we do a change the LoadingComponent
will see the change via the event and therefore updates the UI to the new set values!
The usage is super simple:
<LoadingComponent>
<HeavyWorkComponent></HeavyWorkComponent>
</LoadingComponent>
The result:
Awesome!
Conclusion
We wrote a nice component which does loading bar stuff for us without knowing the real child component. Also the child component does not have an idea about his parent.
Resources
- This sample on GitHub