Skip to content

Instantly share code, notes, and snippets.

@bigabdoul
Last active August 27, 2022 14:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bigabdoul/c60e814b8f497be43e612b62ca1a15db to your computer and use it in GitHub Desktop.
Save bigabdoul/c60e814b8f497be43e612b62ca1a15db to your computer and use it in GitHub Desktop.
An introduction to using PowerConsole

PowerConsole

Makes strongly-typed user input collection and validation through the Console easier, and adds more useful utility methods to it.

What is PowerConsole?

PowerConsole is a .NET Standard project that makes strongly-typed user input collection and validation through a console easier. Through the SmartConsole class, it enhances the traditional system's Console by encapsulating complex and redundant processing logic, and defining a wealth of utility functions. Quickly create a new .NET Core Console application and install the package from NuGet:

Install-Package PowerConsole

Why PowerConsole?

Almost every beginner tutorial for any server-side programming language uses the console to write out on a standard input/output screen the famous "Hello World!" Why? Because console applications are a superb choice for learners: they're easy to create and fast to execute. However, when a console app requires user interaction, such as prompting for their name and password or collecting and validating their inputs against a predefined set of rules, then developing efficiently a console app becomes a real pain in the back.

"Superb choice for learners" doesn't mean not meant for experienced developers. Developing advanced console applications is reserved for those who know what they're doing. So it makes perfect sense to have a tool that allows them to be more productive.

Developing efficient console applications is a daunting task. It's because of the nature of the Console class in .NET's System namespace which is a static class by design and does not retain state, thus making it difficult to collect a set of related data. That's where SmartConsole steps in by encapsulating methods and data required to build an interactive console application that seamlessly enforces complex business logic.

Getting started

using System;
using System.Globalization;
using PowerConsole;

namespace PowerConsoleTest
{
    class Program
    {
        internal static readonly SmartConsole MyConsole = SmartConsole.Default;

        static void Main()
        {
            MyConsole.SetForegroundColor(ConsoleColor.Green)
                .WriteLines("Welcome to the Power Console Demo!\n",
                    $"Project:\t {nameof(PowerConsole)}",
                    "Version:\t 1.0.0",
                    "Description:\t Makes strongly-typed user input collection and validation through ",
                    "\t\t a console easier. This is a Console on steroids.",
                    "Author:\t\t Abdourahamane Kaba",
                    "License:\t MIT",
                    "Copyright:\t (c) 2020 Karfamsoft\n")
                .RestoreForegroundColor();

            if (!MyConsole.PromptNo("Would you like to define a specific culture for this session? (yes/No) "))
            {
                MyConsole.Write("\n\tEnter a culture name to use, like ")
                    // writes a list of comma-separated culture names in blue
                    .WriteList(color: ConsoleColor.Blue, separator: ", ", 
                        /* args: */ "en", "en-us", "fr", "fr-ca", "de")
                    .WriteLine(", etc.")
                    .WriteLine("\tLeave empty if you wish to use your computer's current culture.\n")
                    .Write("\tCulture name: ")

                    // try to set the culture and eventually catch an instance of
                    // CultureNotFoundException; other error types will be rethrown
                    .TrySetResponse<CultureNotFoundException>(
                        culture => MyConsole.SetCulture(culture),
                        error => MyConsole.WriteError(error));
            }

            PlayFizzBuzz();
        }

        private static void PlayFizzBuzz()
        {
            MyConsole.WriteInfo("\nWelcome to FizzBuzz!\nTo quit the loop enter 0.\n\n");

            // nullable types are supported as well
            double? number;
            const string validationMessage = "Only numbers, please! ";

            while ((number = MyConsole.GetResponse<double?>("Enter a number: ", validationMessage)) != 0)
            {
                // Writes out:
                //  "FizzBuzz" if the input is divisible both by 3 and 5
                //  "Buzz" if divisible by 3 only
                //  "Fizz" if divisible by 5 only
                if (number == null) continue;
                if ((number % 3 == 0) && (number % 5 == 0))
                {
                    MyConsole.WriteLine("FizzBuzz");
                }
                else if (number % 3 == 0)
                {
                    MyConsole.WriteLine("Buzz");
                }
                else if (number % 5 == 0)
                {
                    MyConsole.WriteLine("Fizz");
                }
                else
                {
                    MyConsole.WriteLine(number);
                }
            }

            Goodbye("\nThank you for playing FizzBuzz. Goodbye!\n\n");
        }

        
        private static void Goodbye(string message = null)
        {
            // say goodbye and clean up
            MyConsole.WriteSuccess(message ?? "Goodbye!\n\n")
                .Repeat(15, "{0}{1}", "o-", "=-")
                .WriteLine("o\n")
                .Clear();
        }
    }
}

Use cases

Let's quickly walk through a series of use cases to illustrate what problems PowerConsole solves and what extra values it adds.

Use case #1: Yes or No?

A typical prompt in console apps is a question that requires a logical answer (yes or no) which resolves as a boolean (true or false) value.

For instance, Would you like to define a specific culture for this session? (yes/No)

The above prompt expects a yes or a No response. If the response is empty (when the user presses the Enter key without typing anything), the answer is considered a "No." Why? Because of the capitalization of the first letter of the word "No." The user can enter either:

  • a case-insensitive n (or N, it doesn't matter),
  • a case-insensitive no (or No or NO, it makes no difference),
  • or press the Enter key.

Any response different from all of the above is considered a Yes.

How would one collect such input? In C#, it would resemble a variation of the following:

Console.WriteLine("Would you like to define a specific culture for this session? (yes/No) ");

string answer = Console.ReadLine();

if (string.IsNullOrWhiteSpace(answer) ||
    string.Equals(answer, "n", StringComparison.OrdinalIgoreCase) ||
    string.Equals(answer, "no", StringComparison.OrdinalIgoreCase))
{
    // do stuff when the answer is no...
}
else
{
    // do other stuff when the answer is affirmative...
}

With SmartConsole we can write:

if (SmartConsole.Default.PromptNo("Would you like to define a specific culture for this session? (yes/No) "))
{
    // do stuff when the answer is negative...
}
else
{
    // do other stuff when the answer is affirmative...
}

As we can see, the amount of code required to do the same thing is less than what we were used to write: asking a question and getting the response is a one-liner.

Use case #2: Type conversion with graceful error handling

Knowing that Console.ReadLine() (not surprisingly) returns a string (a series of Unicode characters), it becomes clear that we, as developers, must convert the line read into the desired type that a given piece of code expects. For instance, a mortgage calculator requires a few variables such as the principal P (or amount borrowed), the number of monthly payments n, and the annual interest rate R. The following formula determines the monthly down payment M:

var r = R / 100 / 12;
var M = P * r * Math.Pow(1 + r, n) / (Math.Pow(1 + r, n) - 1);

Although this formula looks sophisticated, it's not what we want to focus on here. What's more relevant for us to know are the business requirements:

  • The principal P must be a decimal number at least 1,000.00 (one thousand) and not exceed 1,000,000.00 (one million).
  • The number of monthly payments n must be a whole number greater than zero and not exceed 360 (30 years).
  • The annual interest rate R, expressed as a percentage, must be a single-precision floating point number greater than zero and not exceed 30.

With these constraints clearly defined, let's look at how we can use SmartConsole to solve this problem. Open your preferred code editor, create a new .NET Core Console project named MortgageCalculatorApp, and a file named MortgageCalculator.cs with the following content:

using System;
using PowerConsole;

namespace MortgageCalculatorApp
{
    public static class MortgageCalculator
    {
        // for the sake of reusability, let's gather all inputs from within this class
        internal static readonly SmartConsole MyConsole = SmartConsole.Default;

        public static void GetInputAndCalculate()
        {
            MyConsole.WriteInfo("Welcome to Mortgage Calculator!\n\n");

            // SmartConsole ensures that only a floating-point number is entered
            // where the decimal part (depending on the current culture) is either
            // a period '.', a comma ',' or the appropriate culture-specific separator
            var principal = MyConsole.GetResponse<decimal>("Principal: ",
                validationMessage: "Enter a number between 1000 and 1,000,000: ",
                validator: input => input >= 1000M && input <= 1000000M);

            // numeric integral data types don't have a decimal part, hence SmartConsole
            // will ignore it if the user types such characters;
            var numPayments = MyConsole.GetResponse<short>("Number of payments: ",
                "Please enter a whole number between 1 and 360: ",
                input => input >= 1 && input <= 360);

            // plus, a single negative symbol (usually a minus sign) is allowed at the 
            // beginning of any numeric data type; this is why custom validation is 
            // required to enforce the constraints specification
            var rate = MyConsole.GetResponse<float>("Annual interest rate: ",
                "The interest rate must be > 0 and <= 30.",
                input => input > 0F && input <= 30F);

            var mortgage = Calculate(principal, numPayments, rate);

            MyConsole
                .Write($"The monthly down payment is: ")
                .WriteInfo($"{mortgage:C}\n");
        }

        public static decimal Calculate(decimal principal, short numberOfPayments, float annualInterestRate)
        {
            byte PERCENT = 100;
            byte MONTHS_IN_YEAR = 12;
            float r = annualInterestRate / PERCENT / MONTHS_IN_YEAR;
            double factor = Math.Pow(1 + r, numberOfPayments);
             
            return principal * new decimal(r * factor / (factor - 1));
        }
    }
}

Change (or create) the Program.cs file in such a way to resemble the following:

namespace MortgageCalculatorApp
{
    class Program
    {
        static void Main()
        {
            MortgageCalculator.GetInputAndCalculate();
        }
    }
}

That's it! PowerConsole handles everything internally, from user input collection to casting (type conversion) and validation. Every time the user enters an invalid entry, an appropriate error message is displayed and they will have another opportunity to enter an acceptable value.

Use case #3: Form input collection

We refer here to form inputs, a collection of key/value pairs collected through a series of interactive prompts. Let's assume that we need to collect small bits of information about a user and then store that somewhere. Let's say that we have a user class that looks like this:

class UserInfo
{
    public string FullName { get; set; }
    public int Age { get; set; }
    public string BirthCountry { get; set; }
    public string PreferredColor { get; set; }

    public override string ToString()
    {
        return $"Full name: {FullName}\nAge: {Age}\nCountry of birth: {BirthCountry}\nPreferred color: {PreferredColor}";
    }
}

Very simple! Now let's create a small console app that allows us to achieve this. Create a new console (.NET Core or .NET Framework, it doesn't matter) app called UserInfoCollectionApp and modify the Program class like this:

using PowerConsole;

namespace UserInfoCollectionApp
{
    class Program
    {
        internal static readonly SmartConsole MyConsole = SmartConsole.Default;

        static void Main()
        {
            CollectUserInfo();
        }

        static void CollectUserInfo()
        {
            MyConsole.WriteInfo("Welcome to the user info collection demo!\n");

            // by simply providing a validation message, we force 
            // the input not to be empty or white space only (and to
            // be of the appropriate type if different from string)
            var nameValidationMessage = "Your full name is required: ";

            bool validateAge(int input) => input >= 5 && input <= 100;
            var ageErrorMessage = "Age (in years) must be a whole number from 5 to 100: ";

            // notice the 'promptId' parameter: they'll allow us 
            // strongly-typed object instantiation and property mapping
            while
            (
                MyConsole.Store() // forces all subsequent prompts to be stored
                    .Prompt("\nEnter your full name: ", historyLabel: "Full Name:", validationMessage: nameValidationMessage, promptId: nameof(UserInfo.FullName))
                    .Prompt<int>("How old are you? ", "Plain Age:", validationMessage: ageErrorMessage, validator: validateAge, promptId: nameof(UserInfo.Age))
                    .Prompt("In which country were you born? ", "Birth Country:", promptId: nameof(UserInfo.BirthCountry))
                    .Prompt("What's your preferred color? ", "Preferred Color:", promptId: nameof(UserInfo.PreferredColor))
                    .WriteLine()
                    .WriteLine("Here's what you've entered: ")
                    .WriteLine()
                    .Recall(prefix: "> ")
                    .WriteLine()
                    .Store(false) // stops storing prompts

                    // give the user an opportunity to review and correct their inputs
                    .PromptYes("Is that correct? (Y/n) ") == false
            )
            {
                // nothing else required within this while loop
            }

            MyConsole.WriteInfo("Thank you for providing your details.\n");

            if (!MyConsole.PromptNo("Do you wish to save them now? (y/N) "))
            {
                // Create a new instance of the UserInfo class and initialize
                // its properties with the responses of the previous prompts.
                // CreateObject<T> is an extension method defined in the
                // static SmartConsoleExtensions class.
                var user = MyConsole.CreateObject<UserInfo>();
                
                // do something with it
                MyConsole.SetForegroundColor(System.ConsoleColor.DarkGreen)
                    .WriteLine(user)
                    .RestoreForegroundColor();
            }

            // Removes all prompts from the prompt history;
            // Does NOT clear the console buffer and corresponding 
            // console window of display information.
            MyConsole.Clear();
        }
    }

    class UserInfo
    {
        public string FullName { get; set; }
        public int Age { get; set; }
        public string BirthCountry { get; set; }
        public string PreferredColor { get; set; }

        public override string ToString()
        {
            return $"Full name: {FullName}\nAge: {Age}\nCountry of birth: {BirthCountry}\nPreferred color: {PreferredColor}";
        }
    }
}

The empty while loop does a series of four prompts for the four properties of the UserInfo class, smartly handling the age requirement. The last prompt Is that correct? (Y/n) gives the user an opportunity to review their inputs and make corrections where required.

Extra features

  • Method chaining: In SmartConsole, almost all methods return a reference to the current SmartConsole instance. This makes it easier to chain method calls.

  • Colored outputs: Write out text using basic ConsoleColor values which get automatically restored as soon as control from the method returns.

  • Prompt history: Prompts can be stored and replayed later should your app have a need for that.

  • Extensibilty: Since SmartConsole is an instance class other classes can inherit it to further extend it. We can also add extension methods to it, as shown in the statement MyConsole.CreateObject<UserInfo>();.

  • Timers: New methods similar to JavaScript's window.setTimeout, window.setInterval, window.clearTimeout, and window.clearInterval have been added.

      using PowerConsole;
    
      namespace PowerConsoleTest
      {
          class Program
          {
              static readonly SmartConsole MyConsole = SmartConsole.Default;
    
              static void Main()
              {
                  RunTimers();
              }
    
              public static void RunTimers()
              {
                  // CAUTION: SmartConsole is not thread safe!
                  // Spawn multiple timers carefully when accessing
                  // simultaneously members of the SmartConsole class.
    
                  MyConsole.WriteInfo("\nWelcome to the Timers demo!\n")
    
                  // SetTimeout is called only once after the provided delay and
                  // is automatically removed by the TimerManager class
                  .SetTimeout(e =>
                  {
                      // this action is called back after 5.5 seconds; the name
                      // of the time out is useful should we want to clear it
                      // before this action gets executed
                      e.Console
                          .Write("\n")
                          .WriteError("First timer: Time out occured after 5.5 seconds! " +
                          "Timer has been automatically disposed.\n");
    
                      // the next statement will make the current instance of 
                      // SmartConsole throw an exception on the next prompt attempt
                      // e.Console.CancelRequested = true;
    
                      // use 5500 or any other value not multiple of 1000 to 
                      // reduce write collision risk with the next timer
                  }, millisecondsDelay: 5500, name: "SampleTimeout")
    
                  .SetInterval(e =>
                  {
                      if (e.Ticks == 1)
                      {
                          e.Console.WriteLine();
                      }
    
                      e.Console
                      .Write($"\rSecond timer tick: ", System.ConsoleColor.White)
                      .WriteInfo(e.TicksToSecondsElapsed());
    
                      if (e.Ticks > 4)
                      {
                          // we could remove the previous timer:
                          // e.Console.ClearTimeout("SampleTimeout");
                      }
    
                  }, millisecondsInterval: 1000)
    
                  // we can add as many timers as we want (or the computer's resources permit)
                  .SetInterval(e =>
                  {
                      if (e.Ticks == 1 || e.Ticks == 3) // 1.5 or 4.5 seconds to avoid write collision
                      {
                          e.Console.WriteSuccess($"\nThird timer is {(e.Ticks == 1 ? "" : "still ")}active...\n");
                      }
                      else if (e.Ticks == 5)
                      {
                          e.Console.WriteWarning("\nThird timer is disposing...\n");
    
                          // doesn't dispose the timer
                          // e.Timer.Stop();
    
                          // clean up if we no longer need it
                          e.DisposeTimer();
                      }
                      else
                      {
                          System.Diagnostics.Trace.WriteLine($"Third timer tick: {e.Ticks}");
                      }
                  }, 1500)
                  .Prompt("\nPress Enter to stop the timers: ")
                  
                  // makes sure that any remaining timer is disposed off
                  .ClearTimers()
    
                  .WriteSuccess("Timers cleared!\n");
              }
          }
      }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment