Previously, you built the /patients page: a seeded
list of patients rendered with
@foreach, a friendly empty state, and live search.
Our patient cards look great, but they're mute. Click one and… nothing. The card has no way to tell the page "hey, the user picked me." Today we fix that, and along the way you learn the single most important pattern in component-based UI. Ready?
The rule of the river
Think of your component tree as a river system. Data flows down: a parent hands values to its children through parameters, the way a river feeds its streams. And news flows up: when something happens inside a child, it raises an event, like a message in a bottle floating back upstream. Children never reach into their parent and change things directly — they announce, and the parent decides what to do.
You already know half of this river. PatientCard has taken parameters since
Part 3 — that's the downstream half. Today we
build the upstream half: an EventCallback.
One quick housekeeping move
In Part 5, the Patient class lived inside Patients.razor. Now that
PatientCard needs to know what a Patient is too, cut the class out
of the page's @code block and paste it into its own file,
Patient.cs, at the root of ClinicApp (same folder as
Program.cs). Same three properties — Id, Name,
Age — just in a spot every component can see.
Teaching PatientCard to speak
We'll upgrade the card in two ways. First, instead of separate Name and
Age parameters, it now takes the whole Patient object — one
parameter, everything included. Second, it gets an OnSelected callback and a
button that fires it. Here's the new PatientCard.razor:
<div class="patient-card">
<h3>@Patient.Name</h3>
<p>Age: @Patient.Age</p>
<button @onclick="SelectMe">Select</button>
</div>
@code {
[Parameter]
public Patient Patient { get; set; } = new();
[Parameter]
public EventCallback<Patient> OnSelected { get; set; }
private Task SelectMe() => OnSelected.InvokeAsync(Patient);
}
Read the upstream path carefully: the button click runs SelectMe, which calls
OnSelected.InvokeAsync(Patient). Translation: "whoever is listening — the user
selected this patient." The card doesn't know who's listening or what they'll do
with the news. It just announces. That ignorance is a feature: it keeps the card reusable
anywhere.
The parent listens
Now the page's turn. In Patients.razor, pass each patient down and wire a
handler to OnSelected — then show a little banner when someone's been picked:
@if (selected is not null)
{
<p><strong>Now seeing:</strong> @selected.Name (age @selected.Age)</p>
}
@foreach (var patient in FilteredPatients)
{
<PatientCard Patient="patient" OnSelected="ShowSelected" />
}
@code {
private Patient? selected;
private void ShowSelected(Patient patient)
{
selected = patient;
}
}
Run it and click a card's button. The banner appears with that patient's details. Trace the
river: the page hands patient down into each card; the click floats
the same patient back up through OnSelected; the parent updates its
selected state; Blazor re-renders. It's the Part 4 loop again — event, handler,
state, re-render — just stretched across two components.
Tip: Name your callback parameters OnSomething —
OnSelected, OnDeleted, OnSaved. Anyone reading
<PatientCard OnSelected="ShowSelected" /> instantly knows which way
the river flows.
Why EventCallback and not a plain Action?
C# veterans might wonder: couldn't the parameter just be an
Action<Patient>? It could compile, but you'd lose the magic. In current
Blazor (.NET 10), EventCallback is wired into the render loop: after the
parent's handler runs, Blazor automatically re-renders so the UI reflects the new state —
that's why our banner appeared without any extra code. It also handles async handlers
gracefully and doesn't explode if the parent never assigns it. Plain Action
gives you none of that. Rule of thumb: for component parameters, it's
EventCallback every time.
A taste of CascadingValue
Parameters are perfect for parent-to-child handoffs. But what about data that everyone needs — the clinic's name, a color theme, the signed-in user? Passing it down through five layers of parameters gets old fast. For that, Blazor offers CascadingValue: wrap a section of your app, and any descendant can opt in.
<CascadingValue Value="@clinicName">
<PatientCard Patient="patient" OnSelected="ShowSelected" />
</CascadingValue>
@code {
private string clinicName = "Sunrise Clinic";
}
Any component inside the wrapper — however deep — can receive it like this:
@code {
[CascadingParameter]
public string? ClinicName { get; set; }
}
Note: Consider this intermediate seasoning — file it away rather than reaching for it daily. Regular parameters and callbacks cover the vast majority of component communication, and overusing cascading values makes it hard to see where data comes from. When "everyone needs this" is genuinely true, though, it's the right tool.
Next up: forms done properly
Our patients can now be listed, searched, and selected — but they're all hardcoded. A real clinic checks in new patients, and that means forms: inputs, submit buttons, and validation that politely catches mistakes. That's Part 7, and it's where ClinicApp starts feeling like a real app. See you there!