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:
- A Class Library project
- 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:
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.
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>>
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!");
}
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;
}
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;
}
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;
}
public bool ExecuteEntry(int MenuIndex)
{
if (MenuIndex > this.Count)
{
return false;
}
this[MenuIndex-1].ExecuteEntry();
return true;
}
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:
...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>
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!");
}
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;
}
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;
}
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;
}
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;
}
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:
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!
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.
while (!menu.ExecuteEntry(read, ops))
{
Console.WriteLine("Selection not allowed!");
read = char.Parse(Console.ReadLine());
}
And changed the line in italics:
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