Two-Way Binding
A two-way binding is one decoration that writes a value out to the DOM and reads the user's edit back
into your state. .Bind is Razor's @bind, spelled so that everything it needs is an argument you can
see. Nothing generated compiles an expression tree at runtime, and nothing generated reflects; binding
an enum reaches one reflective lookup inside the framework's own converter.
Binding an element#
Name the attribute that carries the value, the event that reports a change, and a lambda that reads the current value. Only the first of those three appears in the markup:
protected override View Body => Div[ Input.Type("text").Bind("value", "oninput", () => _name), P[$"Hello, {_name}"]];
<div> <input type="text" value="Ada"> <p>Hello, Ada</p> </div>
The lambda is read in both directions. Its body becomes the attribute's value, and — in this
getter-only form — the same body becomes the left-hand side of the assignment the generator writes
for you. So the target has to be assignable: a field, a property with a setter, or a path through
either (_form.Name, Model.Items[0].Title, _dict["k"]). A computed expression such as
() => _name.ToUpper() reports BCF3018, and the way to write that is the
explicit setter below.
Use "oninput" to bind on every keystroke and "onchange" to bind when the element loses focus.
Those are the two you will usually want, and no list restricts the pair. Three rules do: both names
have to be non-empty compile-time constants (BCF3011), the event name has
to start with on (BCF3019), and neither may already be bound on the same
element by another decoration (BCF3010).
Nothing checks the names against HTML.
Checkboxes bind checked#
A checkbox binds a bool to the checked attribute, on onchange. The same decoration, a different
first argument, and a different attribute in the output:
protected override View Body => Label[ Input.Type("checkbox").Bind("checked", "onchange", () => _agreed), " I agree"];
<label><input type="checkbox" checked> I agree</label>
bool is HTML's boolean-attribute form, the same one
.Attr takes: true renders the attribute with an empty
value, false leaves it out entirely.
Why you write both names#
Razor infers the attribute from the markup: it reads the literal type="checkbox" out of your .razor
file and binds checked instead of value. This surface has no literal to read. The tag is a string
and type is an expression — Input.Type(kind) is an ordinary C# call whose value may not be known
until it runs — so there is nothing an inference could be checked against.
Defaulting to value would produce the failure most worth avoiding: a checkbox bound to the wrong
attribute, silently, with no diagnostic. So the rule across the whole surface is infer only what
you can verify. The element side cannot verify, so it does not infer, and you write two short
strings instead.
The half of the mistake that is checkable is caught: an event name that does not start with on
reports BCF3019, so swapping the two arguments stops at compile time
rather than adding an attribute that does nothing.
The component side of .Bind does infer names, because the same rule allows it there — see
binding a component parameter.
Normalizing with an explicit setter#
A fourth argument replaces the generated assignment with your own setter, which is where validation, normalization, or any work that has to happen on each edit belongs:
private string _name = ""; protected override View Body => Input.Type("text").Bind("value", "oninput", () => _name, v => _name = v.Trim());
This covers what Razor splits across @bind:get / @bind:set and @bind:after: the setter is the
write, so anything you would have run afterwards goes in the same lambda. Note that the write is now
yours to perform — nothing assigns for you once you supply a setter. A method group works too
(SetName), the lambda may have a block body, and an async variant is available by returning
Task:
Input.Type("text").Bind("value", "oninput", () => _query, async v => { _query = v; await SearchAsync(v); });
Only the getter-only form needs an assignable getter. With a setter, the getter is only ever read, so any expression will do. The getter itself must be an inline lambda with an expression body either way (BCF3017), because its body is copied into both the attribute value and the binder.
A normalizing setter creates a divergence: the element shows what was typed, while your field holds
the trimmed value. Ordinary diffing writes nothing, because the render tree has not changed since the
last render. On a value or checked binding, .Bind registers that attribute for DOM
resynchronization to close the gap, so the element is corrected to show the normalized value. This
needs no configuration.
That registration covers two names only. Blazor's client sends back a form element's own value — or
checked for a checkbox — and nothing else, so those are the only two names the generator registers.
A binding to any other attribute registers nothing; .Bind("hue", "onhuechange", () => _hue, Normalize)
on a custom element is the usual shape. The setter still runs and the new value still reaches the DOM
by ordinary diffing on the next render. What is missing is the repair above, for the case where
normalizing leaves the render tree unchanged and the element goes on showing what was typed.
The setter receives "" for an emptied text input, never null — which is why it takes a
non-nullable string. Writing to your own state from the setter is allowed, even though writing to
state anywhere else in a Body reports BCF3001: a setter is a deferred
handler, like an .OnClick lambda, and does not run while the tree is being built.
Numbers, dates, and enums#
Any type binds, as long as you write the culture. It is the last argument and it cannot be omitted:
private int _age; protected override View Body => Input.Type("number").Bind("value", "oninput", () => _age, CultureInfo.InvariantCulture);
The culture formats the value on the way out and parses it on the way back, through Blazor's own
BindConverter. Numbers, dates, times, Guid, enums, and every nullable form of those all work,
because the conversion is the framework's rather than this library's.
It is an argument rather than a default because a default would be a culture chosen without your
seeing it. Razor picks one from the element's literal type — the literal this surface does not
read, for the same reason it does not infer the attribute name. The choice moves to the call site.
Write the invariant culture for number and date#
<input type="number"> and <input type="date"> are defined in terms of a fixed format, not the
user's locale. Binding either under CultureInfo.CurrentCulture produces a value the element rejects
as soon as the current locale writes decimals with a comma.
This is not diagnosed. The check would need to read type, and type is an expression here. A
rule that fired only on a literal would catch the mistake in one spelling and miss it in another.
Write CultureInfo.InvariantCulture for those two, and use the current
culture for text the user reads as prose.
A value that will not parse is put back#
If the converter cannot read what was typed, your setter is never called and both your field and the
element return to the previous value. That is Blazor's behaviour, and .Bind reaches it through the
same DOM resynchronization described above.
This has a consequence worth choosing deliberately. On "oninput" the reversion runs on every
keystroke, so a decimal point typed into an int binding does not survive: 4. is rejected and the
. is removed. For numeric input that is usually not what you want:
// Reverts on blur, so a half-typed number survives. Input.Type("number").Bind("value", "onchange", () => _amount, CultureInfo.InvariantCulture);
"oninput" is still right when every intermediate value is meaningful — a range slider, or a text
field whose type accepts anything the user can type.
Emptying the field is a separate matter, and it is not a rejection. Blazor reads an empty string as
the type's default, so clearing an int binding gives you 0 rather than leaving the previous value
in place. Bind an int? where the field is genuinely optional; that takes null there.
Dates need a format#
A date input requires yyyy-MM-dd, and this surface cannot supply that from type the way Razor
does. Write it as the argument before the culture:
private DateOnly _due = new(2026, 8, 14); protected override View Body => Input.Type("date").Bind( "value", "oninput", () => _due, "yyyy-MM-dd", CultureInfo.InvariantCulture);
A format is accepted for DateTime, DateTimeOffset, DateOnly, TimeOnly and their nullable
forms, and for nothing else — those are the types the framework declares a format-taking converter
for. Writing one for an int reports BCF3031. To format a number, format
it in the getter and parse it in an explicit setter instead.
If you publish trimmed#
Binding any value type roots Blazor's BindConverter whole, including converters for types you never
bind. Measured on a trimmed self-contained publish, that is about 10 KB of
Microsoft.AspNetCore.Components.dll. An app that binds only string and bool does not pay it. It
is a one-off, not per binding.
Doing the conversion yourself#
The explicit form still works, and is what you want when the value needs validating rather than converting:
private decimal _amount; protected override View Body => Input.Type("text").Bind( "value", "onchange", () => _amount.ToString(CultureInfo.InvariantCulture), v => _amount = decimal.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var d) ? d : _amount);
Use TryParse rather than Parse here. An exception thrown from a setter is not the framework's
rejection path; it faults the render.
Binding a component parameter#
On a component the names are derived rather than written, because there the derivation can be checked.
.Bind selects the parameter with a lambda and the generator appends {Name}Changed, plus
{Name}Expression when the component declares it:
using Microsoft.AspNetCore.Components.Forms; private readonly NameModel _model = new(); protected override View Body => Component<InputText>().Bind(c => c.Value, () => _model.Name);
That single call supplies Value, ValueChanged, and ValueExpression. Every derived name is looked
up on the component type, so a missing or mistyped {Name}Changed reports
BCF3020 rather than binding nothing. This is what makes the asymmetry with
the element side a rule and not an inconsistency.
{Name}Expression is the reason the target is written as a getter lambda rather than passed by
reference. A component under an EditForm resolves a FieldIdentifier from that expression, and the
identifier is what ties the input to a property of your model, so validation messages land on the right
field. No other spelling could supply it.
The EditForm around it is written the same way. EditForm.ChildContent is a
RenderFragment<EditContext>, and the brackets supply it with the context discarded, so the content
below needs nothing else. Content that reads the EditContext is named with .Template instead:
protected override View Body => Component<EditForm>() .Param(form => form.Model, _model)[ Component<NameFields>().Param(fields => fields.Value, _model)];
The bound inputs may sit directly in the brackets or in their own component; the binding above, and
the ValueExpression it supplies, are unaffected either way. See
generic fragment parameters for the spelling
that reads the EditContext, and for when a cached delegate through .Param is the better choice.
Under static SSR — no interactive render mode on the page — the property holding EditForm.Model
needs [SupplyParameterFromForm] on itself, not a plain field. Only a [SupplyParameterFromForm]
property is repopulated from the posted form; anything else keeps whatever it held before the POST,
typically null. Splitting the bound inputs into their own component does not avoid this either:
InputText/InputTextArea derive the posted field's name from their own ValueExpression, so a
child component's own parameter posts under a different name than the one the holding property
expects back. samples/Guestbook/Components/Pages/GuestbookPage.cs shows the working shape.
An explicit setter and an async setter are available here too, with the same meaning as on an
element. TValue takes no culture and no format: the value goes to a parameter rather than to the
DOM, so nothing formats or parses it on the way and there is no choice to write down.
Remember that Component<T>() cannot name a .razor component declared in the same project
(BCF3012). Framework components such as InputText, and hand-written C#
components, always resolve.
What is checked#
Six diagnostics read a .Bind, and each has an entry in the reference:
| BCF3017 | the getter's shape |
| BCF3018 | a getter-only target that cannot be assigned |
| BCF3019 | an event name missing its on |
| BCF3020 | a component with no matching change callback |
| BCF3024 | a bound class beside a .Class |
| BCF3031 | a format the value's type has no converter for |
An element may carry more than one .Bind. If two of them share an attribute name or an event name,
that is BCF3010, the same duplicate any two decorations would report. DOM
resynchronization — the repair that puts a normalized value back over what the user typed — applies to
value and checked only, because those are the only two the browser sends back with the event.
Next#
See elements and decorations for the one-way .Attr and
.On this is built from, or components and reuse for .Param and the
rest of the component surface.