Now you put together everything you’ve learned: variables, input, branches, loops, lists and methods. The result is a notebook on the console. You write notes into it, have it show them, search through them and delete what’s done. In the next lesson it learns to remember the notes after the program ends.

The Kladde

Merchants used to keep a book for the first draft, the Kladde (German for a rough daybook). Whatever happened during the day went in there first, and only later, neatly, into the ledger. Your notebook is the first building block of the academy’s practice project that carries this name. In the follow-up courses it keeps growing: first into a program with contacts and appointments, later into an application in the browser. All the people and data in it are made up.

Plan first, then type

Before you write the first line, think about what the program should do and how people use it. The notebook shows a menu, reads a choice, does what was chosen and shows the menu again, until someone quits:

1  New note
2  Show all notes
3  Search
4  Delete a note
0  Quit
Your choice:

That gives you the structure:

  • A list List<string> holds the notes.
  • A loop shows the menu until someone chooses “0”.
  • A chain of if and else if decides what happens for which choice.
  • Each menu item gets its own method: AddNote, ShowAll, Search, Delete. That keeps the main program short, and each method has one job.

You build this in steps. After each step the program runs, and you try it out. That way you find a mistake right where you just made it, instead of hunting for it in a hundred lines at the end.

Step 1: the skeleton

The skeleton shows the menu and reacts to the choice, but it can’t do anything yet. Create notebook.cs:

C#
// notebook.cs
Console.WriteLine("Your notebook");
bool running = true;
while (running)
{
    ShowMenu();
    string choice = (Console.ReadLine() ?? "0").Trim();
    if (choice == "0")
    {
        running = false;
    }
    else if (choice == "1" || choice == "2" || choice == "3" || choice == "4")
    {
        Console.WriteLine("Coming soon.");
    }
    else
    {
        Console.WriteLine("There is no such choice.");
    }
}
Console.WriteLine("See you soon!");

static void ShowMenu()
{
    Console.WriteLine();
    Console.WriteLine("1  New note");
    Console.WriteLine("2  Show all notes");
    Console.WriteLine("3  Search");
    Console.WriteLine("4  Delete a note");
    Console.WriteLine("0  Quit");
    Console.Write("Your choice: ");
}

Run it and try a 1, a 7 and finally the 0:

Your notebook

1  New note
2  Show all notes
3  Search
4  Delete a note
0  Quit
Your choice: 1
Coming soon.

1  New note
2  Show all notes
3  Search
4  Delete a note
0  Quit
Your choice: 7
There is no such choice.

1  New note
2  Show all notes
3  Search
4  Delete a note
0  Quit
Your choice: 0
See you soon!

Two things are built this way on purpose:

  • The loop runs as long as running is true. The choice 0 sets running to false, and after that pass the loop ends. A bool variable with a telling name is easier to read here than while (true) with break.
  • ?? "0": when no more input comes, that counts as “Quit”. Without this case the program would show the menu over and over in an endless loop.

Step 2: adding and showing notes

Now the notebook gets its list and its first two methods. AddNote reads a text and stores it, but only if it isn’t empty:

C#
// excerpt from notebook.cs
static void AddNote(List<string> notes)
{
    Console.Write("Note: ");
    string text = (Console.ReadLine() ?? "").Trim();
    if (text == "")
    {
        Console.WriteLine("I won't add an empty note.");
        return;
    }
    notes.Add(text);
    Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}

The return in the middle of the method ends it early. This is a tried-and-true pattern: first catch the cases where there’s nothing to do, then do the actual work. That way the actual work doesn’t sit deeply indented inside an else.

ShowAll shows the notes numbered, counting from 1, or a hint if there aren’t any yet:

C#
// excerpt from notebook.cs
static void ShowAll(List<string> notes)
{
    if (notes.Count == 0)
    {
        Console.WriteLine("No notes yet.");
        return;
    }
    for (int i = 0; i < notes.Count; i++)
    {
        Console.WriteLine($"{i + 1}. {notes[i]}");
    }
}

Both methods get the list as a parameter. The list itself is created in the main program, once, before the loop. Because a list isn’t copied when you pass it on, all methods work on the same list: what AddNote adds, ShowAll sees. This is the whole program after step 2:

C#
// notebook.cs
List<string> notes = [];
Console.WriteLine("Your notebook");
bool running = true;
while (running)
{
    ShowMenu();
    string choice = (Console.ReadLine() ?? "0").Trim();
    if (choice == "1")
    {
        AddNote(notes);
    }
    else if (choice == "2")
    {
        ShowAll(notes);
    }
    else if (choice == "0")
    {
        running = false;
    }
    else if (choice == "3" || choice == "4")
    {
        Console.WriteLine("Coming soon.");
    }
    else
    {
        Console.WriteLine("There is no such choice.");
    }
}
Console.WriteLine("See you soon!");

static void ShowMenu()
{
    Console.WriteLine();
    Console.WriteLine("1  New note");
    Console.WriteLine("2  Show all notes");
    Console.WriteLine("3  Search");
    Console.WriteLine("4  Delete a note");
    Console.WriteLine("0  Quit");
    Console.Write("Your choice: ");
}

static void AddNote(List<string> notes)
{
    Console.Write("Note: ");
    string text = (Console.ReadLine() ?? "").Trim();
    if (text == "")
    {
        Console.WriteLine("I won't add an empty note.");
        return;
    }
    notes.Add(text);
    Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}

static void ShowAll(List<string> notes)
{
    if (notes.Count == 0)
    {
        Console.WriteLine("No notes yet.");
        return;
    }
    for (int i = 0; i < notes.Count; i++)
    {
        Console.WriteLine($"{i + 1}. {notes[i]}");
    }
}

How to test

Testing doesn’t mean taking one look to see whether it runs somehow. Testing means deliberately trying out whether the program does the right thing in every case, including the awkward ones. After step 2, go through this list:

  1. Choose 2 right after the start: does it say “No notes yet.”?
  2. Add a note, then choose 2: is it there with the number 1?
  3. Add a second note: does it say “Number of notes: 2”?
  4. Choose to add a note and just press Enter: do you get the hint about the empty note, and does the count stay the same?
  5. A note made of nothing but spaces: is it rejected too?
  6. A word or a 9 as the choice: do you get “There is no such choice.” without a crash?

For case 2 it looks like this:

Your choice: 1
Note: Dentist on Tuesday
Noted. Number of notes: 1

1  New note
2  Show all notes
3  Search
4  Delete a note
0  Quit
Your choice: 2
1. Dentist on Tuesday

In the follow-up course you write checks like these as automated tests that run by themselves after every change. The way of thinking is the same.

Exercise 1

Add menu item 3: a method Search that asks for a search term and shows all notes that contain it, no matter whether it’s written in uppercase or lowercase. For each match, show the number the note has in the whole list; you’ll need it for deleting in a moment. At the end, show the number of matches. The method rejects an empty search term.

Solution 1

The new method:

C#
// excerpt from notebook.cs
static void Search(List<string> notes)
{
    Console.Write("Search term: ");
    string searchTerm = (Console.ReadLine() ?? "").Trim();
    if (searchTerm == "")
    {
        Console.WriteLine("I can't search without a search term.");
        return;
    }
    int matches = 0;
    for (int i = 0; i < notes.Count; i++)
    {
        if (notes[i].Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"{i + 1}. {notes[i]}");
            matches++;
        }
    }
    Console.WriteLine($"Matches: {matches}");
}

The search uses for instead of foreach because it needs the number i + 1. The main program gets one more branch, and the 3 no longer answers “Coming soon.” The whole program:

C#
// notebook.cs
List<string> notes = [];
Console.WriteLine("Your notebook");
bool running = true;
while (running)
{
    ShowMenu();
    string choice = (Console.ReadLine() ?? "0").Trim();
    if (choice == "1")
    {
        AddNote(notes);
    }
    else if (choice == "2")
    {
        ShowAll(notes);
    }
    else if (choice == "3")
    {
        Search(notes);
    }
    else if (choice == "0")
    {
        running = false;
    }
    else if (choice == "4")
    {
        Console.WriteLine("Coming soon.");
    }
    else
    {
        Console.WriteLine("There is no such choice.");
    }
}
Console.WriteLine("See you soon!");

static void ShowMenu()
{
    Console.WriteLine();
    Console.WriteLine("1  New note");
    Console.WriteLine("2  Show all notes");
    Console.WriteLine("3  Search");
    Console.WriteLine("4  Delete a note");
    Console.WriteLine("0  Quit");
    Console.Write("Your choice: ");
}

static void AddNote(List<string> notes)
{
    Console.Write("Note: ");
    string text = (Console.ReadLine() ?? "").Trim();
    if (text == "")
    {
        Console.WriteLine("I won't add an empty note.");
        return;
    }
    notes.Add(text);
    Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}

static void ShowAll(List<string> notes)
{
    if (notes.Count == 0)
    {
        Console.WriteLine("No notes yet.");
        return;
    }
    for (int i = 0; i < notes.Count; i++)
    {
        Console.WriteLine($"{i + 1}. {notes[i]}");
    }
}

static void Search(List<string> notes)
{
    Console.Write("Search term: ");
    string searchTerm = (Console.ReadLine() ?? "").Trim();
    if (searchTerm == "")
    {
        Console.WriteLine("I can't search without a search term.");
        return;
    }
    int matches = 0;
    for (int i = 0; i < notes.Count; i++)
    {
        if (notes[i].Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"{i + 1}. {notes[i]}");
            matches++;
        }
    }
    Console.WriteLine($"Matches: {matches}");
}

With three notes and the search term “dentist”:

Your choice: 3
Search term: dentist
1. Dentist on Tuesday
3. Pay the dentist bill
Matches: 2

Exercise 2

Add menu item 4: a method Delete. It first shows all notes, then asks for the number and removes that note. A number that doesn’t exist, or input that isn’t a number, leads to a hint, not to a crash. Afterward the program says which note was deleted.

Keep two things in mind: people count from 1, the list counts from 0. And in an empty list there’s nothing to delete.

Solution 2

The new method:

C#
// excerpt from notebook.cs
static void Delete(List<string> notes)
{
    ShowAll(notes);
    if (notes.Count == 0)
    {
        return;
    }
    Console.Write("Delete which number? ");
    string input = Console.ReadLine() ?? "";
    if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
    {
        Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
        return;
    }
    string text = notes[number - 1];
    notes.RemoveAt(number - 1);
    Console.WriteLine($"Deleted: {text}");
}

Delete calls ShowAll so you have the numbers right in front of you. So one method may use another. The condition with TryParse checks three things at once: whether a number came in, whether it’s at least 1, and whether there are that many notes. Only then does the number turn into the position in the list, number - 1. The text is kept before the note is removed, so the message can name it.

For the 4, the main program now calls Delete, and no menu item has to wait anymore. This is where things stand at the end of this lesson:

C#
// notebook.cs
List<string> notes = [];
Console.WriteLine("Your notebook");
bool running = true;
while (running)
{
    ShowMenu();
    string choice = (Console.ReadLine() ?? "0").Trim();
    if (choice == "1")
    {
        AddNote(notes);
    }
    else if (choice == "2")
    {
        ShowAll(notes);
    }
    else if (choice == "3")
    {
        Search(notes);
    }
    else if (choice == "4")
    {
        Delete(notes);
    }
    else if (choice == "0")
    {
        running = false;
    }
    else
    {
        Console.WriteLine("There is no such choice.");
    }
}
Console.WriteLine("See you soon!");

static void ShowMenu()
{
    Console.WriteLine();
    Console.WriteLine("1  New note");
    Console.WriteLine("2  Show all notes");
    Console.WriteLine("3  Search");
    Console.WriteLine("4  Delete a note");
    Console.WriteLine("0  Quit");
    Console.Write("Your choice: ");
}

static void AddNote(List<string> notes)
{
    Console.Write("Note: ");
    string text = (Console.ReadLine() ?? "").Trim();
    if (text == "")
    {
        Console.WriteLine("I won't add an empty note.");
        return;
    }
    notes.Add(text);
    Console.WriteLine($"Noted. Number of notes: {notes.Count}");
}

static void ShowAll(List<string> notes)
{
    if (notes.Count == 0)
    {
        Console.WriteLine("No notes yet.");
        return;
    }
    for (int i = 0; i < notes.Count; i++)
    {
        Console.WriteLine($"{i + 1}. {notes[i]}");
    }
}

static void Search(List<string> notes)
{
    Console.Write("Search term: ");
    string searchTerm = (Console.ReadLine() ?? "").Trim();
    if (searchTerm == "")
    {
        Console.WriteLine("I can't search without a search term.");
        return;
    }
    int matches = 0;
    for (int i = 0; i < notes.Count; i++)
    {
        if (notes[i].Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"{i + 1}. {notes[i]}");
            matches++;
        }
    }
    Console.WriteLine($"Matches: {matches}");
}

static void Delete(List<string> notes)
{
    ShowAll(notes);
    if (notes.Count == 0)
    {
        return;
    }
    Console.Write("Delete which number? ");
    string input = Console.ReadLine() ?? "";
    if (!int.TryParse(input, out int number) || number < 1 || number > notes.Count)
    {
        Console.WriteLine($"Please enter a number from 1 to {notes.Count}.");
        return;
    }
    string text = notes[number - 1];
    notes.RemoveAt(number - 1);
    Console.WriteLine($"Deleted: {text}");
}

Test deleting at the edges: the number 0, the highest number, a number above it, a word, and deleting from an empty list.

What you can do now

  • You plan a program before you write it: what it can do, how people use it, which parts it needs.
  • You build a menu from a loop, a bool variable and a chain of if and else if.
  • You pass a list to methods that all work on the same list.
  • You catch empty and wrong input before the actual work begins.
  • You extend a program in steps and test it after each step, including the awkward cases.

Read more