2
\$\begingroup\$

I want to make small console app where you can choose an option from a list using the arrows on the keyboard. Here is my code:

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

namespace CMD_menu_select
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var cities = new List<string>()
                {
                    "New York",
                    "London",
                    "Mumbai",
                    "Chicago"
                };

            int index = 0;
            ConsoleKeyInfo keyinfo;

            while (true)
            {
                WriteCities(cities, index);
                keyinfo = Console.ReadKey();

                if (keyinfo.Key == ConsoleKey.DownArrow)
                {
                    if (index + 1 < cities.Count)
                    {
                        index++;
                    }
                }

                else if (keyinfo.Key == ConsoleKey.UpArrow)
                {
                    if (index - 1 >= 0)
                    {
                        index--;
                    }
                }

                else if (keyinfo.Key == ConsoleKey.Enter)
                {
                    // there will be another logic in the future here. For now it is irrelevant.
                    Console.WriteLine($"{cities[index]} was chosen as an option");
                    break;
                }

            }

            void WriteCities(List<string> _cities, int _index)
            {
                Console.Clear();
                foreach (var city in _cities)
                {
                    string selectOptionSymbol = "  ";
                    if (city == _cities[_index])
                    {
                        selectOptionSymbol = "> ";
                        Console.BackgroundColor = ConsoleColor.White;
                        Console.ForegroundColor = ConsoleColor.Black;
                    }

                    Console.WriteLine($"{selectOptionSymbol}{city}");
                    Console.BackgroundColor = ConsoleColor.Black;
                    Console.ForegroundColor = ConsoleColor.White;
                }
            }
        }
    }
}

It works fine. However I am not sure if this is the way to go about things like this. Do I need different approach, can my code be shorter(not so repetitive), naming convention etc... Keep in mind I am just beginner. Any improvements will be greatly appreciated.

\$\endgroup\$
3
  • \$\begingroup\$ Which C# version are you using? \$\endgroup\$ Commented Dec 17, 2022 at 7:00
  • \$\begingroup\$ I am using .NET Framework 4.7.2 and for language version it says (Automatically selected based on framework version). Which means I use C# 11. I hope this was your question and I did not misunderstood. \$\endgroup\$ Commented Dec 17, 2022 at 13:30
  • \$\begingroup\$ I've just left a post where I detailed how can you refactor your code into a more maintainable one. I've tried to avoid to use advanced or fancy C# features. \$\endgroup\$ Commented Dec 17, 2022 at 13:54

1 Answer 1

6
\$\begingroup\$

First let me share with you the revised version of your code.

Please note that this version contains a bug.
The fixed version can be found under the update #2 section.

Then I will guide you through the changes.

static readonly string[] Cities = new []
{
    "New York",
    "London",
    "Mumbai",
    "Chicago"
};

static void Main()
{
    int selectedLineIndex = 0;
    ConsoleKey pressedKey;
    do
    {
        UpdateMenu(selectedLineIndex);
        pressedKey = Console.ReadKey().Key;

        if (pressedKey == ConsoleKey.DownArrow && selectedLineIndex + 1 < Cities.Length)
            selectedLineIndex++;

        else if (pressedKey == ConsoleKey.UpArrow && selectedLineIndex - 1 >= 0)
            selectedLineIndex--;

    } while (pressedKey != ConsoleKey.Enter);

    Console.WriteLine($"{Cities[selectedLineIndex]} was chosen as an option");        
}
static void UpdateMenu(int index)
{
    Console.Clear();
    foreach (var city in Cities)
    {
        bool isSelected = city == Cities[index];
        ChangeLineColor(isSelected);
        Console.WriteLine($"{(isSelected ? "> " : "  ")}{city}");
    }
}

static void ChangeLineColor(bool shouldHighlight)
{
    Console.BackgroundColor = shouldHighlight ? ConsoleColor.White : ConsoleColor.Black;
    Console.ForegroundColor = shouldHighlight ? ConsoleColor.Black : ConsoleColor.White;
}

Cities

  • It seems like this collection is fairly static, so you can define it as a class-level static field
  • In order to emphasize that the collection should not be changed during runtime you can mark the field as readonly
  • I would also suggest to use a simple array rather than a List for this simple use case

Index and KeyInfo

  • I would suggest to aim for more meaningful names
  • I've used selectedLineIndex and pressedKey to express the intent of the variables
  • I've also changed the data type of keyinfo from ConsoleKeyInfo to ConsoleKey

The infinite loop

  • You have a single exit condition when the user has pressed Enter
  • So, we could use that in a do-while construction

Nested if statements

  • Your nested (two levels) if statements can be combined by using the && operator
  • With this change the conditions become a bit more complex
    • but the your intention will remain clear

Write Cities

  • Since we have moved the cities collection to class level that's why we don't need to pass it as a parameter
  • I've also changed the name of the method to UpdateMenu to express the intent
    • I've declared the UpdateMenu as a static method outside of Main
  • Please note that in case of C# the parameter's naming convention is camel casing
  • I've refactored your code a bit to streamline your logic

Changing Colors

  • I've extract your line color changing logic into its own method to reduce code duplication
  • I've used conditional ternary operators to choose the right colors

UPDATE #1

I just noticed that there is some problem with this code. When you press down and the last city is already selected the background changes to white and the city black

According to my understanding you were referring to this bug

The bug

The root cause of the problem is that the UpdateMenu is called unconditionally inside the do-while loop. It should be called only if the selection is changed.

One way to fix this problem is to store the previously selected line index and the current index. The update of the menu will be called only if the numbers differ.

static void Main()
{
    int previousLineIndex = -1 , selectedLineIndex = 0;
    ConsoleKey pressedKey;

    do
    {
        if(previousLineIndex != selectedLineIndex)
        {
            UpdateMenu(selectedLineIndex);
            previousLineIndex = selectedLineIndex;
        }
        pressedKey = Console.ReadKey().Key;

        ...

    } while (pressedKey != ConsoleKey.Enter);
    ...      
}

UPDATE #2

Yes that is the bug a was talking about. However it still has this problem when you are on the last element and you go up the list.

second bug

This strange background colouring issue drive my attention to the ChangeLineColor method. After re-reading the code I've just realised that we don't need to change all the time the Background and Foreground colors. It is enough to do that only once when we draw the selected menu:

  • Change colors to the inverted ones
  • Print the selected line
  • Change colors to the original ones

After the above code change the code looks like this

static readonly string[] Cities = new []
{
    "New York",
    "London",
    "Mumbai",
    "Chicago"
};

static void Main()
{
    int previousLineIndex = -1 , selectedLineIndex = 0;
    ConsoleKey pressedKey;

    do
    {
        if(previousLineIndex != selectedLineIndex)
        {
            UpdateMenu(selectedLineIndex);
            previousLineIndex = selectedLineIndex;
        }
        pressedKey = Console.ReadKey().Key;

        if (pressedKey == ConsoleKey.DownArrow && selectedLineIndex + 1 < Cities.Length)
            selectedLineIndex++;

        else if (pressedKey == ConsoleKey.UpArrow && selectedLineIndex - 1 >= 0)
            selectedLineIndex--;

    } while (pressedKey != ConsoleKey.Enter);

    Console.WriteLine($"{Cities[selectedLineIndex]} was chosen as an option");        
}
static void UpdateMenu(int index)
{
    Console.Clear();
    foreach (var city in Cities)
    {
        bool isSelected = city == Cities[index];
        if (isSelected)
            DrawSelectedMenu(city);
        else
            Console.WriteLine($"  {city}");
    }
}

static void DrawSelectedMenu(string city)
{
    Console.BackgroundColor = ConsoleColor.White;
    Console.ForegroundColor = ConsoleColor.Black;
    Console.WriteLine($"> {city}");
    Console.BackgroundColor = ConsoleColor.Black;
    Console.ForegroundColor = ConsoleColor.White;
}

And gladly the bug is gone

after bug fixes

\$\endgroup\$
11
  • 1
    \$\begingroup\$ Thanks @Peter Csala for the feed back. This helped me a lot. Have a nice day! \$\endgroup\$ Commented Dec 17, 2022 at 14:27
  • 1
    \$\begingroup\$ @HappyCoconut Let me check that later today \$\endgroup\$ Commented Dec 19, 2022 at 17:03
  • 1
    \$\begingroup\$ @HappyCoconut I've updated my post with a fix. Apologize for the inconvenience. (BTW I could not reproduce the problem inside the Visual Studio for Mac's terminal ... which is strange... ) \$\endgroup\$ Commented Dec 19, 2022 at 19:32
  • 1
    \$\begingroup\$ @HappyCoconut Could you please create a screen recording about it? \$\endgroup\$ Commented Dec 19, 2022 at 20:12
  • 1
    \$\begingroup\$ Tomorrow I will try to fix that as well \$\endgroup\$ Commented Dec 19, 2022 at 20:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.