Previously, in Part 6, we gave
PatientCard.razor a Patient parameter and an
OnSelected event so a card could talk back to its page. Today ClinicApp grows up
a little: instead of hard-coding patients into a list, you will add them through a real form —
one that politely refuses bad data.
Why the hard-coded patients have to go
Since Part 5, our Patients.razor page (the one answering at
/patients) has carried a List<Patient> that we typed by hand.
That was perfect for learning, but think about a real clinic for a second. Does the
receptionist recompile the app every time a new patient walks in? Of course not. They fill in
a form.
Blazor gives us a small team of built-in components for exactly this job:
EditForm wraps the form, a validator watches the data, and
validation messages report problems to the user. We'll meet each one in a
moment. First, though, our Patient model needs to learn some rules.
Step 1: give Patient some rules
How does Blazor know that a patient without a name is a mistake, but an age of 45 is fine?
You tell it — right on the model, using data annotations. These are little
attributes you attach to properties, like sticky notes that say "this field must follow this
rule." Open your Patient class and add two of them:
using System.ComponentModel.DataAnnotations;
public class Patient
{
public int Id { get; set; }
[Required(ErrorMessage = "Every patient needs a name.")]
public string Name { get; set; } = "";
[Range(0, 120, ErrorMessage = "Age must be between 0 and 120.")]
public int Age { get; set; }
}
[Required] says Name cannot be left empty, and
[Range(0, 120)] keeps Age inside believable human limits. Both
attributes live in the System.ComponentModel.DataAnnotations namespace — that's
what the using line at the top is for. (If you ever need these rules inside a
.razor file instead, the same line is written
@using System.ComponentModel.DataAnnotations.)
Notice the custom ErrorMessage on each rule. You could leave it out and get a
default message, but writing your own keeps the app friendly — and you'll see exactly where
these messages show up next.
Step 2: build the form on Patients.razor
Now for the form itself. Add this above your patient list in Patients.razor:
@page "/patients"
@rendermode InteractiveServer
<EditForm Model="newPatient" OnValidSubmit="AddPatient">
<DataAnnotationsValidator />
<label>
Name:
<InputText @bind-Value="newPatient.Name" />
</label>
<ValidationMessage For="() => newPatient.Name" />
<label>
Age:
<InputNumber @bind-Value="newPatient.Age" />
</label>
<ValidationMessage For="() => newPatient.Age" />
<button type="submit">Add patient</button>
</EditForm>
(That @rendermode InteractiveServer line under @page has been with
us since Part 5 — it keeps the page interactive so clicks and keystrokes actually reach your
code. Part 12 explains what it really does; for now, just make sure it's there.)
The trio: who does what
There's a lot of new markup up there, but it boils down to three characters, each with one job:
EditFormwraps. It replaces the plain HTML<form>tag, and itsModelattribute tells Blazor which object this form is editing — ournewPatient.DataAnnotationsValidatorwatches. It reads the sticky notes we put onPatientand checks the form's values against them. No validator, no validation — forget this line and every submit sails through.ValidationMessagereports. Each one displays the error for a single field, chosen by itsForexpression. Put it next to the input it describes, so the user never has to hunt for what went wrong.
The inputs themselves — InputText and InputNumber — are Blazor's
form-aware cousins of <input>. Their @bind-Value attribute
keeps the box on screen and the property on newPatient in sync, in both
directions, automatically.
Step 3: handle a valid submit
When the user clicks the button and every rule passes, EditForm calls whatever
method you named in OnValidSubmit. Ours is AddPatient, and it lives
in the page's @code block:
@code {
private Patient newPatient = new();
private void AddPatient()
{
newPatient.Id = patients.Count == 0 ? 1 : patients.Max(p => p.Id) + 1;
patients.Add(newPatient);
newPatient = new Patient();
}
}
Three small moves. First, we assign a new Id — one higher than the biggest one
in the list, or 1 if the list is empty. Second, we add the patient to patients,
the same List<Patient> our live search from Part 5 already reads from, so
the new patient appears instantly. Third, we point newPatient at a fresh
Patient object — and because the inputs are bound to it, the form clears itself,
ready for the next entry.
Tip: OnValidSubmit only fires when every rule
passes. Try it: leave the form empty and hit the button. Nothing is added — instead, your
error messages appear right where you placed the ValidationMessage
components. That's the whole trio working together in one click.
Where did these rules come from?
One last thing worth noticing: we didn't invent Name and Age on a
whim. We decided these attributes deliberately, back in the
design series, when we worked out what a
clinic actually needs to store about a patient. Design decisions on paper became C#
properties, and today those properties grew validation rules. That's the thread connecting
both series: good systems are designed first and coded second.
Up next
Our clinic can finally take on new patients without a recompile. But a patient is more than a
name and an age — a real details view needs room for an address, history, and more, and that
deserves its own page with its own URL. How does a URL like /patients/2 find its
way to the right patient? That's routing, and it's exactly where we're headed
in Part 8.