Click here to Skip to main content
15,887,596 members
Articles / Programming Languages / C# 5.0
Tip/Trick

A Simple C# Console Menu

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
17 Jan 2015CPOL6 min read 61.7K   1.2K   6   3
A really simple implementation of a C# menu for console applications

Introduction

This tip explains how to create a simple implementation of a C# menu for console applications. Then, we'll put it inside a DLL in order to make it reusable.

Set Up the Project

The first thing we have to do is to create a new C# solution with two projects inside:

  1. A Class Library project
  2. A Console Application project (to test the library created above)

Secondly, we have to reference the DLL in the console application. In order to do this, right-click on the 'Reference' tab of the console application and select "Add Reference". Then choose "solution" and then the Console Menu project as in the image below:

Image 1

Writing the Code for the Menu

This part is the most complex one because we are going to deal with the core of our library.

We are going to:

  • Create a CMenu class (which will be generic in order to be reusable).
  • Create a MenuEntry class (which will be generic in order to be reusable).
  • Link them together (CMenu is a list of MenuEntries).
  • Make the menu working.
  • Enable the user to choose among multiple options.

Create the MenuEntry and the CMenu Classes

In order to have a single menu entry, we are going to use this class to represent one.

C#
public class MenuEntry<T>
    {
        public Action<T> RelatedAction { get; set; }
        public string Description;

        public void ExecuteEntry(T inputValue)
        {
            RelatedAction.Invoke(inputValue);
        }
    }

This class has an action delegate, which specifies the method to be executed when the user selects this entry and a description, which will be shown in the menu. The method ExecuteEntry is for executing the method and passing a parameter inside. So the generic type is the input type of the Action delegate and has to be the same for all the MenuEntries!

Then, we are going to create a class which inherits List<MenuEntries<T>>

C#
public class CMenu:List<MenuEntry>
    {
        public MenuType Type { get; private set; }

        public CMenu(MenuType type, params MenuEntry[] entries)
        {
            this.AddRange(entries);
            Type = type;
            if ((Type == MenuType.UpperLetters || Type == MenuType.LowerLetters) && this.Count > 26)
                throw new ArgumentOutOfRangeException("If 'Letters' is chosen as the MenuType, the entries should be equal or less than 26!");
        }

        /// <summary>
        /// Returns a string with the menu printed on it
        /// </summary>
        /// <param name="SeparatorChar">This is the char which separates the Letter/Number from the description</param>
        /// <param name="FinalChar">This is the char which is always put at the end of each Entry description (I.E. the last char to be appended to the description of each entry)</param>
        /// <returns></returns>
        public string PrintMenu(char SeparatorChar = '.', char FinalChar = '\n')
        {
            string Menu = "";
            char Letter = Type == MenuType.LowerLetters ? 'a' : 'A';
            ushort Numbers = 1;
            foreach (MenuEntry ME in this)
            {
                switch (Type)
                {
                    case MenuType.LowerLetters:
                    case MenuType.UpperLetters:
                        Menu += string.Format("{0}{1} {2}{3}", Letter++, SeparatorChar, ME.Description, FinalChar);
                        break;
                    case MenuType.Numbers:
                        Menu += string.Format("{0}{1} {2}{3}", Numbers++, SeparatorChar, ME.Description, FinalChar);
                        break;
                }
            }
            return Menu;
        }

        /// <summary>
        /// Returns a string with the menu printed on it
        /// </summary>
        /// <param name="Separatorstring">This is the string which separates the Letter/Number from the description</param>
        /// <param name="Finalstring">This is the string which is always put at the end of each Entry description (I.E. the last string to be appended to the description of each entry)</param>
        /// <returns></returns>
        public string PrintMenu(string SeparatorString = ".", string FinalString = "\n")
        {
            string Menu = "";
            char Letter = Type == MenuType.LowerLetters ? 'a' : 'A';
            ushort Numbers = 1;
            foreach (MenuEntry ME in this)
            {
                switch (Type)
                {
                    case MenuType.LowerLetters:
                    case MenuType.UpperLetters:
                        Menu += string.Format("{0}{1} {2}{3}", Letter++, SeparatorString, ME.Description, FinalString);
                        break;
                    case MenuType.Numbers:
                        Menu += string.Format("{0}{1} {2}{3}", Numbers++, SeparatorString, ME.Description, FinalString);
                        break;
                }
            }
            return Menu;
        }
        /// <summary>
        /// This will execute an entry characterised by the letter given in input (the letter of the menu)
        /// It returns false if the index is out of range
        /// </summary>
        /// <param name="Letter">The correspondant letter of the menu</param>
        public bool ExecuteEntry(char Letter)
        {
            if (Type == MenuType.LowerLetters)
            {
                int Index = (int)Letter - 97;
                if (Index >= this.Count)
                {
                    return false;
                }
                this[Index].ExecuteEntry();
            }
            else if (Type == MenuType.UpperLetters)
            {
                int Index = (int)Letter - 65;
                if (Index >= this.Count)
                {
                    return false;
                }
                this[Index].ExecuteEntry();
            }
            return true;
        }
        /// <summary>
        /// This will execute the entry characterised by the menu index given
        /// It returns false if the index is out of range
        /// </summary>
        /// <param name="MenuIndex">Index of the entry in the menu</param>
        public bool ExecuteEntry(int MenuIndex)
        {
            if (MenuIndex > this.Count)
            {
                return false;
            }
            this[MenuIndex-1].ExecuteEntry();
            return true;
        }
        /// <summary>
        /// This will execute an Entry based on its index in the Menuthis list. To be used only for debugging purposes.
        /// </summary>
        /// <remarks>This method is not intended to be normally used! Use <see cref="ExecuteEntry">ExecuteEntry</see> instead </remarks>
        /// <param name="ListIndex">The index of the Entry in the list</param>
        public void ExecuteEntryWithListIndex(int ListIndex)
        {
            this[ListIndex].ExecuteEntry();
        }
    }

It does some really simple things:

  • It stores the entries.
  • It prints them out (by using a lot of overloads in order to make this class suitable for a lot of uses).
  • It executes an entry based on: a letter or an index or the absolute collection index (instead of the one given by the menu).

This class is reusable as the input type of the methods is variable. This aim is achieved by using Generics. Generics are a really powerful feature. It is possible to use whenever we are looking for something reusable.

Explaining the Code

The constructor of the class takes in input either only the type of the menu (letter-characterised elements or number-characterised ones) or the type and a params array. This keyword means that the length of the array is not priorly known. So we can use the ctor in this way:

C#
...new CMenu(MenuType.Numbers, new MenuEntry(...), new MenuEntry(...));

and C# will automatically parse those into an array.

Then, we have two overloads of the same method used to pass the separator char/string and the final char/string for printing the menu in a string. It can also be improved by using the StringBuilder class.

Finally, we have two overloads of a method which executes the method related to the letter/number chosen by the user. We also have another method whose aim is to execute a MenuEntry based on its Index and not its MenuIndex (MenuIndex starts from 1, while normal indexes start from 0). This method has been created for debugging purposes.

Update

As the class inherits List<MenuEntries<T>>, it is now a list and we do not need any implementation of a list inside it (please, see the v1000.zip at the top of the page to see the other version). So the CMenu class no more has a list of MenuEntries, but it is a list of MenuEntries.

Return a variable from the methods

In the previous code, the code could just take in input a value, but it cannot return any value/object from the CMenu entries. So I've developed this code which works with the delegate Func<T,T1>

C#
public class CMenu<T, T1>:List<MenuEntry<T,T1>>
    {
        public MenuType Type { get; private set; }

        public CMenu(MenuType type, params MenuEntry<T, T1>[] entries)
        {
            this.AddRange(entries);
            Type = type;
            if ((Type == MenuType.UpperLetters || Type == MenuType.LowerLetters) && this.Count > 26)
                throw new ArgumentOutOfRangeException("If 'Letters' is chosen as the MenuType, the entries should be equal or less than 26!");
        }
        public CMenu(MenuType type)
        {
            Type = type;
            if ((Type == MenuType.UpperLetters || Type == MenuType.LowerLetters) && this.Count > 26)
                throw new ArgumentOutOfRangeException("If 'Letters' is chosen as the MenuType, the entries should be equal or less than 26!");
        }
        /// <summary>
        /// Returns a string with the menu printed on it
        /// </summary>
        /// <param name="SeparatorChar">This is the char which separates the Letter/Number from the description</param>
        /// <param name="FinalChar">This is the char which is always put at the end of each Entry description (I.E. the last char to be appended to the description of each entry)</param>
        /// <returns></returns>
        public string PrintMenu(char SeparatorChar = '.', char FinalChar = '\n')
        {
            string Menu = "";
            char Letter = Type == MenuType.LowerLetters ? 'a' : 'A';
            ushort Numbers = 1;
            foreach (MenuEntry<T, T1> ME in this)
            {
                switch (Type)
                {
                    case MenuType.LowerLetters:
                    case MenuType.UpperLetters:
                        Menu += string.Format("{0}{1} {2}{3}", Letter++, SeparatorChar, ME.Description, FinalChar);
                        break;
                    case MenuType.Numbers:
                        Menu += string.Format("{0}{1} {2}{3}", Numbers++, SeparatorChar, ME.Description, FinalChar);
                        break;
                }
            }
            return Menu;
        }

        /// <summary>
        /// Returns a string with the menu printed on it
        /// </summary>
        /// <param name="Separatorstring">This is the string which separates the Letter/Number from the description</param>
        /// <param name="Finalstring">This is the string which is always put at the end of each Entry description (I.E. the last string to be appended to the description of each entry)</param>
        /// <returns></returns>
        public string PrintMenu(string SeparatorString = ".", string FinalString = "\n")
        {
            string Menu = "";
            char Letter = Type == MenuType.LowerLetters ? 'a' : 'A';
            ushort Numbers = 1;
            foreach (MenuEntry<T, T1> ME in this)
            {
                switch (Type)
                {
                    case MenuType.LowerLetters:
                    case MenuType.UpperLetters:
                        Menu += string.Format("{0}{1} {2}{3}", Letter++, SeparatorString, ME.Description, FinalString);
                        break;
                    case MenuType.Numbers:
                        Menu += string.Format("{0}{1} {2}{3}", Numbers++, SeparatorString, ME.Description, FinalString);
                        break;
                }
            }
            return Menu;
        }
        /// <summary>
        /// This will execute an entry characterised by the letter given in input (the letter of the menu)
        /// It returns false if the index is out of range
        /// </summary>
        /// <param name="Letter">The correspondant letter of the menu</param>
        /// <param name="InputValue">The input value</param>
        public bool ExecuteEntry(char Letter, T InputValue, out T1 output)
        {
            output = default(T1);
            if (Type == MenuType.LowerLetters)
            {
                int Index = (int)Letter - 97;
                if (Index >= this.Count)
                {
                    return false;
                }
                output = this[Index].ExecuteEntry(InputValue);
            }
            else if (Type == MenuType.UpperLetters)
            {
                int Index = (int)Letter - 65;
                if (Index >= this.Count)
                {
                    return false;
                }
                output = this[Index].ExecuteEntry(InputValue);
            }
            return true;
        }
        /// <summary>
        /// This will execute the entry characterised by the menu index given
        /// It returns false if the index is out of range
        /// </summary>
        /// <param name="MenuIndex">Index of the entry in the menu</param>
        /// <param name="InputValue">The input value</param>
        public bool ExecuteEntry(int MenuIndex, T InputValue, out T1 output)
        {
            if (MenuIndex > this.Count)
            {
                output = default(T1);
                return false;
            }
            output = this[MenuIndex-1].ExecuteEntry(InputValue);
            return true;
        }
        /// <summary>
        /// This will execute an Entry based on its index in the Menuthis list. To be used only for debugging purposes.
        /// </summary>
        /// <remarks>This method is not intended to be normally used! Use <see cref="ExecuteEntry">ExecuteEntry</see> instead </remarks>
        /// <param name="ListIndex">The index of the Entry in the list</param>
        /// <param name="InputValue">The input value</param>
        public void ExecuteEntryWithListIndex(int ListIndex, T InputValue, out T1 output)
        {
            output = this[ListIndex].ExecuteEntry(InputValue);
        }
    }

The code above is really similar to the first one, but here the code can return a value (please see the example below under ReturnGeneric method).

Test Out the Menu

Now we are going to test the menu inside the console application. Let's write some simple code:

C#
class Program
    {
        public struct Operators
        {
            public float Op1;
            public float Op2;
            public Operators(float op1, float op2)
            {
                Op1 = op1;
                Op2 = op2;
            }
        }
        static void Main(string[] args)
        {
            CMenu MainMenu = new CMenu(MenuType.Numbers);
            MainMenu.Add(new MenuEntry("Simple generic example", new Action(() => { SimpleGeneric(); })));
            MainMenu.Add(new MenuEntry("Return generic example", new Action(() => { ReturnGeneric(); })));
            Console.WriteLine(MainMenu.PrintMenu('.', '\t'));
            while (!MainMenu.ExecuteEntry(int.Parse(Console.ReadLine())))
                Console.WriteLine("Selection not allowed!");
            Console.ReadKey();
        }

        public static void ReturnGeneric()
        {
            Operators ops = new Operators();
            Console.Write("Write the first operator -> ");
            ops.Op1 = int.Parse(Console.ReadLine());
            Console.Write("Write the second operator -> ");
            ops.Op2 = int.Parse(Console.ReadLine());
            CMenu<Operators, float> menu = new CMenu<Operators, float>(MenuType.UpperLetters);
            menu.Add(new MenuEntry<Operators, float>("Addition", new Func<Operators, float>((Operators opers) => { return AddReturn(opers); })));
            menu.Add(new MenuEntry<Operators, float>("Subtraction", new Func<Operators, float>((Operators opers) => { return SubtractReturn(opers); })));
            menu.Add(new MenuEntry<Operators, float>("Multiplication", new Func<Operators, float>((Operators opers) => { return MultiplyReturn(opers); })));
            menu.Add(new MenuEntry<Operators, float>("Division", new Func<Operators, float>((Operators opers) => { return DivideReturn(opers); })));

            Console.WriteLine(menu.PrintMenu('.'));
            Console.WriteLine("Choose among the up-listed options -> ");
            float result;
            while (!menu.ExecuteEntry(char.Parse(Console.ReadLine()), ops, out result))
            {
                Console.WriteLine("Selection not allowed!");
            }
            Console.WriteLine(result);
        }

        public static void SimpleGeneric()
        {
            Operators ops = new Operators();
            Console.Write("Write the first operator -> ");
            ops.Op1 = int.Parse(Console.ReadLine());
            Console.Write("Write the second operator -> ");
            ops.Op2 = int.Parse(Console.ReadLine());
            CMenu<Operators> menu = new CMenu<Operators>(MenuType.LowerLetters);
            menu.Add(new MenuEntry<Operators>("Addition", new Action<Operators>((Operators opers) => { Add(opers); })));
            menu.Add(new MenuEntry<Operators>("Subtraction", new Action<Operators>((Operators opers) => { Subtract(opers); })));
            menu.Add(new MenuEntry<Operators>("Multiplication", new Action<Operators>((Operators opers) => { Multiply(opers); })));
            menu.Add(new MenuEntry<Operators>("Division", new Action<Operators>((Operators opers) => { Divide(opers); })));

            Console.WriteLine(menu.PrintMenu('.'));
            Console.WriteLine("Choose among the up-listed options -> ");
            while (!menu.ExecuteEntry(char.Parse(Console.ReadLine()),ops))
            {
                Console.WriteLine("Selection not allowed!");
            }
        }

        #region float methods
        public static float AddReturn(Operators Ops)
        {
            return Ops.Op1 + Ops.Op2;
        }
        public static float SubtractReturn(Operators Ops)
        {
            return Ops.Op1 - Ops.Op2;
        }
        public static float MultiplyReturn(Operators Ops)
        {
            return Ops.Op1 * Ops.Op2;
        }
        public static float DivideReturn(Operators Ops)
        {
            return Ops.Op1 / Ops.Op2;
        }
        #endregion
        #region void methods
        public static void Add(Operators Ops)
        {
            Console.WriteLine( Ops.Op1 + Ops.Op2);
        }
        public static void Subtract(Operators Ops)
        {
            Console.WriteLine( Ops.Op1 - Ops.Op2);
        }
        public static void Multiply(Operators Ops)
        {
            Console.WriteLine( Ops.Op1 * Ops.Op2);
        }
        public static void Divide(Operators Ops)
        {
            Console.WriteLine( Ops.Op1 / Ops.Op2);
        }
        #endregion
    }

This really simple example does simple math operations and shows the behaviour of our Menu class in the three types of menus: UpperLetters, LowerLetters and Numbers. It also shows us the use of FinalChar as the first menu has been created by using a tabulation char to separate different entries. Keep in mind that, in the code, I've included a class with the non-generic implementation of the class for methods that does not require any input parameter. I've used it in the first menu.

It may seem that it is difficult to create a new menu because there is a Lambda expression inside a constructor inside another constructor, but this is not true as they are all similar and could be copied and pasted!

Image 2

Points of Interest

This menu is really simple and compact but powerful reusable component in C# console applications. It can be used for multiple purposes such as quickly testing methods, debugging and also to write some real console application even if I'm aware that they are a little outdated nowadays.

You can download the code and use the DLL in whichever project! I've also created a non-generic class to deal with Actions that does not need any input.

Some notes:

  • If you need more than one type of input, create a base class for all of your types and use it cleverly. For instance: if a method needs an hexagon, you can instantiate an hexagon, convert it to Shape and then, inside the method, reconvert it to hexagon.
  • Keep in mind that this menu does not offer the ability for a method to return a value. If you need to, you can use a double generic class and use T1 as the return type. Use Func<in T, out T1> instead of Action<T>

History

  • 25/01/15: Improved Code. The CMenu class is a List<MenuEntries<T>> which implies that there's no need to have a List inside it. Then added a new class which allows the programmer to return a value out of a function.
  • 20/01/15: Added result.png and corrected another error. If somebody downloaded the buggy version, please, download the new version again. Actually in the DLL, it's missing a "-1" (as seen in the point below) in ExecuteEntry of Generic.cs and Non-Generic.cs. Sorry for the inconvenience. In plus, I've added a new exception if the char overload of ExecuteEntry is executed if the MenuType is Numbers.
  • 19/01/15: Corrected some errors inside the sample code and in the DLL: added the line in bold.
    C#
    while (!menu.ExecuteEntry(read, ops))
                {
                    Console.WriteLine("Selection not allowed!");
                    read = char.Parse(Console.ReadLine());
                }

    And changed the line in italics:

    C#
    public bool ExecuteEntry(int MenuIndex, T InputValue)
            {
                if (MenuIndex >= Entries.Count)
                {
                    return false;
                }
                Entries[MenuIndex-1].ExecuteEntry(InputValue);
                return true;
            }

    and removed some lines I could not strike because of layout mistakes.

  • 17/01/15: First version of the tip

License

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


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

Comments and Discussions

 
BugBug in previous versions Pin
LLLLGGGG20-Jan-15 3:59
LLLLGGGG20-Jan-15 3:59 
QuestionResult.png is not visible Pin
Alexms20-Jan-15 2:18
Alexms20-Jan-15 2:18 
AnswerRe: Result.png is not visible Pin
LLLLGGGG20-Jan-15 3:59
LLLLGGGG20-Jan-15 3:59 

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.