Post

šŸ› ļøCulminating Project: Build Your Own Miniature Task Tracker in C#!

šŸ› ļøCulminating Project: Build Your Own Miniature Task Tracker in C#!

Welcome to the final project in our C# learning journey: Building a Miniature Task Tracker! šŸŽÆ

This step-by-step console application project ties together everything we’ve learned so far — from variables and control flow to classes, inheritance, interfaces, and even delegates and lambdas. It’s more than just a coding exercise — it’s a challenge for you to apply your knowledge and build something functional from scratch.

🚧 Project Structure

The project is broken into several parts, each focusing on key OOP principles:

  1. Task Class – Define a basic class with constructors, fields, and a DisplayDetails() method.
  2. Task Manager – Manage a list of tasks using collections and basic menu-based UI.
  3. Encapsulation – Refactor fields to private, and add properties and validation logic.
  4. Inheritance and Polymorphism – Extend Task with PriorityTask and DeadlineTask using override.
  5. Interfaces – Implement an ITaggable interface for adding/removing tags.
  6. Delegates & Lambdas – Add sorting and filtering features using delegate patterns and lambda expressions.
  7. Review & Next Steps – A complete walkthrough of your final application and optional advanced challenges.

Key Concepts Applied

  • Object-Oriented Programming: encapsulation, inheritance, polymorphism
  • Interfaces and abstraction
  • Delegates and anonymous methods
  • Control flow with menus and user interaction
  • Collections (List<T>) and LINQ (optional)
  • Validation and property logic
  • Hands-on debugging and project structure planning

What You’ll Build

A console-based application that allows users to:

  • Add tasks with title and description
  • View all tasks and their completion status
  • Mark tasks as complete or incomplete
  • Sort tasks by title or ID
  • Filter tasks by completion status
  • Add or remove tags from tasks
  • View tasks with associated tags

Let’s Start

Open your IDE, create those .cs files, and start coding! The guide provides you with all the boilerplate, instructions, and challenges to get the most out of your practice.

If you’ve been following this blog series, this project is your final boss battle. Don’t just read — build.

And once you’ve completed it, share your version with us. Tag your GitHub repo or email me — I’d love to see how you extended the app beyond the tutorial.

Project: Building a Miniature Task Tracker

Welcome to your culminating project! In this guide, we’ll build a Miniature Task Tracker console application step-by-step. This project is designed to bring together all the C# concepts you’ve learned throughout this blog series, from basic variables and control flow to classes, inheritance, interfaces, and even delegates and lambdas.

Each part will guide you through a new feature or refinement, providing explanations and some starting code. However, there will be intentional gaps where you’ll need to implement logic based on what you’ve learned in previous posts. This is your chance to apply your knowledge and build a functional application from the ground up!

Let’s get started!


Part 1: Laying the Foundation - The Basic Task Class

Concept Focus: Basic Class Definition, Fields, Static Members, Constructors, Simple Methods.

Our first step is to define what a ā€œTaskā€ looks like in our system. We’ll create a simple class to represent a task with its essential details.

1.1 Defining the Task Class

Create a new C# file, for example, Task.cs, and define the Task class as follows. For now, we’ll make the fields public for simplicity. We will refine this in Part 3 when we discuss encapsulation and properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Task.cs
using System;

public class Task
{
    // Fields to store task information
    public int Id;
    public string Title;
    public string Description;
    public bool IsCompleted;

    // A static field to help generate unique IDs for each new task.
    // 'static' means this field belongs to the Task class itself, not to any specific Task object.
    // It's shared across all Task objects.
    private static int _nextId = 1;

    // Constructor: This special method is called when you create a new Task object.
    // It initializes the new object.
    public Task(string title, string description)
    {
        this.Id = _nextId++; // Assign the current _nextId then increment it for the next task
        this.Title = title;
        this.Description = description;
        this.IsCompleted = false; // New tasks are not completed by default
    }

    // Method to display the task's details
    public void DisplayDetails()
    {
        // YOUR CODE HERE: Implement the logic to display task details.
        // This method should print the Id, Title, Description, and IsCompleted status
        // to the console in a clear, readable format.
        // For example:
        // Console.WriteLine($"ID: {this.Id}");
        // Console.WriteLine($"Title: {this.Title}");
        // ... and so on for Description and IsCompleted status (e.g., "Status: Completed" or "Status: Pending").
    }
}

1.2 Understanding the Code

  • Fields: Id, Title, Description, and IsCompleted are variables that will hold the data for each individual task object.
  • _nextId (Static Field): This static integer is used to automatically assign a unique Id to each new task. Because it’s static, its value is shared across all instances of the Task class. Each time a new Task is created, _nextId is used for the Id and then incremented.
  • Constructor (public Task(string title, string description)): This is the blueprint for creating new Task objects. It takes a title and description as input, assigns a unique Id, and sets IsCompleted to false by default.
  • DisplayDetails() Method: This method will be responsible for printing the information of a Task object to the console.

1.3 Your Task: Implement DisplayDetails()

Your first challenge is to complete the DisplayDetails() method within the Task.cs file.

Instructions:

  1. Open Task.cs.
  2. Locate the DisplayDetails() method.
  3. Inside this method, write C# code using Console.WriteLine() to print out:
    • The task’s Id.
    • The task’s Title.
    • The task’s Description.
    • The task’s completion status (e.g., display ā€œStatus: Completedā€ if IsCompleted is true, and ā€œStatus: Pendingā€ if it’s false). You can use an if statement here (refer back to your Control Flow lesson!).

Hint: Remember how to use string interpolation (`$ā€ā€¦


Part 2: Managing Our Tasks - The TaskManager Class and Basic Menu

Concept Focus: List<T> for collections, Basic Control Flow (while, switch or if-else if, foreach), User Input, Basic Class for Management.

Now that we can represent a single task, we need a way to manage a collection of them. We also need a basic menu system for our users to interact with the application.

2.1 Introducing the TaskManager Class

We’ll create a new class called TaskManager to handle operations like adding new tasks and viewing all tasks. Create a new file, TaskManager.cs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// TaskManager.cs
using System;
using System.Collections.Generic; // Required for using List<T>

public class TaskManager
{
    // A private list to hold all the Task objects.
    // We use List<Task> because it's a flexible collection that can grow as we add tasks.
    private List<Task> _tasks = new List<Task>();

    // Method to add a new task based on user input
    public void AddTaskFromUserInput()
    {
        Console.WriteLine("\n--- Add New Task ---");
        Console.Write("Enter Task Title: ");
        string title = Console.ReadLine();

        Console.Write("Enter Task Description: ");
        string description = Console.ReadLine();

        // YOUR CODE HERE: Create a new Task object and add it to the _tasks list.
        // 1. Create a new Task object using the 'title' and 'description' obtained from the user.
        //    (Remember the Task constructor from Part 1?)
        // 2. Add this newly created Task object to the '_tasks' list.
        //    (Refer to your notes on how to add items to a List<T>.)

        // Console.WriteLine("Task added successfully!"); // Uncomment this after implementing the above
    }

    // Method to display all tasks
    public void ViewAllTasks()
    {
        Console.WriteLine("\n--- All Tasks ---");
        // YOUR CODE HERE: Implement the logic to view all tasks.
        // 1. Check if the '_tasks' list is empty (i.e., its Count is 0).
        //    If it is empty, print a message like "No tasks available."
        // 2. If the list is not empty, iterate through each Task object in the '_tasks' list.
        //    (A 'foreach' loop is perfect for this! Refer to your Control Flow lesson.)
        // 3. For each Task object in the list, call its 'DisplayDetails()' method (which you implemented in Part 1).
        //    You might want to print a separator (e.g., "-----") between tasks for better readability.
    }

    // We will add more methods here in later parts (e.g., FindTaskById, MarkTaskComplete, RemoveTask)
}

2.2 Creating the Main Application Loop (Program.cs)

Now, let’s set up the main entry point of our application. This will be in your Program.cs file. It will contain the main loop that shows a menu to the user and processes their choices.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Program.cs
using System;

public class Program
{
    public static void Main()
    {
        TaskManager taskManager = new TaskManager(); // Create an instance of our TaskManager
        bool keepRunning = true;

        Console.WriteLine("Welcome to the Miniature Task Tracker!");

        while (keepRunning)
        {
            Console.WriteLine("\nMain Menu:");
            Console.WriteLine("1. Add New Task");
            Console.WriteLine("2. View All Tasks");
            Console.WriteLine("3. Exit");
            Console.Write("Select an option: ");

            string choice = Console.ReadLine();

            // YOUR CODE HERE: Implement the main menu logic using a switch or if-else if statement.
            // Based on the user's 'choice':
            // - If "1": Call the 'taskManager.AddTaskFromUserInput()' method.
            // - If "2": Call the 'taskManager.ViewAllTasks()' method.
            // - If "3": Set 'keepRunning' to 'false' to exit the loop. Also, print a goodbye message.
            // - Default/Else: Print an "Invalid option, please try again." message.
            //
            // Remember to refer to your Control Flow lesson for 'switch' or 'if-else if' syntax.
        }

        // Console.WriteLine("Thank you for using the Task Tracker. Goodbye!"); // Uncomment when choice "3" is handled
    }
}

2.3 Understanding the New Code

  • TaskManager Class: This class will be the central hub for managing our tasks. For now, it holds a List<Task> called _tasks.
    • List<Task>: This is a generic collection from System.Collections.Generic. It allows us to store an ordered list of Task objects and easily add or remove them.
  • Program.cs - Main Method: This is where our application starts.
    • We create an instance of TaskManager.
    • A while loop keeps the menu running until the user chooses to exit.
    • Inside the loop, we display options and read the user’s choice.

2.4 Your Tasks: Implement TaskManager Methods and Menu Logic

You have two main areas to complete in this part:

  1. In TaskManager.cs:
    • Complete the AddTaskFromUserInput() method:
      • Get the title and description from the user (the code for this is provided).
      • Create a new Task object using these details.
      • Add the new Task object to the _tasks list.
    • Complete the ViewAllTasks() method:
      • Check if there are any tasks in the _tasks list.
      • If not, display a message like ā€œNo tasks available.ā€
      • If there are tasks, loop through the _tasks list and call DisplayDetails() for each task.
  2. In Program.cs:
    • Inside the while loop, implement the logic (using a switch statement or if-else if statements) to handle the user’s menu choice:
      • If the choice is ā€œ1ā€, call taskManager.AddTaskFromUserInput().
      • If the choice is ā€œ2ā€, call taskManager.ViewAllTasks().
      • If the choice is ā€œ3ā€, set keepRunning to false and print a goodbye message.
      • For any other input, print an error message.

Hints:

  • Remember to instantiate a Task object using new Task(title, description);.
  • To add to a list, use the .Add() method (e.g., _tasks.Add(newTask);).
  • A foreach (Task task in _tasks) loop is ideal for iterating through the list.
  • For the menu, a switch (choice) statement is often cleaner for multiple options.

Compile and run your application after implementing these parts. You should be able to add tasks and view them!


Part 3: Enhancing the Task Class - Encapsulation and Properties

Concept Focus: Encapsulation (private fields), Properties (getters, setters), Property Validation, Methods for state change.

In Part 1, we made our Task class fields public for simplicity. However, good object-oriented design often involves encapsulation – bundling data (fields) with the methods that operate on that data, and restricting direct access to an object’s internal state. We achieve this by making fields private and providing controlled access through public properties.

3.1 Refactoring Task Class for Encapsulation

Let’s modify Task.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Task.cs
using System;

public class Task // We will make this implement ITaggable in Part 5
{
    // Fields are now private to enforce encapsulation
    private int _id;
    private string _title;
    private string _description;
    private bool _isCompleted;
    // private List<string> _tags = new List<string>(); // Will be added in Part 5 for ITaggable

    private static int _nextId = 1;

    // Properties provide controlled public access to private fields
    public int Id { get { return _id; } } // Read-only after object creation

    public string Title
    {
        get { return _title; }
        set
        {
            // YOUR CODE HERE: Implement validation for the Title.
            // 1. Check if the incoming 'value' is null or empty (e.g., using string.IsNullOrEmpty(value)).
            // 2. If it is null or empty, print an error message to the console (e.g., "Title cannot be empty.")
            //    and DO NOT update _title.
            // 3. If 'value' is valid (not null or empty), then assign it to the private field '_title'.
            // _title = value; // This line should be inside your validation logic
        }
    }

    public string Description
    {
        get { return _description; }
        set { _description = value; } // Simple setter for now, could add validation
    }

    public bool IsCompleted
    {
        get { return _isCompleted; }
        // Setter is private because we want to control completion status via methods like MarkComplete()
        private set { _isCompleted = value; }
    }

    // Constructor: Updated to initialize private fields
    public Task(string title, string description)
    {
        this._id = _nextId++;
        // Use the Title property here to leverage its validation logic during construction!
        this.Title = title;
        this.Description = description;
        this.IsCompleted = false;
    }

    // Method to mark the task as complete
    public void MarkComplete()
    {
        this.IsCompleted = true;
        Console.WriteLine($"Task '{this.Title}' marked as complete.");
    }

    // YOUR TASK: Implement a method to mark the task as incomplete
    public void MarkIncomplete()
    {
        // YOUR CODE HERE:
        // 1. Set the IsCompleted property (or private _isCompleted field) to false.
        // 2. Print a confirmation message to the console, e.g., $"Task
        //    '{this.Title}' marked as pending."
    }

    // DisplayDetails method (make it virtual for Part 4)
    public virtual void DisplayDetails() // Added 'virtual' keyword
    {
        Console.WriteLine($"ID: {this.Id}");
        Console.WriteLine($"Title: {this.Title}");
        Console.WriteLine($"Description: {this.Description}");
        string status = this.IsCompleted ? "Completed" : "Pending";
        Console.WriteLine($"Status: {status}");
        // We will add tag display here in Part 5
    }

    // We will add ITaggable interface methods here in Part 5
}

3.2 Understanding the Changes

  • Private Fields: _id, _title, _description, _isCompleted are now private. This means they can only be accessed from within the Task class itself.
  • Public Properties:
    • Id: Has only a get accessor, making it read-only from outside the class once the object is constructed. Its value is set in the constructor.
    • Title: Has get and set. The set accessor is where you’ll add validation.
    • Description: Simple get and set for now.
    • IsCompleted: The set accessor is private. This means the completion status can only be changed by methods within the Task class (like MarkComplete() and the MarkIncomplete() you will write). This provides better control over the object’s state.
  • Constructor Update: Now initializes the private fields. Notice this.Title = title; uses the property, so the validation logic in the Title property’s setter will run even during object creation.
  • MarkComplete() Method: A public method to change the IsCompleted state.
  • virtual Keyword: Added to DisplayDetails() in preparation for Part 4 (Inheritance).

3.3 Your Tasks: Implement Validation and MarkIncomplete()

  1. In Task.cs - Title Property Setter:
    • Implement the validation logic as described in the comments. If the provided value for the title is null or empty, print an error and don’t change _title. Otherwise, update _title.
  2. In Task.cs - MarkIncomplete() Method:
    • Implement this method to set IsCompleted to false and print a confirmation message.

3.4 Updating TaskManager and Program.cs (Conceptual - Reader Challenge)

Now that Task.IsCompleted has a private setter, you can’t directly change it from TaskManager. You’ll need to use the MarkComplete() and MarkIncomplete() methods.

Challenge for the Reader (Guidance, not explicit code to copy):

  1. In TaskManager.cs:
    • Create a new public method, e.g., public void ToggleTaskCompletionStatusById(). This method should:
      • Prompt the user for a Task ID.
      • Attempt to parse the ID to an integer.
      • Find the task in the _tasks list (you might need a helper method private Task FindTaskById(int id) that loops through _tasks and returns the task if found, or null if not).
      • If the task is found:
        • If task.IsCompleted is true, call task.MarkIncomplete().
        • Else, call task.MarkComplete().
      • If the task is not found, print an error message.
  2. In Program.cs:
    • Add a new menu option (e.g., ā€œ4. Mark Task as Complete/Incompleteā€).
    • In your switch or if-else if block, call the new taskManager.ToggleTaskCompletionStatusById() method when this option is selected.

Test your application. You should be able to add tasks, view them, and now mark them as complete or incomplete using your new menu option. Check if your title validation works!

DueDate: {this.DueDate.ToShortDateString()}ā€);`).

  1. Update TaskManager and Program.cs (Reader Challenge):
    • Modify your AddTaskFromUserInput() method in TaskManager.cs (or create a new, more versatile method) to allow the user to choose what type of task they want to create (e.g., Generic Task, Priority Task, Deadline Task).
    • Based on the user’s choice, prompt for the specific information needed (e.g., priority level or due date).
    • Instantiate the correct task type and add it to the _tasks list.
    • When viewing tasks, the overridden DisplayDetails() methods should automatically show the specific information for each task type thanks to polymorphism.

Test your application. You should now be able to add different types of tasks and see their specific details displayed correctly!


Part 5: Adding Capabilities - Interfaces

Concept Focus: Defining Interfaces, Implementing Interfaces in classes.

Interfaces define a contract of capabilities that a class can agree to implement. They specify what a class must do, but not how. This is useful for adding common functionalities to different classes, even if those classes are not related by inheritance.

Let’s say we want to add a tagging system to our tasks.

5.1 Defining the ITaggable Interface

Create a new C# file, for example, ITaggable.cs, and define the interface:

1
2
3
4
5
6
7
8
9
10
11
12
// ITaggable.cs
using System.Collections.Generic;

public interface ITaggable
{
    // Property contract: Implementing classes must have a Tags property (read-only list)
    List<string> Tags { get; }

    // Method contracts: Implementing classes must provide these methods
    void AddTag(string tag);
    void RemoveTag(string tag);
}

5.2 Understanding the Interface

  • public interface ITaggable: Defines an interface named ITaggable. By convention, interface names often start with ā€œIā€.
  • List<string> Tags { get; }: This declares a read-only property. Any class implementing ITaggable must provide a public getter for a List<string> named Tags.
  • void AddTag(string tag); and void RemoveTag(string tag);: These declare method signatures. Implementing classes must provide public methods with these names and parameters.
  • No Implementation: Notice that interfaces only declare the members; they don’t provide any implementation (no method bodies, no property logic).

5.3 Implementing ITaggable in the Task Class

Now, let’s make our base Task class implement the ITaggable interface. This means all task types (including PriorityTask and DeadlineTask through inheritance) will gain tagging capabilities.

Modify Task.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Task.cs
using System;
using System.Collections.Generic; // Make sure this is included

public class Task : ITaggable // Add ", ITaggable" to declare implementation
{
    // ... (existing private fields: _id, _title, _description, _isCompleted)
    // ... (existing static _nextId)

    // Add a private field to store the tags for this task
    private List<string> _tags = new List<string>();

    // ... (existing Properties: Id, Title, Description, IsCompleted)

    // Constructor: No changes needed here for ITaggable initially
    public Task(string title, string description)
    {
        this._id = _nextId++;
        this.Title = title;
        this.Description = description;
        this.IsCompleted = false;
    }

    // ... (existing methods: MarkComplete, MarkIncomplete)

    // --- ITaggable Implementation ---
    // Property implementation for ITaggable
    public List<string> Tags { get { return new List<string>(_tags); } } // Return a copy for read-only safety

    // Method implementation for ITaggable
    public void AddTag(string tag)
    {
        // YOUR CODE HERE: Implement AddTag
        // 1. Check if the provided 'tag' is not null or empty and not already in the _tags list.
        //    (You can use _tags.Contains(tag) to check if it's already there).
        // 2. If valid and not a duplicate, add the 'tag' to the private '_tags' list.
        // 3. Print a confirmation message, e.g., $"Tag
        //    '{tag}' added to task
        //    '{this.Title}'."
    }

    public void RemoveTag(string tag)
    {
        // YOUR CODE HERE: Implement RemoveTag
        // 1. Attempt to remove the 'tag' from the private '_tags' list.
        //    (The _tags.Remove(tag) method returns true if the item was found and removed, false otherwise).
        // 2. Based on whether removal was successful, print an appropriate message
        //    (e.g., $"Tag
        //    '{tag}' removed." or $"Tag
        //    '{tag}' not found.").
    }
    // --- End ITaggable Implementation ---

    public override void DisplayDetails() // Modified from Part 4 to be override if Task was derived
                                        // If Task is your ultimate base, it should be virtual as before.
                                        // For this project, let's assume Task is the base, so 'virtual' is correct.
    // public virtual void DisplayDetails() // This was from Part 3, ensure it's virtual
    {
        Console.WriteLine($"ID: {this.Id}");
        Console.WriteLine($"Title: {this.Title}");
        Console.WriteLine($"Description: {this.Description}");
        string status = this.IsCompleted ? "Completed" : "Pending";
        Console.WriteLine($"Status: {status}");

        // YOUR CODE HERE: Display Tags
        // 1. Check if the _tags list has any tags (e.g., _tags.Count > 0).
        // 2. If it does, print a heading like "Tags:" and then print all the tags,
        //    perhaps separated by commas (e.g., using string.Join(", ", _tags)).
    }
}

5.4 Understanding Interface Implementation

  • public class Task : ITaggable: The Task class now declares that it implements the ITaggable interface.
  • private List<string> _tags = new List<string>();: A private list is added to actually store the tags for each task instance.
  • public List<string> Tags { get { return new List<string>(_tags); } }: This implements the Tags property from ITaggable. We return a new copy of the _tags list to prevent external code from modifying the internal list directly through the property (upholding read-only intent of the interface property for the list itself, though the list content can be modified via AddTag/RemoveTag).
  • AddTag(string tag) and RemoveTag(string tag): These are the concrete implementations of the methods defined in ITaggable.

5.5 Your Tasks: Implement Interface Methods and Update Display

  1. In Task.cs - AddTag(string tag) Method:
    • Implement the logic to add a tag: check for null/empty, check for duplicates, add to _tags, and print confirmation.
  2. In Task.cs - RemoveTag(string tag) Method:
    • Implement the logic to remove a tag: attempt removal from _tags and print appropriate feedback.
  3. In Task.cs - DisplayDetails() Method:
    • Modify this method to also display the tags associated with the task if any exist.

5.6 Updating TaskManager and Program.cs (Reader Challenge)

Challenge for the Reader (Guidance):

  1. In Program.cs:
    • Add new menu options like ā€œ5. Add Tag to Taskā€ and ā€œ6. Remove Tag from Taskā€.
  2. In TaskManager.cs:
    • Create new public methods like public void AddTagToTaskFromUserInput() and public void RemoveTagFromTaskUserInput().
    • These methods will:
      • Prompt the user for a Task ID to identify the task.
      • Find the task (you can reuse or adapt your FindTaskById helper method).
      • If the task is found, prompt the user for the tag string.
      • Call the task’s AddTag() or RemoveTag() method.
      • Ensure you are calling these methods on an object that is known to be ITaggable. Since our base Task class implements it, any Task (or its derived types) can be cast to ITaggable if needed, or you can directly call the methods if they are public on Task.

Test your application. You should be able to add tasks, and then add or remove tags from them. When you view task details, the tags should also be displayed.


Part 6: Advanced Operations - Delegates, Lambdas, Sorting, and Filtering

Concept Focus: Delegates for defining method signatures, Lambdas for concise inline methods, List<T>.Sort() with custom comparison, LINQ for querying (optional).

Our Task Tracker is becoming quite functional! Now, let’s add some more advanced capabilities like sorting tasks based on different criteria and filtering them. This is a great place to use delegates and lambda expressions.

6.1 Defining a Delegate for Task Comparison

A delegate is like a blueprint for a method. We can define a delegate that specifies the signature for a method that compares two Task objects. This will allow us to pass different sorting logics to a sorting method.

Let’s define this delegate. You can place it within the TaskManager.cs file, or in its own file if you prefer, or even inside the Program.cs if it’s only used there. For simplicity, let’s add it to TaskManager.cs for now, outside the class definition or as a nested type if you prefer.

1
2
3
4
5
6
// At the top of TaskManager.cs or within its namespace, but outside the TaskManager class itself
// Or, if you prefer, as a nested delegate within TaskManager: public delegate int TaskComparisonDelegate(Task t1, Task t2);
public delegate int TaskComparisonDelegate(Task t1, Task t2);

// TaskManager.cs continues...
// public class TaskManager { ... }

This TaskComparisonDelegate defines that any method matching its signature must take two Task objects (t1, t2) and return an int. The returned integer follows the standard comparison pattern:

  • Less than 0: t1 comes before t2.
  • 0: t1 and t2 are considered equal in terms of sort order.
  • Greater than 0: t1 comes after t2.

6.2 Implementing Sorting in TaskManager

Now, let’s add a method to TaskManager that can sort the _tasks list using a comparison logic provided via our delegate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// In TaskManager.cs
// ... (existing code)

public class TaskManager
{
    private List<Task> _tasks = new List<Task>();
    // ... (existing methods: AddTaskFromUserInput, ViewAllTasks, ToggleTaskCompletionStatusById, AddTagToTaskFromUserInput, etc.)

    public void SortTasks(TaskComparisonDelegate comparisonLogic)
    {
        if (_tasks == null || _tasks.Count == 0)
        {
            Console.WriteLine("No tasks to sort.");
            return;
        }

        // The List<T>.Sort() method can take a Comparison<T> delegate.
        // Our TaskComparisonDelegate is compatible with Comparison<Task>.
        _tasks.Sort(new Comparison<Task>(comparisonLogic));
        Console.WriteLine("Tasks sorted.");
        ViewAllTasks(); // Display sorted tasks
    }

    // YOUR TASK: Implement Filtering Methods
    public void ViewFilteredTasks(Func<Task, bool> filterPredicate, string filterDescription)
    {
        Console.WriteLine($"\n--- Filtered Tasks: {filterDescription} ---");
        if (_tasks == null || _tasks.Count == 0)
        {
            Console.WriteLine("No tasks available to filter.");
            return;
        }

        // YOUR CODE HERE: Implement task filtering
        // 1. Use LINQ's .Where() method with the 'filterPredicate' to get a new list of filtered tasks.
        //    Example: var filteredTasks = _tasks.Where(filterPredicate).ToList();
        //    (Make sure you have 'using System.Linq;' at the top of TaskManager.cs)
        // 2. If no tasks match the filter, print a message like "No tasks match the filter: {filterDescription}".
        // 3. Otherwise, iterate through 'filteredTasks' and call task.DisplayDetails() for each.
        //    If you prefer not to use LINQ yet, you can achieve this with a foreach loop and an if statement:
        //    List<Task> filteredTasks = new List<Task>();
        //    foreach (Task task in _tasks)
        //    {
        //        if (filterPredicate(task))
        //        {
        //            filteredTasks.Add(task);
        //        }
        //    }
        //    // Then proceed to display filteredTasks or the 'no tasks match' message.
    }
}

6.3 Using Lambdas for Sorting and Filtering in Program.cs

Lambda expressions provide a very concise way to write anonymous methods, which are perfect for providing implementations for delegates on the fly.

Let’s update Program.cs to add sorting and filtering options.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// In Program.cs
// ... (existing Main method structure)
// Add 'using System.Linq;' if you plan to use LINQ in TaskManager for filtering.

            // Inside the while loop for the menu:
            Console.WriteLine("\nMain Menu:");
            // ... (existing menu options 1-6 for Add, View, Toggle Complete, Add Tag, Remove Tag, Exit)
            Console.WriteLine("7. Sort Tasks by Title");
            Console.WriteLine("8. Sort Tasks by ID");
            Console.WriteLine("9. View Completed Tasks");
            Console.WriteLine("10. View Pending Tasks");
            Console.WriteLine("11. Exit"); // Adjust exit number
            Console.Write("Select an option: ");

            string choice = Console.ReadLine();

            switch (choice)
            {
                // ... (cases for 1-6)
                case "7": // Sort Tasks by Title
                    taskManager.SortTasks((t1, t2) => string.Compare(t1.Title, t2.Title, StringComparison.OrdinalIgnoreCase));
                    // The lambda (t1, t2) => ... defines the comparison logic directly.
                    // string.Compare returns <0 if t1<t2, 0 if equal, >0 if t1>t2.
                    // StringComparison.OrdinalIgnoreCase makes the sort case-insensitive.
                    break;

                // YOUR TASK: Implement Case "8" for Sorting by ID
                case "8": // Sort Tasks by ID
                    // YOUR CODE HERE: Call taskManager.SortTasks() with a lambda expression
                    // that compares two tasks (t1, t2) based on their Id property.
                    // Hint: t1.Id.CompareTo(t2.Id) is a simple way to compare integers.
                    break;

                // YOUR TASK: Implement Case "9" for Viewing Completed Tasks
                case "9": // View Completed Tasks
                    // YOUR CODE HERE: Call taskManager.ViewFilteredTasks() with a lambda expression
                    // that checks if a task's IsCompleted property is true.
                    // Provide a suitable description string like "Completed Tasks".
                    // Example lambda: task => task.IsCompleted
                    break;

                // YOUR TASK: Implement Case "10" for Viewing Pending Tasks
                case "10": // View Pending Tasks
                    // YOUR CODE HERE: Call taskManager.ViewFilteredTasks() with a lambda expression
                    // that checks if a task's IsCompleted property is false.
                    // Provide a suitable description string like "Pending Tasks".
                    break;

                case "11": // Exit (adjust number as needed)
                    keepRunning = false;
                    Console.WriteLine("Thank you for using the Task Tracker. Goodbye!");
                    break;
                default:
                    Console.WriteLine("Invalid option, please try again.");
                    break;
            }

6.4 Understanding Delegates and Lambdas Here

  • TaskComparisonDelegate: Defines the shape of a method that can compare two tasks.
  • _tasks.Sort(new Comparison<Task>(comparisonLogic)): The List<T>.Sort() method can accept a Comparison<T> delegate. Our TaskComparisonDelegate is compatible.
  • Lambda for Sorting by Title: (t1, t2) => string.Compare(t1.Title, t2.Title, StringComparison.OrdinalIgnoreCase)
    • This is a lambda expression. (t1, t2) are the input parameters (matching our delegate).
    • => is the lambda operator.
    • string.Compare(...) is the body of the anonymous method, providing the comparison logic.
  • Func<Task, bool> filterPredicate: Func<T, TResult> is a built-in delegate type in C#. Func<Task, bool> represents a method that takes a Task object as input and returns a bool (true if the task matches the filter, false otherwise).
  • Lambda for Filtering: task => task.IsCompleted
    • This lambda takes one Task object (implicitly named task) and returns true if task.IsCompleted is true, and false otherwise.

6.5 Your Tasks: Implement Sorting by ID and Filtering

  1. In TaskManager.cs - ViewFilteredTasks Method:
    • Implement the filtering logic as described in the comments, using either LINQ’s Where() method (preferred if you’re comfortable, remember using System.Linq;) or a manual foreach loop with an if condition using the filterPredicate.
  2. In Program.cs - Menu Option ā€œ8ā€ (Sort by ID):
    • Call taskManager.SortTasks() and provide a lambda expression that compares two tasks based on their Id property.
  3. In Program.cs - Menu Option ā€œ9ā€ (View Completed Tasks):
    • Call taskManager.ViewFilteredTasks() with a lambda expression that returns true if task.IsCompleted is true. Pass an appropriate description string.
  4. In Program.cs - Menu Option ā€œ10ā€ (View Pending Tasks):
    • Call taskManager.ViewFilteredTasks() with a lambda expression that returns true if task.IsCompleted is false. Pass an appropriate description string.

Test your application thoroughly. You should be able to sort tasks by title and ID, and filter to see only completed or pending tasks.


Part 7: Putting It All Together & Further Challenges

Concept Focus: Review of all integrated concepts, project structure, and encouraging independent further development.

Congratulations! You’ve built a functional Miniature Task Tracker application from scratch, applying a wide range of C# concepts along the way.

7.1 Review of Concepts Integrated

Let’s quickly recap what we’ve built and the concepts we’ve used:

  • Variables and Data Types: Used throughout for storing task details (ID, title, description, status, priority, due date, tags), user input, and control variables.
  • Control Flow:
    • Sequence: Natural order of execution within methods.
    • Selection (if-else, switch): For menu navigation, input validation, conditional display (e.g., task status).
    • Repetition (while, foreach): For the main application loop, iterating through task lists for display or processing.
  • Classes and Objects (Task, PriorityTask, DeadlineTask, TaskManager):
    • Fields: Storing the internal state of tasks and the list of tasks in TaskManager.
    • Constructors: Initializing new task objects with default or provided values.
    • Methods: Defining behaviors like DisplayDetails(), MarkComplete(), AddTag(), SortTasks(), AddTaskFromUserInput().
    • Encapsulation: Using private fields and public properties (with validation) in the Task class to control access to data.
  • Inheritance (PriorityTask : Task, DeadlineTask : Task):
    • Creating specialized task types that reuse common functionality from the base Task class and add their own specific features.
  • Polymorphism (virtual and override with DisplayDetails()):
    • Allowing different task types to be treated uniformly (e.g., stored in List<Task>) while still exhibiting their specific display behavior.
  • Interfaces (ITaggable):
    • Defining a contract for tagging functionality, implemented by the Task class, allowing any task to be taggable.
  • Delegates (TaskComparisonDelegate):
    • Defining a signature for comparison methods, enabling flexible sorting logic to be passed to the SortTasks method.
  • Lambda Expressions:
    • Providing concise, inline implementations for delegates, especially for sorting comparisons and filtering predicates.
  • Collections (List<Task>):
    • Storing and managing a dynamic collection of task objects.

7.2 High-Level Code Structure

Your project likely has the following main components:

  1. Task.cs: Defines the base Task class, including its fields, properties, constructor, and methods. Implements ITaggable.
  2. PriorityTask.cs: Defines the PriorityTask class, inheriting from Task and adding priority features.
  3. DeadlineTask.cs: Defines the DeadlineTask class (created by you), inheriting from Task and adding due date features.
  4. ITaggable.cs: Defines the ITaggable interface.
  5. TaskManager.cs: Defines the TaskManager class, responsible for holding the list of tasks and providing methods to add, view, sort, filter, and manage tasks. Contains the TaskComparisonDelegate.
  6. Program.cs: Contains the Main method, which is the entry point of the application. It handles the user interface (menu loop) and interacts with the TaskManager.

7.3 Further Challenges (Expand Your Learning!)

This project provides a solid foundation. Here are some ideas for how you can extend it further, practicing the concepts you’ve learned and exploring new ones:

  1. Edit Task Details: Implement functionality to edit the title, description, priority, or due date of an existing task.
  2. Remove Task: Add a feature to remove a task from the list (you’ll need to find it first, perhaps by ID).
  3. Persistent Storage: Currently, tasks are lost when the application closes. Research how to save the task list to a file (e.g., CSV, JSON, or XML) and load it back when the application starts. (This might involve learning about file I/O and possibly serialization).
  4. More Sophisticated Filtering: Allow filtering by keywords in the title/description, or by a date range for DeadlineTasks.
  5. Sub-Tasks: Modify the Task class to potentially hold a list of sub-tasks (which would also be Task objects). This introduces a hierarchical structure.
  6. User Accounts: (Very advanced) Introduce a concept of users, where tasks belong to specific users.
  7. Error Handling: Improve input validation and add more robust error handling using try-catch blocks for things like invalid input formats.
  8. Refactor TaskManager: As TaskManager grows, consider if some of its responsibilities could be moved to other classes or if helper classes could be introduced.
  9. Unit Testing: (Advanced) Learn about unit testing frameworks (like MSTest, NUnit, xUnit) and write tests for your classes, especially Task and TaskManager, to ensure they behave as expected.

This culminating project has given you a taste of building a complete application. The key is to keep practicing, experimenting, and building. The more you apply these C# concepts, the more comfortable and proficient you’ll become.

Well done on completing this journey through C# fundamentals and beyond! Happy coding!


šŸ’¬ Got questions? Leave a comment below.

šŸŽ“ Want more challenges? Try adding persistence, user roles, or task reminders.

šŸ“‚ Need a reference? Download the complete solution code here: MiniTaskTracker_Solution

Happy coding! šŸ±ā€šŸ’»

This post is licensed under CC BY 4.0 by the author.