Previously, you made Home.razor interactive with a
waiting-room counter and met the three
flavors of binding: show a value, react to an event, bind both ways.
A counter is fun, but a clinic doesn't run on a number — it runs on people. Today we
give ClinicApp its first real data: a Patient model, a list of
patients, and a brand-new page that renders them, handles the empty case gracefully, and
filters live as you type. Ready?
Meet the Patient model
If you followed our Conversation to System Design series, this moment should feel like a reunion: Patient was one of the very first entities we discovered by talking to the doctor. Now we get to write it as actual C#. A model is just a class that describes the shape of your data — what a patient is, as far as our app cares:
public class Patient
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
Three properties: an Id to tell patients apart (two patients
can share a name — the database design series taught us that!), a Name, and an
Age. That's it. Models are usually this humble.
A new page: Patients.razor
Create a new file called Patients.razor next to Home.razor. Give it
a route of /patients and seed it with a few friendly faces. For now, both the
model and the data live right inside the page — simple and honest:
@page "/patients"
@rendermode InteractiveServer
<h2>Patients</h2>
@foreach (var patient in patients)
{
<PatientCard Name="@patient.Name" Age="@patient.Age" />
}
@code {
public class Patient
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
private List<Patient> patients = new()
{
new Patient { Id = 1, Name = "Asha", Age = 34 },
new Patient { Id = 2, Name = "Ben", Age = 41 },
new Patient { Id = 3, Name = "Carla", Age = 29 },
new Patient { Id = 4, Name = "Dev", Age = 52 },
new Patient { Id = 5, Name = "Elena", Age = 8 },
};
}
The star here is @foreach. It's the same C# loop you already know, dropped
straight into your markup: for each patient in the list, render one
PatientCard — the very component you built in
Part 3. Five patients in the list means five
cards on the page. Add a sixth patient, and a sixth card appears. The markup is a
template; the data decides how many copies you get.
(You've seen @rendermode InteractiveServer before — in current Blazor
(.NET 10) it tells the page to respond to events. Full story in
Part 12.)
When the list is empty
What should the page show when there are no patients? A blank screen feels broken. This is
where @if comes in — plain C# conditionals, right in the markup:
@if (patients.Count == 0)
{
<p>The waiting room is quiet 🎉</p>
}
else
{
@foreach (var patient in patients)
{
<PatientCard Name="@patient.Name" Age="@patient.Age" />
}
}
Every list in every app you'll ever build deserves an empty state like this. It's a small kindness to your users — and to future you, staring at a page wondering whether it's empty or broken.
Live search: binding meets LINQ
Now for the payoff. Remember the tip from Part 4 — @bind:event="oninput"
updates a bound field on every keystroke? Combine that with LINQ's Where method
and you get live search in about three lines. (New to LINQ? Our
beginner's guide to LINQ has you covered —
Where is just "keep the items that match.")
Here's the complete Patients.razor, all the pieces together:
@page "/patients"
@rendermode InteractiveServer
<h2>Patients</h2>
<input placeholder="Search by name..." @bind="searchText" @bind:event="oninput" />
@if (FilteredPatients.Count == 0)
{
<p>The waiting room is quiet 🎉</p>
}
else
{
@foreach (var patient in FilteredPatients)
{
<PatientCard Name="@patient.Name" Age="@patient.Age" />
}
}
@code {
public class Patient
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
private string searchText = "";
private List<Patient> patients = new()
{
new Patient { Id = 1, Name = "Asha", Age = 34 },
new Patient { Id = 2, Name = "Ben", Age = 41 },
new Patient { Id = 3, Name = "Carla", Age = 29 },
new Patient { Id = 4, Name = "Dev", Age = 52 },
new Patient { Id = 5, Name = "Elena", Age = 8 },
};
private List<Patient> FilteredPatients =>
patients
.Where(p => p.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.ToList();
}
Run it and type "a" into the box. The list shrinks as you type — no search button, no page
reload. Trace the loop from Part 4: each keystroke is an event, @bind
updates the state (searchText), Blazor re-renders, and the
re-render recomputes FilteredPatients. Clear the box and everyone comes back.
Type "zzz" and — there's your empty state, earning its keep.
Tip: FilteredPatients is a property, not a stored list. It
recalculates from patients and searchText every render, so it
can never fall out of date. When state can be derived, derive it — one less
thing to keep in sync.
Note: Keeping the Patient class and its data inside the
page is perfectly fine while we learn. In Part 9 we'll move them into a shared service so
every page sees the same list — for now, the page is their home.
Next up: components that talk back
Our cards can display a patient, but they can't tell us anything — click one and
nothing happens. In Part 6, we'll teach
PatientCard to raise events, so the page can react when you select a patient.
Data down, news up — see you there!