Kihagyás

2nd Homework - language tools

Introduction

This independent assignment builds on the content presented in the 2nd Lab - Language Tools lab session.

Based on the above, the tasks in this independent exercise can be completed with the guidance provided after the task descriptions.

Goals of the independent exercise:

  • Practicing the use of properties
  • Applying delegates and events
  • Practicing the use of .NET attributes
  • Utilizing basic collection types
  • Practicing lambda expressions

A description of the necessary development environment can be found here.

Usage of C# 12 (and newer) language features

When solving the homework, C# 12 and newer language features (e.g., primary constructor) must not be used, as the verification system running on GitHub does not yet support them.

Submission Process & Pre-evaluation

The submission process is the same as for the first homework (detailed instructions can be found at the usual location, see Homework Workflow and Using Git/GitHub):

  1. Use GitHub Classroom to create a repository for yourself. The invitation URL can be found in the notification message or post (see Teams or Neptun). It is important to use the correct invitation URL corresponding to this homework assignment (each homework has a different URL).
  2. Clone the newly created repository. This will contain the expected structure of the solution.
  3. After completing the tasks, commit and push your solution.

The pre-evaluation process also works as usual. Detailed instructions: Pre-evaluation and official grading of the homework.

Task 1 – The Phantom Menace

Task

As is widely known, Jedi knights derive their power from small life forms living in their cells, called midi-chlorians. The highest midi-chlorian level ever recorded (above 20,000) was measured in Anakin Skywalker.

Create a class named Jedi, which has a string type property Name and an int type property MidiChlorianCount. For the latter, ensure that the MidiChlorianCount value cannot be set to 35 or lower. If someone attempts this, the class must throw an exception. For validation, choose the simplest and cleanest solution: use a simple if statement in the property setter and throw an exception. The if statement should not have an else branch, and there is no need to use return.

Solution

The task can be solved in a manner similar to Lab 2, Task 1. In the MidiChlorianCount property setter, throw an exception if an invalid value is provided. This can be done using the following statement:

throw new ArgumentException("You are not a true Jedi!");

Task 2 – Attack of the Clones

Task

Extend the class created in Task 1 with attributes so that when a Jedi object is serialized into an XML file using the XmlSerializer class, its properties appear as XML attributes with names JediName and JediMidiChlorianCount respectively. Then, write a function that serializes an instance of the Jedi class into a text file and then reads it back into a new object (effectively cloning the original object).

XML Serializer Attributes

Place the XML serialization attributes above the properties, not the member variables!

The Jedi Class Must Be Public

The XML serializer can only process public classes, so ensure that the Jedi class is public:

public class Jedi { ...}

Important

Write the save and load demonstration code in a dedicated function, and annotate it with the [Description("Task2")] C# attribute (this should be placed in the line before the function declaration). The saved/loaded object should be implemented as a local variable within this function. The class/function name can be anything (e.g., it can be placed inside the Program class). The function should contain only the relevant code and should not include unrelated subtasks. Call this function from the Main function of the Program class. To use the above attribute, you need to include the System.ComponentModel namespace.

Key requirements:

  • The attribute should be placed above a function, NOT a class.
  • The attribute should be applied to the function that performs testing, not the one implementing the logic.
  • The attribute should only appear above one function.

Solution

The solution follows the approach of Lab 2, Task 4. We provide the following guidance:

  • After serialization, the XML file should look like this:

    <?xml version="1.0"?>
    <Jedi xmlns:xsi="..." JediName="Obi-Wan" JediMidiChlorianCount="15000" />
    

    It is essential that each Jedi appears as a Jedi XML element, their name as JediName, and their midi-chlorian count as JediMidiChlorianCount XML attributes.

  • Since we did not examine example code for deserializing objects during the lab, we provide one here:

    var serializer = new XmlSerializer(typeof(Jedi));
    var stream = new FileStream("jedi.txt", FileMode.Open);
    var clone = (Jedi)serializer.Deserialize(stream);
    stream.Close();
    

    The above code first creates a serializer (serializer), which is later used for reading the data. The reading operation is performed from a file named jedi.txt, which is opened in the second line for reading (note that if we intended to write to it, we would have needed to specify FileMode.Create).

Task 3 – Revenge of the Sith

Task

The Jedi Council has been experiencing high turnover recently. To track changes more easily, create a class that keeps a record of council members and sends text notifications about any changes via an event! The list should be modified with two functions. The Add function should register a new Jedi knight in the council, while the Remove function should remove the last added council member. A separate notification should indicate when the council becomes empty (use the same event as for other changes, but with a different message).

The council members (members) should be stored in a List<Jedi> member variable. The Add function should append new elements to this list, while the Remove function should always remove the last added member using the generic list's RemoveAt method (the index of the last element can be determined from the list length, which is provided by the Count property).

Notifications should be handled via a C# event. The event delegate type should take a simple string as a parameter. Adding a new member, removing individual members, and removing the last member should each trigger a different message. Fire the event directly within the Add and Remove methods (do not introduce a helper function for this).

Do not use a built-in delegate type for the event; instead, define a custom one.

Important

The code for creating and testing the Jedi Council object (subscribing to its C# event, calling Add and Remove) should be placed in a single dedicated function, which should be annotated with the [Description("Task3")] C# attribute. The class/function name can be anything. The function should contain only code relevant to this task and should not include other subtasks. Call this function from the Main function of the Program class.

Key requirements:

  • The attribute should be placed above a function, NOT a class.
  • The attribute should be applied to the function that performs testing, not the one implementing the logic.
  • The attribute should only appear above a single function.

Solution

The solution builds on several parts of Lab 2. The introduction of the new event should follow the approach described in Tasks 2 and 3, while council members should be stored in a list.

Try solving the task independently based on the above information. Once finished, compare your solution with the reference solution provided in the expandable section below! Adjust your solution as needed.

Public Visibility

The example assumes that the involved classes, properties, and delegates have public visibility. If you encounter strange compilation errors or if XmlSerializer throws a runtime error, first check that all relevant elements have the appropriate public visibility settings.

Reference Solution

The steps for the reference solution are as follows:

  1. Create a new class named JediCouncil.
  2. Add a List<Jedi> type field and initialize it with an empty list.
  3. Implement the Add and Remove functions.

    After completing these steps, the following code is obtained:

    public class JediCouncil
    {
        List<Jedi> members = new List<Jedi>();
    
        public void Add(Jedi newJedi)
        {
            members.Add(newJedi);
        }
    
        public void Remove()
        {
            // Removes the last member of the list
            members.RemoveAt(members.Count - 1);
        }
    }
    

    Next, implement event handling.

  4. Define a new delegate type (outside the class, as it is a type) that will pass notification messages:

    public delegate void CouncilChangedDelegate(string message);
    
  5. Extend the JediCouncil class with the event handler:

    public class JediCouncil
    {
        public event CouncilChangedDelegate CouncilChanged;
    
        // ...
    }
    
  6. Trigger the event when a new council member is added. To do this, modify the Add method.

    public void Add(Jedi newJedi)
    {
        members.Add(newJedi);
    
        // TODO: Trigger the event here.
        // Ensure it is only triggered if at least one subscriber is present.
        // Use the modern ?.Invoke approach instead of verbose null checking.
    }
    
  7. Trigger the event when a council member leaves! Differentiate the case when the council becomes completely empty. To do this, modify the Remove method.

    public void Remove()
    {
        // Removes the last member of the list
        members.RemoveAt(members.Count - 1);
    
        // TODO: Trigger the event here.
        // Ensure it is only triggered if at least one subscriber is present.
    }
    
  8. To test our solution, add a MessageReceived function in the class where you want to test event subscription and handling (e.g., the Program class). This function will subscribe to JediCouncil notifications.

    Program.cs
    private static void MessageReceived(string message)
    {
        Console.WriteLine(message);
    }
    
  9. Finally, test our new class by writing a dedicated function (this can be in the Program class). Add the [Description("Task3")] attribute above the function! The function template:

    // Create the council
    var council = new JediCouncil();
    
    // TODO: Subscribe to the CouncilChanged event of the council
    
    // TODO: Add two Jedi objects to the council using the Add method
    
    council.Remove();
    council.Remove();
    
  10. If everything is implemented correctly, the following output should appear when the program is executed:

    A new member has joined
    A new member has joined
    I sense a disturbance in the Force
    The council has fallen!
    

Null Checking for Events

If you used explicit null checks in JediCouncil.Add to determine whether there is at least one subscriber, update your approach to the more modern ?.Invoke method, which performs the same check in a more concise manner without explicit null validation. This change is required for JediCouncil.Add, but for JediCouncil.Remove, both approaches are acceptable.

Task 4 – Delegates

Task

Extend the JediCouncil class with a parameterless function (the function name must end with _Delegate, this is mandatory) that returns all members of the Jedi Council whose midi-chlorian count is below 530!

  • Use a function, not a property, for the query.
  • Within the function, use the FindAll() method of the List<Jedi> class to filter members.
  • In this task, you are NOT allowed to use lambda expressions!

Write a dedicated "test" function (e.g., in the Program class) that calls the above function and prints the names of the returned Jedi knights! This function should contain only the relevant test code and should not include unrelated subtasks.

Important

Annotate this "test" function with [Description("Task4")] in C#. Call this function from the Main function of the Program class.

Key requirements:

  • The attribute should be placed above a function, NOT a class.
  • The attribute should be applied to the function that performs testing, not the one implementing the logic.
  • The attribute should only appear above a single function.

Extract Initialization

Introduce a separate static method (e.g., in the Program class) that takes a JediCouncil object as a parameter and adds at least three initialized Jedi objects using the Add method. The goal is to have an initialization method that can be reused in later tasks, avoiding redundant initialization code.

Solution

The solution follows the approach of Lab 2, Task 6. Here are some guidelines:

  • The function may return multiple results, so its return type should be List<Jedi>.
  • The FindAll method expects a filtering function with the signature bool FunctionName(Jedi j).

Task 5 – Lambda Expressions

This task is similar to the previous one, but now we will use lambda expressions. This topic was covered in both lectures and labs (Lab 2, Task 6).

Extend the JediCouncil class with a parameterless function (the function name must end with _Lambda, this is mandatory) that returns all members of the Jedi Council whose midi-chlorian count is below 1000!

  • Use a function, not a property, for the query.
  • Within the function, use the FindAll() method of the List<Jedi> class to filter members.
  • You must use a lambda expression (either a statement or an expression lambda)!

Write a dedicated "test" function (e.g., in the Program class) that calls the above function and prints the names of the returned Jedi knights!
This function should contain only the relevant test code and should not include unrelated subtasks.

Important

Annotate this "test" function with [Description("Task5")] in C#. Call this function from the Main function of the Program class.

Key requirements:

  • The attribute should be placed above a function, NOT a class.
  • The attribute should be applied to the function that performs testing, not the one implementing the logic.
  • The attribute should only appear above a single function.

Task 6 – Using Action/Func

Follow the detailed instructions. This topic was not covered in the lab (due to time constraints), but it is an essential topic in the course.

Add Person and ReportPrinter classes to the project (each in a separate file named after the class, within the default ModernLangToolsApp namespace) with the following content:

Person and ReportPrinter Classes
class Person
{
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public string Name { get; set; }
    public int Age { get; set; }
}
class ReportPrinter
{
    private readonly IEnumerable<Person> people;
    private readonly Action headerPrinter;

    public ReportPrinter(IEnumerable<Person> people, Action headerPrinter)
    {
        this.people = people;
        this.headerPrinter = headerPrinter;
    }

    public void PrintReport()
    {
        headerPrinter();
        Console.WriteLine("-----------------------------------------");
        int i = 0;
        foreach (var person in people)
        {
            Console.Write($"{++i}. ");
            Console.WriteLine("Person");
        }
        Console.WriteLine("--------------- Summary -----------------");
        Console.WriteLine("Footer");
    }
}

This ReportPrinter class is used to generate a formatted report in the console for the persons provided in its constructor, structured in a header/data/footer format.
Add the following function to the Program.cs file to test the ReportPrinter, and call this function from the Main function:

Testing ReportPrinter
[Description("Task6")]
static void test6()
{
    var employees = new Person[] { new Person("Joe", 20), new Person("Jill", 30) };

    ReportPrinter reportPrinter = new ReportPrinter(
        employees,
        () => Console.WriteLine("Employees")
        );

    reportPrinter.PrintReport();
}

Run the application. The following output will appear in the console:

Employees
-----------------------------------------
1. Person
2. Person
--------------- Summary -----------------
Footer
The first row above the "----" represents the header. Below, each person is displayed with a hardcoded "Person" text, followed by the footer section below the "----", which currently only contains a hardcoded "Footer" text.

In the solution, you can see that the header text is not hardcoded inside the ReportPrinter class. Instead, the ReportPrinter user provides it through the constructor as a delegate, in this case, a lambda expression. The delegate type used is the built-in .NET Action type.

Tasks are the following:

Warning

You MUST NOT use a custom delegate type in this solution. Only the built-in .NET delegate types are allowed; otherwise, the solution will not be accepted.

  1. Modify the ReportPrinter class so that the user can specify not only the header but also the footer as a delegate in the constructor.

  2. Further modify the ReportPrinter class so that instead of displaying the fixed "Person" text for each individual, the user of ReportPrinter can specify how each person's data is displayed in the console using a delegate provided in the constructor. It is important that the serial number at the beginning of each row must always be displayed and cannot be modified by the ReportPrinter user. This means that ReportPrinter itself must handle this output.

    Solution Approach

    Use a similar approach as for the header and footer, but ensure that the ReportPrinter user receives the person object so they can format and display the data appropriately.

  3. Modify the ReportPrinter usage in Program.cs (using appropriate lambda expressions) so that the console output looks like this:

    Employees
    -----------------------------------------
    1. Name: Joe (Age: 20)
    2. Name: Jill (Age: 30)
    --------------- Summary -----------------
    Number of Employees: 2
    

    Displaying the Employee Count in the Footer

    To properly display the number of employees in the footer, you will need to understand "variable capturing" (covered in Lecture 3 under "Variable Capturing, Closure").

    Homework Evaluation

    The automatic GitHub checker does NOT verify whether you correctly modified ReportPrinter and its usage for "Task 6". Be sure to thoroughly test your solution so that any issues do not only become apparent during manual homework evaluation after the deadline.
    (Update: Since March 13, 2024, partial automatic verification is available for this task.)

  4. The following task is optional but provides a good opportunity to practice using the built-in Func delegates.
    One major limitation of the ReportPrinter class is that it only allows the output report to be displayed on the console. A more flexible solution would allow generating the report as a string instead, which could then be used as needed (e.g., written to a file).

    The task is as follows: introduce a ReportBuilder class modeled after ReportPrinter, but instead of writing to the console, it should generate a string containing the entire report, which can be retrieved using a new GetResult() method.

    Submission

    If you submit this task, do not include the instantiation/testing code for ReportBuilder in the test6 function. Instead, introduce a test6b function and annotate it with [Description("Task6b")].

    Solution Hints

    • Introduce a StringBuilder field in the class to handle string operations efficiently. This is significantly more efficient than concatenating strings using "+".
    • The ReportBuilder user should not write directly to the console but should provide formatted strings to ReportBuilder using built-in delegate types (in this case, Action is not suitable). Use lambda expressions for testing as well.

Task 7 (Optional task) – Using Built-in Func/Action Generic Delegate Types

Completing this task is not mandatory, but it is highly recommended, as it is core material that may appear on exams. It was covered in lectures but not in lab sessions.

Task

Extend the JediCouncil class.

  • Create a property named Count with an int return type that, when queried, returns the number of Jedi currently in the council. Ensure that this value can only be read (it should not be set).

    Tip

    The JediCouncil class contains a member variable named members, which has a Count property. The solution should build upon this.

  • Create a function named CountIf that also counts the number of council members, but only considers those that meet a specified condition. The function should return an int and take a delegate as a parameter, which defines the condition for counting the Jedi (meaning CountIf must have a parameter).

    Delegate Type

    The delegate type must be one of the built-in generic Action / Func delegate types (custom delegate types and the built-in Predicate type are NOT allowed).

    Because of this, you CANNOT use the built-in FindAll method on the list, as the delegate type we use would not be compatible with the parameter expected by FindAll. Instead, iterate through the members using a foreach loop!

  • Demonstrate the functionality of both the property and the function in a dedicated testing function, which should be annotated with [Description("Task7")]. This function should contain only relevant test code and should not include unrelated subtasks. For filling the JediCouncil, call the helper function introduced in the previous task. Call this function from the Main function of the Program class.

    Important

    The [Description("Task7")] attribute should be placed above ONLY ONE function.

Solution

  • The Count property should have only a get accessor, without a set accessor, making it a read-only property.
  • The implementation of the CountIf function is similar to Task 4. The difference is that CountIf does not return the matching council members but only their count.
    • The CountIf function should take a filtering function as a parameter, with the signature bool FunctionName(Jedi jedi).

Submission

Checklist:

Checklist for submitting homework

  • Enter your Neptun code in the neptun.txt file located in the root directory of the repository. The code should be in uppercase letters only, and the file should contain only these six characters—nothing else.
  • Work within the solution/projects downloaded from GitHub, not in a newly created project.
  • If you are not yet experienced in using Visual Studio's Git features, after pushing your changes (or at the latest when you consider your homework submitted), it is advisable to check the repository on GitHub's web interface to ensure that all changes have been successfully uploaded.
  • After pushing, check on GitHub to see if the GitHub Action-based pre-check has run without errors.
  • Assignments will only be accepted if they are fully completed and meet all the requirements. Do not expect acceptance of non-compiling code or partial solutions.
  • Naturally, you must submit your own work, as it will be evaluated.

2025-03-07 Szerzők