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!