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:
- a simple console application
- 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:
TemperatureMeasuredshould be raised every time.TemperatureTooHighshould only be raised if the temperature is greater than or equal to30.
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 °CTasks
- Create a new console project.
- Create the
TemperatureSensorclass. - Add the two events.
- Implement the
Measuremethod. - In
Program.cs, create a sensor object. - Subscribe to both events using lambdas.
- Call
Measureseveral times with different temperatures. - 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 EventCounterDemoor, in newer .NET versions:
dotnet new blazor -n EventCounterDemoIf your template asks for render mode, choose an interactive server mode.
Step 2: Create the shared service
Create a file:
Services/CounterService.csAdd 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:
Countstores the shared value.CounterChangedis 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.razoror, depending on the project structure:
Pages/SharedCounter.razorAdd 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
- Start the application.
- Open the page:
/shared-counter- Open the same page in a second browser window.
- Click
Incrementin the first browser window. - The counter in the second browser window should update automatically.
- Click
Resetin the second browser window. - 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:10Display 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: AnnaHint:
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
- Which object is the publisher in this exercise?
- Which objects are the subscribers?
- Why is the service registered as
Singleton? - What would happen if each browser window had its own separate service instance?
- Why should the component unsubscribe in
Dispose? - What are advantages and disadvantages of this event-based solution?