Exercises: C# Events

These exercises are designed for pupils who already know delegates, lambdas and LINQ method syntax.

The goal is to use events in two different situations:

  1. a simple console application
  2. a Blazor Server application where multiple browser windows react to the same server-side event

Exercise 1: Console App — Temperature Alarm

Goal

Create a console application with a TemperatureSensor class.

The sensor should raise events when:

  • a new temperature is measured
  • the temperature is too high

Requirements

Create a class named TemperatureSensor.

It should contain:

public event Action<double>? TemperatureMeasured;
public event Action<double>? TemperatureTooHigh;

The class should also have a method:

public void Measure(double temperature)

When Measure is called:

  1. TemperatureMeasured should be raised every time.
  2. TemperatureTooHigh should only be raised if the temperature is greater than or equal to 30.

Example behavior

TemperatureSensor sensor = new TemperatureSensor();
 
sensor.TemperatureMeasured += temp =>
{
    Console.WriteLine($"Measured: {temp} °C");
};
 
sensor.TemperatureTooHigh += temp =>
{
    Console.WriteLine($"WARNING: Temperature too high: {temp} °C");
};
 
sensor.Measure(22.5);
sensor.Measure(29.0);
sensor.Measure(31.5);

Expected output:

Measured: 22.5 °C
Measured: 29 °C
Measured: 31.5 °C
WARNING: Temperature too high: 31.5 °C

Tasks

  1. Create a new console project.
  2. Create the TemperatureSensor class.
  3. Add the two events.
  4. Implement the Measure method.
  5. In Program.cs, create a sensor object.
  6. Subscribe to both events using lambdas.
  7. Call Measure several times with different temperatures.
  8. Test that the warning only appears for temperatures greater than or equal to 30.

Optional extensions

Extension A

Count how many measurements were made.

Create an event:

public event Action<int>? MeasurementCountChanged;

Raise it after every measurement.


Extension B

Allow the maximum temperature to be configured:

public double WarningLimit { get; set; } = 30;

Then check against WarningLimit instead of hardcoding 30.


Exercise 2: Blazor Server — Shared Counter With Events

Goal

Create a Blazor Server application with a shared counter service.

When the counter is changed in one browser window, all other connected browser windows should update automatically.

This demonstrates how events can be used for communication between a service and multiple UI components.


Important idea

In Blazor Server, components run on the server.

If a shared service raises an event, multiple connected components can subscribe to it.

When one browser window changes the shared state, the service raises an event.

Other browser windows receive the event and update their UI.


Step 1: Create a Blazor Server project

Create a new Blazor Server project.

Depending on your .NET version, the template name may be different.

For example:

dotnet new blazorserver -n EventCounterDemo

or, in newer .NET versions:

dotnet new blazor -n EventCounterDemo

If your template asks for render mode, choose an interactive server mode.


Step 2: Create the shared service

Create a file:

Services/CounterService.cs

Add this code:

namespace EventCounterDemo.Services;
 
public class CounterService
{
    public event Action? CounterChanged;
 
    public int Count { get; private set; }
 
    public void Increment()
    {
        Count++;
        CounterChanged?.Invoke();
    }
 
    public void Reset()
    {
        Count = 0;
        CounterChanged?.Invoke();
    }
}

Explanation:

  • Count stores the shared value.
  • CounterChanged is raised whenever the value changes.
  • Components can subscribe to CounterChanged.

Step 3: Register the service

Open Program.cs.

Add the service to the dependency injection container.

For this exercise, use Singleton, because all browser windows should share the same counter.

using EventCounterDemo.Services;

Then add:

builder.Services.AddSingleton<CounterService>();

Why Singleton?

A singleton service exists once for the whole server application.

That means all connected browser windows use the same CounterService.


Step 4: Create a Blazor component

Create a page or component for the counter.

For example, create:

Components/Pages/SharedCounter.razor

or, depending on the project structure:

Pages/SharedCounter.razor

Add this code:

@page "/shared-counter"
@implements IDisposable
@inject EventCounterDemo.Services.CounterService CounterService
 
<h3>Shared Counter</h3>
 
<p>
    Current count:
    <strong>@CounterService.Count</strong>
</p>
 
<button class="btn btn-primary" @onclick="Increment">
    Increment
</button>
 
<button class="btn btn-secondary" @onclick="Reset">
    Reset
</button>
 
<p class="mt-3">
    Open this page in two browser windows. Click a button in one window.
    The other window should update automatically.
</p>
 
@code {
    protected override void OnInitialized()
    {
        CounterService.CounterChanged += OnCounterChanged;
    }
 
    private void Increment()
    {
        CounterService.Increment();
    }
 
    private void Reset()
    {
        CounterService.Reset();
    }
 
    private void OnCounterChanged()
    {
        InvokeAsync(StateHasChanged);
    }
 
    public void Dispose()
    {
        CounterService.CounterChanged -= OnCounterChanged;
    }
}

Step 5: Test the application

  1. Start the application.
  2. Open the page:
/shared-counter
  1. Open the same page in a second browser window.
  2. Click Increment in the first browser window.
  3. The counter in the second browser window should update automatically.
  4. Click Reset in the second browser window.
  5. The first browser window should also update automatically.

Why is InvokeAsync(StateHasChanged) used?

The event is raised from a service, not directly from the component’s normal UI event handling.

Blazor UI updates should happen on the correct renderer context.

Therefore we use:

InvokeAsync(StateHasChanged);

This tells Blazor:

Please re-render this component safely.


Why do we implement IDisposable?

The component subscribes to the service event:

CounterService.CounterChanged += OnCounterChanged;

When the component is destroyed, it should unsubscribe:

CounterService.CounterChanged -= OnCounterChanged;

Otherwise, the service may still keep a reference to the old component.

This can cause memory leaks or unexpected behavior.


Optional extensions

Extension A: Show a history

Store the last 10 counter changes in the service.

For example:

public List<string> History { get; } = new();

Each time the counter changes, add a text like:

Counter changed to 5 at 14:32:10

Display the history in the component.


Extension B: Add a user name

Add an input field where each browser window can enter a name.

When a user increments the counter, store who changed it last.

Example display:

Last changed by: Anna

Hint:

The service method could look like this:

public void Increment(string userName)

Extension C: Use event data

Instead of:

public event Action? CounterChanged;

use:

public event Action<int>? CounterChanged;

Then pass the new count:

CounterChanged?.Invoke(Count);

The component can subscribe like this:

CounterService.CounterChanged += OnCounterChanged;
 
private void OnCounterChanged(int newCount)
{
    InvokeAsync(StateHasChanged);
}

Questions for reflection

  1. Which object is the publisher in this exercise?
  2. Which objects are the subscribers?
  3. Why is the service registered as Singleton?
  4. What would happen if each browser window had its own separate service instance?
  5. Why should the component unsubscribe in Dispose?
  6. What are advantages and disadvantages of this event-based solution?