Scenario
In any kind of real-world application, you will need to eventually display a loading spinner. Usually pretty early on and usually pretty often. Any time a page is loading, any time you click a button or interact with any other type of UI control.
The Problem
Have you ever seen this kind of code and progression?
It begins very simple.
IsLoading = true;
var data = await CallExternalService();
IsLoading = false;
Now let's add a standard programming pattern with early returns.
3/8 lines of actual code here are reserved for loading state management.
IsLoading = true;
var result = await CallExternalService();
if (result is "cool")
{
var differentResult = await GetDataFromSomewhereElse();
IsLoading = false;
return differentResult;
}
IsLoading = false;
return result;
Can't forget error handling.
3/7 lines of actual code here are reserved for loading state management.
try
{
IsLoading = true;
var data = await CallExternalService();
IsLoading = false;
}
catch (Exception ex)
{
// log..
IsLoading = false;
}
If you combine the two and add more real-life code than just a single call to an external service it gets even messier. I know I have seen this type of code hundreds of times. You probably did too.
The Solution
What we will end up with is this.
private async Task GetDataFromSomwhere()
{
using var _ = DisplayLoading();
var result = await CallExternalService();
}
The first line here sets IsLoading = true at the beginning of the method and IsLoading = false at the end of the method. No matter what happens or where exactly you exit the method.
If you just want the code, here it is in a blazor page.
public partial class BasePage: ComponentBase, IBasePage
{
public bool IsLoading { get; set; } = false;
protected LoadingHelper DisplayLoading() => new LoadingHelper(this);
}
public class LoadingHelper : IDisposable
{
private IBasePage basePage;
public LoadingHelper(IBasePage basePage)
{
this.basePage = basePage;
basePage.IsLoading = true;
}
public void Dispose()
{
basePage.IsLoading = false;
}
}
How it works
The LoadingHelper implements IDisposable
. IDisposable
is typically implemented by classes that need to do their own cleanup. It's usually heavier classes like SqlConnections or file access.
The Dispose()
method runs when the class gets cleaned up by the GarbageCollector
.
When the BasePage
creates an instance of the LoadingHelper
in a local method, that instance will get cleaned up at the end of the method. Always.
So this is where I ultimately set IsLoading = false.
I am using some c# syntax sugar to get the code as terse as possible.
This is actually the same code.
private async Task GetDataFromSomewhere()
{
using (var loadingHelper = DisplayLoading())
{
var result = await CallExternalService();
}
}
The first one is the _ discard operator. If you don't need access to a local variable, you can simply assign it to _.
The second one is the using declaration
. A using declaration
is scoped to the surrounding scope. In this case the method.
How I got there
My first attempt was just writing a wrapping method. Something like this.
private T WrapInLoading<T>(Func<T> func)
{
IsLoading = true;
var res = func.Invoke();
IsLoading = false;
return res;
}
This leads to client code looking like this
private async Task GetDataV5()
{
await WrapInLoading(async () =>
{
//...
//...
var data = await CallExternalService();
});
}
This is not terrible. However not that pretty either. It also can get pretty complicated if you want synchronous and asynchronous code with and without return values. Handling void, T, Task and Task is certainly possible, however not simple.
Ultimately this just "looked like" the standard using syntax to me and I dreamed up a syntax I would like to have.
Something like
using(Loading){
}
I like what I ended up with even more and it actually compiles.
It even avoids the extra level of nesting.
What I would love to have now is using(Loading);
however, I don't think it is possible with the current c# language features.
Top comments (0)