In this tip, you will learn how to use a generic method to add a spinner to a library method running in a Console application.
Introduction
A spinner is used in conjunction with a time-consuming silent method to give an indication that work is progressing and that the application has not stalled. Adding a spinner to the source code is straight forward, it's just a question of making sure that the marathon method outputs some sort of active display as it progresses. It's not quite so simple when the method forms part of a sealed library as it's necessary to implement a degree of thread management between the library method running on one thread and the spinner running on a different thread.
A Spinner Class
One way that spinner functionality can be implemented is to instantiate a class that actions a new thread in its Start
method and runs some sort of active display on that thread until the long running method completes. Here’s an example:
public class Spinner
{
private readonly int delay;
private bool isRunning = false;
private Thread thread;
public Spinner(int delay = 25)
{
this.delay = delay;
}
public void Start()
{
if (!isRunning)
{
isRunning = true;
thread = new Thread(Spin);
thread.Start();
}
}
public void Stop()
{
isRunning = false;
}
private void Spin()
{
while (isRunning)
{
Console.Write('.');
Thread.Sleep(delay);
}
}
}
It can be used like this:
public class Program
{
static void Main()
{
int lifeValue=42;
var spinner = new Spinner();
spinner.Start();
int meaningOfLife = LongLifeMethod(lifeValue);
spinner.Stop();
Console.WriteLine($"\r\nThe meaning of life is {meaningOfLife}");
Console.ReadLine();
}
private static int LongLifeMethod(int lifeValue)
{
Thread.Sleep(3000);
return lifeValue;
}
}
There’s a hidden gotcha here. If the marathon method throws an exception, the Stop
method will not be run. So it’s best to call the Stop
method from inside a finally
block to make sure that the spinner always stops spinning. But this adds a bit more complexity to the code to the extent that the Spinner
class now looks like it needs a spinner service to manage it. There must be an easier way.
An Alternative Approach
The trick here is to use a method that's self-determining so that it switches itself off and can be used on a fire and forget basis. The Task-based Asynchronous Pattern can encapsulate all the required functionality within a single generic method.
public static TResult RunWithSpinner<TResult>
(Func<TResult> longFunc, Action<CancellationToken> spinner)
{
CancellationTokenSource cts = new();
Task.Run(() => spinner(cts.Token));
TResult result;
try
{
result = longFunc();
}
finally
{
cts.Cancel();
}
return result;
}
Using the Method
The method is used like this:
static void Main()
{
int lifeValue = 42;
int meaningOfLife = RunWithSpinner(() => LongLifeMethod(lifeValue), Spin);
Console.WriteLine($"\r\nThe meaning of life is {meaningOfLife}");
}
private static void Spin(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Console.Write('.');
Thread.Sleep(25);
}
}
The longFunc
parameter of the RunWithSpinner
method is expressed as a lambda expression. The empty brackets on the left side of the => characters signify that the required method’s signature has no parameters and the call to LongLifeMethod
leads the compiler to infer that the returned value is that method’s return value. So, at compile time, it will compile the lambda expression into an anonymous function that calls the LongLifeMethod
and returns an int
. Although the function itself does not take any parameters, it calls the LongLifeMethod
and uses the captured variable, lifeValue
, as a parameter for that method. The technique of using captured variables in this manner is very powerful and is commonly used in Linq expressions.
Conclusion
Generic methods can be useful for encapsulating, in a few lines of code, the kind of functionality that usually requires a class
instance. In this case, the RunWithSpinner
method removes the need for a Spinner
class along with the code that's needed to manage it.
History
- 16th June, 2021: Initial version