Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / C#
Tip/Trick

How to Action a Spinner in a Console Application

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
16 Jun 2021CPOL2 min read 11.9K   3   14
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)


Written By
Student
Wales Wales
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionMisunderstanding of try/finally and object lifetime Pin
Stacy Dudovitz19-Jun-21 22:07
professionalStacy Dudovitz19-Jun-21 22:07 
AnswerRe: Misunderstanding of try/finally and object lifetime Pin
George Swan20-Jun-21 1:52
mveGeorge Swan20-Jun-21 1:52 

Thanks for your observations and advice. Stephen Toub, in response to a question raised here, states that "It's very rare for CancellationToken.WaitHandle to be used, so cleaning up after it typically isn't a great reason to use Dispose." In view of this, I will remove the call to that method.


With regard to your assertion that finally blocks are not always called, this document from Microsoft clearly states that "A finally block always executes, regardless of whether an exception is thrown." As far as I can see, the only rebuttals to that assertion involve cases where the application crashes.


Have you tested your example of the way the method should be coded? There does not seem to be a call to CancellationTokenSource.Cancel() and it seems to me that the spinner will just keep on spinning. The reason I put the cancel method inside a finally block is to make sure that the spinner is always stopped. You have appeared to have overlooked the fact that, as stated here, "Calling Dispose does not communicate a request for cancellation to consumers of the associated Token."

Thanks again for your input and best wishes.


GeneralRe: Misunderstanding of try/finally and object lifetime Pin
Stacy Dudovitz21-Jun-21 8:37
professionalStacy Dudovitz21-Jun-21 8:37 
GeneralRe: Misunderstanding of try/finally and object lifetime Pin
George Swan21-Jun-21 12:14
mveGeorge Swan21-Jun-21 12:14 
SuggestionMisleading article title Pin
Stylianos Polychroniadis17-Jun-21 4:47
Stylianos Polychroniadis17-Jun-21 4:47 
GeneralRe: Misleading article title Pin
George Swan17-Jun-21 5:23
mveGeorge Swan17-Jun-21 5:23 
GeneralRe: Misleading article title Pin
Gary R. Wheeler21-Jun-21 11:19
Gary R. Wheeler21-Jun-21 11:19 
GeneralRe: Misleading article title Pin
George Swan21-Jun-21 21:43
mveGeorge Swan21-Jun-21 21:43 
GeneralRe: Misleading article title Pin
Member 1323303720-Jun-22 21:10
Member 1323303720-Jun-22 21:10 
GeneralRe: Misleading article title Pin
George Swan21-Jun-22 1:13
mveGeorge Swan21-Jun-22 1:13 
GeneralRe: Misleading article title Pin
sx200822-Jun-21 11:15
sx200822-Jun-21 11:15 
GeneralRe: Misleading article title Pin
George Swan22-Jun-21 13:05
mveGeorge Swan22-Jun-21 13:05 
GeneralRe: Misleading article title Pin
Stylianos Polychroniadis22-Jun-21 23:01
Stylianos Polychroniadis22-Jun-21 23:01 
GeneralRe: Misleading article title Pin
George Swan23-Jun-21 2:04
mveGeorge Swan23-Jun-21 2:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.