Skip to main content
ANVISoftware Solutions
Lesson 5 of 14Intermediate18 min

Parameters and Events

By the end of this lesson

Pass data in and raise events out to a parent.

Data goes down, notifications come up. A parent passes values into a child through parameters. The child reads them and does not change them. When something happens that the parent needs to know about, the child raises an event and lets the parent decide what to do.

That single rule is what keeps a screen built from twenty components debuggable. For any piece of state there is exactly one component that owns it, so when the state is wrong you know which file to open.

Components/Employees/EmployeeRow.razor — the child
C#
<tr>
    <td>@Employee.Name</td>
    <td>@Employee.Department</td>
    <td>
        <button type="button" @onclick="RaiseSelected">
            View @Employee.Name
        </button>
    </td>
</tr>

@code {
    [Parameter, EditorRequired]
    public Employee Employee { get; set; } = default!;

    [Parameter]
    public EventCallback<Employee> OnSelected { get; set; }

    private Task RaiseSelected() => OnSelected.InvokeAsync(Employee);
}
  • [Parameter] makes a property settable from markup. A public property without it cannot be set as an attribute, so the attribute marks the component's intended inputs rather than exposing everything public.
  • EditorRequired turns a missing value into a build warning instead of a null reference at render time. The default! suffix tells the compiler you accept responsibility for it being supplied.
  • EventCallback<Employee> is the way out. It is a parameter too, so the parent provides it. The child does not know what happens when it is invoked, and that ignorance is what makes the component reusable.
  • InvokeAsync runs whatever the parent supplied and tells the parent's renderer that it should render again. A plain Action would run the method but not trigger that render, which produces a screen that updates one click late.
  • Returning the Task rather than marking the handler async void lets Blazor await it, so an exception inside the parent's handler surfaces instead of disappearing.
  • Nothing in this component decides what selection means. It reports that a button was pressed and which employee it was pressed for.
Components/Pages/Employees.razor — the parent
C#
<tbody>
    @foreach (var employee in employees)
    {
        <EmployeeRow @key="employee.Id"
                     Employee="employee"
                     OnSelected="Show" />
    }
</tbody>

@if (selected is not null)
{
    <EmployeeDetail Employee="selected" />
}

@code {
    private Employee? selected;

    private void Show(Employee employee) => selected = employee;
}
  • Attributes on a component set its parameters. Employee="employee" is a C# expression, not the string employee — Razor treats component attribute values as C#.
  • OnSelected="Show" hands the child a callback built from the parent's method. Blazor converts the method group to an EventCallback for you, and the compiler checks that the signatures agree.
  • The parent owns selected. Both children receive what they need through parameters, and neither of them assigns to it.
  • Because Show changed the parent's state, the parent re-renders and EmployeeDetail appears. The child that raised the event did nothing to make that happen.
  • Reading this file tells you the whole flow: what each row receives, what it announces, and what the screen does in response. That is the contract a page template cannot express.

Four ways data crosses a component boundary, and when each fits:

[Parameter]
Data in. The parent sets it and the child reads it. It is set again on every render of the parent, so any value the child wrote over is replaced.
[Parameter] with EventCallback<T>
Notification out. The child invokes it, the parent decides what happens, and the parent re-renders afterwards. Use EventCallback rather than Action or Func for anything a parent hands to a child.
@bind on an element
Two-way shorthand for an input. It writes the current value into the element and assigns the element's new value back to your field. It expands to a value attribute plus a change handler.
@bind-Value on a component
The same idea across a component boundary. A child exposes Value and ValueChanged; the parent writes @bind-Value once instead of wiring both. The Input components used in the next lesson work this way.
[CascadingParameter]
A value supplied by an ancestor without each level passing it on, used for things like the current user or a theme. It is easy to overuse: the dependency is invisible in the markup, so a component can stop working when it is moved.
Two-way binding on the edit fields
C#
<label for="employee-name">Full name</label>
<input id="employee-name" @bind="draft.Name" @bind:event="oninput" />

<label for="employee-department">Department</label>
<select id="employee-department" @bind="draft.Department">
    @foreach (var name in departments)
    {
        <option value="@name">@name</option>
    }
</select>

<p role="status">Preview: @draft.Name in @draft.Department</p>
  • @bind does two jobs: it writes the current value into the element, and it assigns the element's new value back to draft.Name.
  • By default the write-back happens on the change event, which for a text box means when it loses focus. @bind:event="oninput" moves it to every keystroke, which is what you want when something else on screen mirrors the value.
  • This is the pair from the previous lesson, collapsed into one directive. Knowing what it expands to is what lets you drop back to the long form when you need to do something extra in the handler.
  • Every field has a label whose for matches the input's id, so clicking the label focuses the field and a screen reader reads the two together. Placeholder text is not a label: it disappears as soon as someone types.
  • draft is a private field on this component. It is deliberately not the employee object that arrived as a parameter, which is what the next section is about.

Summary

  • [Parameter] carries data into a child; the child reads it and does not own it
  • EventCallback<T> carries notifications out, and invoking it re-renders the component that supplied it
  • @bind is shorthand for a value attribute plus a change handler, with @bind:event choosing when the write-back happens
  • Assigning to a parameter inside a child is overwritten on the parent's next render, which produces bugs that look intermittent
  • A child that needs to edit should copy the value into a private field and announce the result through an event

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Add a delete request

Add a Remove button to EmployeeRow that asks the parent to delete that employee. The child must not remove anything itself.

Then write down why the child is not allowed to do the deletion, in one sentence, before you look at the solution.

Show solution

The child receives one employee. It has no list, and the list it would need belongs to the parent — reaching it through a parameter and calling Remove on it is the mistake from this lesson, with a slower fuse.

There is a second reason, which matters more as the application grows. Deleting means calling an API, handling the failure, and possibly confirming with the user. Put any of that in the row and the row is no longer reusable anywhere the rules differ.

The child's whole job is to say that the button was pressed, for this employee. Naming the parameter OnDeleteRequested rather than OnDelete makes that boundary obvious to the next reader.

C#
@* EmployeeRow.razor *@
<button type="button" @onclick="RaiseDeleteRequested">
    Remove @Employee.Name
</button>

@code {
    [Parameter]
    public EventCallback<Employee> OnDeleteRequested { get; set; }

    private Task RaiseDeleteRequested() => OnDeleteRequested.InvokeAsync(Employee);
}

@* Employees.razor — the parent owns the list, so the parent removes from it *@
@code {
    private async Task RemoveEmployee(Employee employee)
    {
        await api.DeleteAsync(employee.Id);
        employees.Remove(employee);
        message = $"Removed {employee.Name}.";
    }
}

Think about it

Why EventCallback and not Action?

A child could expose Action<Employee> OnSelected and call it directly. Blazor provides EventCallback<T> instead.

What does EventCallback do that a plain delegate does not, and what would you notice if you used the delegate?

Show solution

EventCallback knows which component supplied it, so invoking it re-renders that component. With a plain Action the parent's field changes and nothing renders, so the screen updates on the next unrelated render — usually the next click. The symptom is a UI that is consistently one interaction behind, which is a maddening thing to debug.

EventCallback also handles async properly. InvokeAsync returns a Task the framework can await, so a slow handler can show a loading state and an exception in the parent's handler is not lost.

The rule worth remembering: a delegate parameter a parent hands to a child should be an EventCallback. Delegates inside one component, such as a comparison passed to a sort method, are ordinary C# and stay ordinary C#.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

A child component has a [Parameter] property Employee. In a handler, the child assigns a new object to that property. What happens?
What does adding @bind:event="oninput" to an input change?

Saved in this browser only.