Console Progress Bar with C#

csharp wallpaper

At work, I sometimes find myself needing to make Console applications in C# which take some time and I want to display a progress bar.

For example, I have a few console applications which parse dump files into objects. Another example is to insert the seed data into a database. Display a progress bar gives me the opportunity to understand the application is still running.

Usually, the progress bar representation is a simple incremental percentage display. However, I thought I’d create a generic method which would display an ASCII progress bar.

Example of Progress bar in the Console in C#
Example of Progress bar in the Console in C#

At work, I sometimes find myself needing to make Console applications which take some time. For example, I have a few console applications which parse dump files into objects and then insert the data into a database. Usually, I represent the progress of these applications with a simple incremental percentage display, however I thought I’d create a generic method which would display an ASCII progress bar.

using System;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    /// <summary>
    /// An ASCII progress bar
    /// </summary>
    public class ProgressBar : IDisposable, IProgress<double>
    {
        private const int blockCount = 10;
        private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8);
        private const string animation = @"|/-\";
        private bool showProgressBar = true;

        private readonly Timer timer;

        private double currentProgress = 0;
        private string currentText = string.Empty;
        private bool disposed = false;
        private int animationIndex = 0;


        public ProgressBar(bool ShowProgressBar = true)
        {
            showProgressBar = ShowProgressBar;
            timer = new Timer(TimerHandler);

            // A progress bar is only for temporary display in a console window.
            // If the console output is redirected to a file, draw nothing.
            // Otherwise, we'll end up with a lot of garbage in the target file.
            if (!Console.IsOutputRedirected)
            {
                ResetTimer();
            }
        }

        public void Report(double value)
        {
            // Make sure value is in [0..1] range
            value = Math.Max(0, Math.Min(1, value));
            Interlocked.Exchange(ref currentProgress, value);
        }

        private void TimerHandler(object state)
        {
            lock (timer)
            {
                if (disposed) return;


                string text = "";
                if (showProgressBar)
                {
                    int progressBlockCount = (int)(currentProgress * blockCount);
                    int percent = (int)(currentProgress * 100);
                    text = string.Format("[{0}{1}] {2,3}% {3}",
                           new string('#', progressBlockCount), 
                           new string('-', blockCount - progressBlockCount),
                           percent,
                           animation[animationIndex++ % animation.Length]);
                }
                else
                {
                    text = animation[animationIndex++ % animation.Length].ToString();
                }
                UpdateText(text);


                ResetTimer();
            }
        }


        private void UpdateText(string text)
        {
            // Get length of common portion
            int commonPrefixLength = 0;
            int commonLength = Math.Min(currentText.Length, text.Length);
            while (commonPrefixLength < commonLength &&
                   text[commonPrefixLength] == currentText[commonPrefixLength])
            {
                commonPrefixLength++;
            }


            // Backtrack to the first differing character
            StringBuilder outputBuilder = new StringBuilder();
            outputBuilder.Append('\b', currentText.Length - commonPrefixLength);


            // Output new suffix
            outputBuilder.Append(text.Substring(commonPrefixLength));


            // If the new text is shorter than the old one: delete overlapping characters
            int overlapCount = currentText.Length - text.Length;
            if (overlapCount > 0)
            {
                outputBuilder.Append(' ', overlapCount);
                outputBuilder.Append('\b', overlapCount);
            }


            Console.Write(outputBuilder);
            currentText = text;
        }


        private void ResetTimer()
        {
            timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1));
        }


        public void Dispose()
        {
            lock (timer)
            {
                disposed = true;
                UpdateText(string.Empty);
            }
        }
    }
}

Now, I can use this class for a progress bar with C# in my console application. In the following example, I only want to display the progress bar. You can replace the for cycles with your code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Performing some task... ");
            using (var progress = new ProgressBar())
            {
                for (int i = 0; i <= 1000; i++)
                {
                    progress.Report((double)i / 100);
                    Thread.Sleep(20);
                }
            }
            Console.WriteLine("Done.");
        }
    }
}

The code itself is pretty self-explanatory and probably more verbose than it really needs to be, but it gets the job done and looks good.

If you want the code, I have created a repository on Github.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.