š ļø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:
- Task Class ā Define a basic class with constructors, fields, and a
DisplayDetails()
method. - Task Manager ā Manage a list of tasks using collections and basic menu-based UI.
- Encapsulation ā Refactor fields to private, and add properties and validation logic.
- Inheritance and Polymorphism ā Extend
Task
withPriorityTask
andDeadlineTask
usingoverride
. - Interfaces ā Implement an
ITaggable
interface for adding/removing tags. - Delegates & Lambdas ā Add sorting and filtering features using delegate patterns and lambda expressions.
- 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
, andIsCompleted
are variables that will hold the data for each individual task object. _nextId
(Static Field): Thisstatic
integer is used to automatically assign a uniqueId
to each new task. Because itāsstatic
, its value is shared across all instances of theTask
class. Each time a newTask
is created,_nextId
is used for theId
and then incremented.- Constructor (
public Task(string title, string description)
): This is the blueprint for creating newTask
objects. It takes atitle
anddescription
as input, assigns a uniqueId
, and setsIsCompleted
tofalse
by default. DisplayDetails()
Method: This method will be responsible for printing the information of aTask
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:
- Open
Task.cs
. - Locate the
DisplayDetails()
method. - 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 anif
statement here (refer back to your Control Flow lesson!).
- The taskās
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 aList<Task>
called_tasks
.List<Task>
: This is a generic collection fromSystem.Collections.Generic
. It allows us to store an ordered list ofTask
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.
- We create an instance of
2.4 Your Tasks: Implement TaskManager
Methods and Menu Logic
You have two main areas to complete in this part:
- 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 callDisplayDetails()
for each task.
- Check if there are any tasks in the
- Complete the
- In
Program.cs
:- Inside the
while
loop, implement the logic (using aswitch
statement orif-else if
statements) to handle the userās menuchoice
:- If the choice is ā1ā, call
taskManager.AddTaskFromUserInput()
. - If the choice is ā2ā, call
taskManager.ViewAllTasks()
. - If the choice is ā3ā, set
keepRunning
tofalse
and print a goodbye message. - For any other input, print an error message.
- If the choice is ā1ā, call
- Inside the
Hints:
- Remember to instantiate a
Task
object usingnew 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 nowprivate
. This means they can only be accessed from within theTask
class itself. - Public Properties:
Id
: Has only aget
accessor, making it read-only from outside the class once the object is constructed. Its value is set in the constructor.Title
: Hasget
andset
. Theset
accessor is where youāll add validation.Description
: Simpleget
andset
for now.IsCompleted
: Theset
accessor isprivate
. This means the completion status can only be changed by methods within theTask
class (likeMarkComplete()
and theMarkIncomplete()
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 theTitle
propertyās setter will run even during object creation. MarkComplete()
Method: A public method to change theIsCompleted
state.virtual
Keyword: Added toDisplayDetails()
in preparation for Part 4 (Inheritance).
3.3 Your Tasks: Implement Validation and MarkIncomplete()
- 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
.
- Implement the validation logic as described in the comments. If the provided
- In
Task.cs
-MarkIncomplete()
Method:- Implement this method to set
IsCompleted
tofalse
and print a confirmation message.
- Implement this method to set
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):
- 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 methodprivate Task FindTaskById(int id)
that loops through_tasks
and returns the task if found, ornull
if not). - If the task is found:
- If
task.IsCompleted
is true, calltask.MarkIncomplete()
. - Else, call
task.MarkComplete()
.
- If
- If the task is not found, print an error message.
- Create a new public method, e.g.,
- In
Program.cs
:- Add a new menu option (e.g., ā4. Mark Task as Complete/Incompleteā).
- In your
switch
orif-else if
block, call the newtaskManager.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()}ā);`).
- Update
TaskManager
andProgram.cs
(Reader Challenge):- Modify your
AddTaskFromUserInput()
method inTaskManager.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.
- Modify your
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 namedITaggable
. By convention, interface names often start with āIā.List<string> Tags { get; }
: This declares a read-only property. Any class implementingITaggable
must provide a public getter for aList<string>
namedTags
.void AddTag(string tag);
andvoid 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
: TheTask
class now declares that it implements theITaggable
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 theTags
property fromITaggable
. 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 viaAddTag
/RemoveTag
).AddTag(string tag)
andRemoveTag(string tag)
: These are the concrete implementations of the methods defined inITaggable
.
5.5 Your Tasks: Implement Interface Methods and Update Display
- 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.
- Implement the logic to add a tag: check for null/empty, check for duplicates, add to
- In
Task.cs
-RemoveTag(string tag)
Method:- Implement the logic to remove a tag: attempt removal from
_tags
and print appropriate feedback.
- Implement the logic to remove a tag: attempt removal from
- 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):
- In
Program.cs
:- Add new menu options like ā5. Add Tag to Taskā and ā6. Remove Tag from Taskā.
- In
TaskManager.cs
:- Create new public methods like
public void AddTagToTaskFromUserInput()
andpublic 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()
orRemoveTag()
method. - Ensure you are calling these methods on an object that is known to be
ITaggable
. Since our baseTask
class implements it, anyTask
(or its derived types) can be cast toITaggable
if needed, or you can directly call the methods if they are public onTask
.
- Create new public methods like
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 beforet2
. - 0:
t1
andt2
are considered equal in terms of sort order. - Greater than 0:
t1
comes aftert2
.
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))
: TheList<T>.Sort()
method can accept aComparison<T>
delegate. OurTaskComparisonDelegate
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.
- This is a lambda expression.
Func<Task, bool> filterPredicate
:Func<T, TResult>
is a built-in delegate type in C#.Func<Task, bool>
represents a method that takes aTask
object as input and returns abool
(true if the task matches the filter, false otherwise).- Lambda for Filtering:
task => task.IsCompleted
- This lambda takes one
Task
object (implicitly namedtask
) and returnstrue
iftask.IsCompleted
is true, andfalse
otherwise.
- This lambda takes one
6.5 Your Tasks: Implement Sorting by ID and Filtering
- 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, rememberusing System.Linq;
) or a manualforeach
loop with anif
condition using thefilterPredicate
.
- Implement the filtering logic as described in the comments, using either LINQās
- In
Program.cs
- Menu Option ā8ā (Sort by ID):- Call
taskManager.SortTasks()
and provide a lambda expression that compares two tasks based on theirId
property.
- Call
- In
Program.cs
- Menu Option ā9ā (View Completed Tasks):- Call
taskManager.ViewFilteredTasks()
with a lambda expression that returnstrue
iftask.IsCompleted
istrue
. Pass an appropriate description string.
- Call
- In
Program.cs
- Menu Option ā10ā (View Pending Tasks):- Call
taskManager.ViewFilteredTasks()
with a lambda expression that returnstrue
iftask.IsCompleted
isfalse
. Pass an appropriate description string.
- Call
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 andpublic
properties (with validation) in theTask
class to control access to data.
- Fields: Storing the internal state of tasks and the list of tasks in
- Inheritance (
PriorityTask : Task
,DeadlineTask : Task
):- Creating specialized task types that reuse common functionality from the base
Task
class and add their own specific features.
- Creating specialized task types that reuse common functionality from the base
- Polymorphism (
virtual
andoverride
withDisplayDetails()
):- Allowing different task types to be treated uniformly (e.g., stored in
List<Task>
) while still exhibiting their specific display behavior.
- Allowing different task types to be treated uniformly (e.g., stored in
- Interfaces (
ITaggable
):- Defining a contract for tagging functionality, implemented by the
Task
class, allowing any task to be taggable.
- Defining a contract for tagging functionality, implemented by the
- Delegates (
TaskComparisonDelegate
):- Defining a signature for comparison methods, enabling flexible sorting logic to be passed to the
SortTasks
method.
- Defining a signature for comparison methods, enabling flexible sorting logic to be passed to the
- 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:
Task.cs
: Defines the baseTask
class, including its fields, properties, constructor, and methods. ImplementsITaggable
.PriorityTask.cs
: Defines thePriorityTask
class, inheriting fromTask
and adding priority features.DeadlineTask.cs
: Defines theDeadlineTask
class (created by you), inheriting fromTask
and adding due date features.ITaggable.cs
: Defines theITaggable
interface.TaskManager.cs
: Defines theTaskManager
class, responsible for holding the list of tasks and providing methods to add, view, sort, filter, and manage tasks. Contains theTaskComparisonDelegate
.Program.cs
: Contains theMain
method, which is the entry point of the application. It handles the user interface (menu loop) and interacts with theTaskManager
.
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:
- Edit Task Details: Implement functionality to edit the title, description, priority, or due date of an existing task.
- Remove Task: Add a feature to remove a task from the list (youāll need to find it first, perhaps by ID).
- 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).
- More Sophisticated Filtering: Allow filtering by keywords in the title/description, or by a date range for
DeadlineTask
s. - Sub-Tasks: Modify the
Task
class to potentially hold a list of sub-tasks (which would also beTask
objects). This introduces a hierarchical structure. - User Accounts: (Very advanced) Introduce a concept of users, where tasks belong to specific users.
- Error Handling: Improve input validation and add more robust error handling using
try-catch
blocks for things like invalid input formats. - Refactor
TaskManager
: AsTaskManager
grows, consider if some of its responsibilities could be moved to other classes or if helper classes could be introduced. - Unit Testing: (Advanced) Learn about unit testing frameworks (like MSTest, NUnit, xUnit) and write tests for your classes, especially
Task
andTaskManager
, 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! š±āš»