Here is my approach to creating a loading text spinner in the console. I did what I thought was best in designing the class, which means there is probably a lot of room for feedback.
The class is below, along with it's usage inside a Utils class. You can also find it here on github if that's easier.
Concerns
I have posted a question on StackOverflow regarding thread-safety/other issues regarding the Task API, since my first usage of it is surely wrong.
Post-writing the code, I also considered returning strings from the Task instead of forcing it to be Console output, but did not implement this functionality.
I am primarily interested in your feedback concerning the code style and general design. Pointers on the asynchrony are welcome, but not mandatory for a good answer.
Layout
ConsoleLoadingText
: class to encapsulate functionality.
- Constants
- Defaults for
ProductName
,LoadingText
, andMillisecondsDelay
- Array of spinner pieces
- Defaults for
- Constructors
- Chained together constructors allow you to provide some or all of the data, relying on defaults to fill in the gaps. They all delegate to one constructor which does the work.
- Methods
Display
returns a Task which, when run, does the display workStop
stops the display
Utils
: Utility class
- Methods
CreateLoading
creates an instance of theConsoleLoadingText
class with the parameters passed in; uses optional parameters for maximum flexibility
Code
ConsoleLoadingText.cs
namespace Knoble.Utils
{
/// <summary>
/// A class that represents a possibily infinitely looping load screen.
/// It displays a product name, loading text, and spinner that spins for a given delay.
/// </summary>
public class ConsoleLoadingText
{
public const string DefaultProductName = "";
public const string DefaultLoadingText = "Loading...";
public const int DefaultMillisecondsDelay = 250;
static string[] spinner = { "|", "/", "-", "\\" };
readonly string productName, loadingText;
readonly int millisecondsDelay;
int i;
bool @continue = true;
/// <summary>
/// Initializes a new instance of the <see cref="T:Knoble.Utils.Loading"/> class.
/// Defaults to displaying "Loading... x" where the spinner (x) spins every quarter second.
/// </summary>
public ConsoleLoadingText () : this (DefaultProductName)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Knoble.Utils.Loading"/> class.
/// Defaults to displaying "{productName} Loading... x" where the spinner (x) spins every quarter second.
/// </summary>
/// <param name="productName">Product name.</param>
public ConsoleLoadingText (string productName) : this (productName, DefaultLoadingText)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Knoble.Utils.Loading"/> class.
/// Defaults to displaying "{productName} {loadingText} x" where the spinner (x) spins every quarter second.
/// </summary>
/// <param name="productName">Product name.</param>
/// <param name="loadingText">Loading text.</param>
public ConsoleLoadingText (string productName, string loadingText) : this (productName, loadingText, DefaultMillisecondsDelay)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Knoble.Utils.Loading"/> class.
/// Displays "{productName} {loadingText} x" where the spinner (x) spins every {millisecondsDelay} milliseconds.
/// </summary>
/// <param name="productName">Product name.</param>
/// <param name="loadingText">Loading text.</param>
/// <param name="millisecondsDelay">Milliseconds delay.</param>
public ConsoleLoadingText (string productName, string loadingText, int millisecondsDelay)
{
if (productName == null)
throw new ArgumentException (nameof (productName));
if (loadingText == null)
throw new ArgumentException (nameof (loadingText));
if (millisecondsDelay < 0)
throw new ArgumentException (nameof (millisecondsDelay));
this.productName = productName;
this.loadingText = loadingText;
this.millisecondsDelay = millisecondsDelay;
}
/// <summary>
/// Returns a task that, when running, continously prints the loading text.
/// </summary>
public Task Display ()
{
return Task.Run (() =>
{
@continue = true;
while (@continue)
{
Console.Write ($"\r{productName} {loadingText} {spinner[i]}");
i = (i + 1) % spinner.Length;
Thread.Sleep (millisecondsDelay);
}
});
}
/// <summary>
/// Stop this instance from displaying.
/// </summary>
public void Stop ()
{
@continue = false;
}
}
}
Utils.cs
namespace Knoble.Utils
{
public static class Utils
{
/// <summary>
/// Creates a <see cref="T:Knoble.Utils.Loading"/> object for displaying loading text to the console.
/// </summary>
/// <returns>The <see cref="T:Knoble.Utils.Loading"/> object.</returns>
/// <param name="productName">Product name.</param>
/// <param name="loadingText">Loading text.</param>
/// <param name="millisecondsDelay">Milliseconds delay.</param>
public static ConsoleLoadingText CreateLoading (string productName = ConsoleLoadingText.DefaultProductName, string loadingText = ConsoleLoadingText.DefaultLoadingText, int millisecondsDelay = ConsoleLoadingText.DefaultMillisecondsDelay)
{
return new ConsoleLoadingText (productName, loadingText, millisecondsDelay);
}
}
}