Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

How to Action a Spinner in a Console Application

4.00/5 (2 votes)
16 Jun 2021CPOL2 min read 13.9K  
How to add a Spinner to a long-running library method
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:

C#
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:

C#
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.

C#
public static TResult RunWithSpinner<TResult>
(Func<TResult> longFunc, Action<CancellationToken> spinner)
 {
     CancellationTokenSource cts = new();
     //run the spinner on its own thread
     Task.Run(() => spinner(cts.Token));
     TResult result;
     try
     {
         result = longFunc();
     }
     finally
     {
         //cancel the spinner
         cts.Cancel();
     }
     return result;
 }

Using the Method

The method is used like this:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)