Design proposal: Bind get/set/after modifiers
See original GitHub issueSummary
Provides ways of customizing how Blazor’s @bind
reads and writes a value, solving several common problems at once.
Motivation and goals
After thinking through the “multiple event handlers” proposal (https://github.com/dotnet/aspnetcore/issues/39815), I’ve started to think the real problem we should solve is quite different. That is:
- I’m not convinced we need multiple event handlers.
- There are in fact some real problems with
@bind
today: A. No safe way to run async logic after a binding updates (#22394). This is a basic scenario even beginners will hit early. B. The magic consistency mechanism built into@bind
(essential for Blazor Server apps) can’t be used if you’re implementing avalue
/onchange
pair manually (#17281 #38520). This is a potential severe gotcha you may not notice until in production. C. When building a component with aValue
/ValueChanged
pair, there are two ways to do it, but neither are safe:- If you do a
@value
/@onchange
pair manually, you lose consistency guarantees (can lose keystrokes on Server) - Or, if you
@bind
to a property with aget
/set
pair, you have to discard theTask
returned byValueChanged.InvokeAsync
- If you do a
In scope
- Provide a safe and easy way to run async logic after a binding has updated
- Provide a safe and easy way to implement a component with a
Value
/ValueChanged
pair
These two directly address A and C above. If these two are done, I think B is solved implicitly because there’s no longer any reason to want to do a manual value
/onchange
pair. If the behavior you want is conceptually a two-way binding, then @bind
would now always have as much power as you need.
Also, I’m keen to do this in an idiomatically Blazory way. For example, there should not be some kind of new BindableValue
that you have to instantiate at runtime. Instead, typical components should be built only with basic C# types and patterns (e.g., string
and Func<string, Task>
).
Out of scope
- Although the design should clearly guide people to use
@bind
safely (that is, in a way that won’t drop keystrokes in async update cases like Blazor Server), I don’t think we can realistically stop people from using extra powers to do something misguided.
Proposed solution
There are really two distinct cases to deal with. I’ve considered extensively designs that would solve both using only a single concept, but didn’t feel that any of them led to enough elegance or convenience. So in the end I’m proposing two different but closely-related new mechanisms.
bind:after
To handle the “run async logic after binding”, consider a @bind:after
directive attribute:
<input @bind="searchText" @bind:after="PerformSearch" />
@code {
string searchText;
async Task PerformSearch()
{
// ... do something asynchronously with 'searchText' ...
}
}
I really like @bind:after
because it’s so basic. Any beginner can understand this. Even just seeing its name in autocompletion is probably enough to work out how to use it. This solves problem A in a super direct way.
It’s also very good for guiding people not to break the safety mechanism for not losing keystrokes in Blazor Server, since your custom logic doesn’t run until we’ve already assigned the new value to searchText
synchronously - all existing guarantees remain. Presumably we’d invoke PerformSearch
before the initial re-render, and as usual, have it trigger a further re-render asynchronously. This would happen automatically if the Razor compiler generates an event handler that does the assignment and then returns the task given by EventCallback.Factory.Create(yourBindAfterExpression).InvokeAsync()
.
bind:get and bind:set
This one’s less obvious, but for components that take a Value
/ValueChanged
pair, you’d be able to take the scary code you have to write today:
<input value="@Value" @onchange="@(EventCallback.Factory.CreateBinder<TValue>(this, _ => ValueChanged.InvokeAsync(_), Value))" />
@code {
[Parameter] public TValue Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
}
… and simplify it to this:
<input @bind:get="@Value" @bind:set="@ValueChanged" />
@code {
[Parameter] public TValue Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
}
With benefits:
- Simpler code
- Now you also get binding’s automatic formatting features on the
value=
side - Doesn’t risk losing keystrokes like the old one did (and of course it can still preserve the task returned from
ValueChanged
)
The behavior of course is that you’re controlling parts of the code that @bind
generates on both the reading and writing sides. The difference between @bind:set
and @bind:after
is that @bind:after
does the value writing for you before calling your code, whereas @bind:set
just calls your code (and passes the new value, having gone through @bind
’s built-in parsing logic).
Questions you may have:
- Why is it
@bind:get
+@bind:set
and not just@bind
+@bind:set
?- Because if you see
<input @bind="@val" @bind:set="@MyMethod" />
often, it creates confusion:- It looks as if the
@bind:set
is what makes it a two-way binding, and that you could make it one-way by removing that. Whereas in fact that would be wrong (you’d still have a two-way binding, just one that behaves differently now). - It looks as if it would be equivalent to write
<input value="@val" @bind:set="@MyMethod />
, and it almost is, but not quite because the formatting logic would differ. Much better not to create the ambiguity and have one correct solution.
- It looks as if the
- We can avoid the above problems by having a compiler rule that
@bind:get
and@bind:set
must always be used as a pair - you can’t just have one of them and not the other (nor can you have them with@bind
). So none of the weird cases will arise.
- Because if you see
- Couldn’t you use
@bind:set
to achieve (in effect)@bind:after
, and hence we don’t need@bind:after
?- Theoretically yes. You could
@bind:set
to a method that writes to your field and then runs your async logic. However, this is far less obvious for newcomers, and is less convenient in common cases. And it invites mistakes: if you do something async before setting the field, the UI will temporarily revert and generally behave badly. So it’s valuable to have@bind:after
for convenience and to guide correct usage. We can regard@bind:get
/@bind:set
as a more advanced case mainly for people implementing bindable components, as it gives them a really clean and safe solution, and such developers are advanced enough to understand that they just shouldn’t do async work before callingValueChanged
.
- Theoretically yes. You could
- Can you use all three at once, e.g.,
<input @bind:get="@value" @bind:set="@MyMethod" @bind:after="@DoStuff" />
?- Sure, why not? I think that the generated logic should await
MyMethod
before callingDoStuff
, since “after” feels like it means “after all the work involved in calling set”. It’s an edge case but I can’t think of any problems this will cause nor any major increase in implementation cost.
- Sure, why not? I think that the generated logic should await
- Do other
@bind
modifiers like@bind:event
and@bind:format
work with this?- Yes, and that’s partly why it’s a big improvement over manual
value
/onchange
pairs.
- Yes, and that’s partly why it’s a big improvement over manual
Risks / unknowns
Depends on the exact design chosen.
In the above design, it’s increasing the conceptual surface area of @bind
a bit and introducing new rules about which combinations of modifiers can be used together. I don’t think this is a killer problem because the compiler can enforce all this cleanly. However it is potentially more for people to learn. Mitigation: only tell most people about @bind:after
, as that’s the most widely needed and is super easy to understand.
Examples
See above
Issue Analytics
- State:
- Created 2 years ago
- Reactions:68
- Comments:14 (9 by maintainers)
Top GitHub Comments
Perhaps
@bind:onset
since it’s an event as opposed to a straight up setter unlike the getter? But I like this proposal a whole lot better.Will I be able to use :after (and others) with components?