-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Input*
- allow correct async handling of ValueChanged
callbacks (incl. @bind-Value:after
)#44105Description
Summary
In the current InputBase
design, the binding to underlying input
element is implemented by synchronous setter (CurrentValue_set
). The asynchronous ValueChanged
callback is called from the synchronous setter as "fire and forget" task (line 78):
aspnetcore/src/Components/Web/src/Forms/InputBase.cs
Lines 69 to 82 in 905557e
protected TValue? CurrentValue | |
{ | |
get => Value; | |
set | |
{ | |
var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value); | |
if (hasChanged) | |
{ | |
Value = value; | |
_ = ValueChanged.InvokeAsync(Value); | |
EditContext?.NotifyFieldChanged(FieldIdentifier); | |
} | |
} | |
} |
Although there already have been less frequent scenarios where this turned out to be an issue (e.g. manual binding using Value
, ValueChanged
and ValueExpression
parameters), starting with @bind-Value:after
this will become much bigger pain. Subscribtion to the ValueChanged
callback will become much easier and I expect the users will start using is much more (imagine continuous saving of values when filling the form).
I would like to start discussion on this topic, try to find a new design for InputBase
and derived components and allow proper asynchronous handling of the ValueChanged
callback (incl. @bind-Value:after
variant).
Motivation and goals
Consider this sample code. The exception is "lost", neither blazor-error-ui
, nor ErrorBoundary
or browser/server-console will capture the exception:
@page "/"
<EditForm Model="model">
<ErrorBoundary>
<InputText @bind-Value="model" @bind-Value:after="DoSomethingAfterValueChanged" />
</ErrorBoundary>
@*<input type="text" @bind-value="model" @bind-value:after="DoSomethingAfterValueChanged" />*@
</EditForm>
@code {
string model = String.Empty;
private Task DoSomethingAfterValueChanged()
{
Console.WriteLine("This executes.");
throw new InvalidOperationException("[1] This exception is lost in async-over-sync call from InputBase.CurrentValue_set.");
// exception not logged in Console
// exception not caught by ErrorBoundary
// exception not caught by Blazor global error UI (the yellow strip of death :-D)
}
}
(In opposite, the plain input
HTML element will behave correctly.)
Risks
It is obvious, that the solution will cause major breaking changes in current Input*
components as their protected
API for inherited components would have to change significantly (the CurrentValue
and CurrentValueAsString
properties will have to be replaced with some asynchronous subsitutes). With such major impact, consider creating of new set of NewInput*
components and keep the old ones as they are.
Examples
Give brief examples of possible developer experiences (e.g., code they would write).
Don't be deeply concerned with how it would be implemented yet. Your examples could even be from other technology stacks.
Detailed design
I would like to discuss the issue first with major stakeholders (e.g. @SteveSandersonMS, @javiercn, ...) and based on the approval that "this is something we want to solve" we can elaborate more on the detailed design. It is obvious that we have to handle the onchange
callback from the input
element in some asynchronous method rather than synchronous property setter (line 38).
aspnetcore/src/Components/Web/src/Forms/InputText.cs
Lines 32 to 41 in 905557e
protected override void BuildRenderTree(RenderTreeBuilder builder) | |
{ | |
builder.OpenElement(0, "input"); | |
builder.AddMultipleAttributes(1, AdditionalAttributes); | |
builder.AddAttributeIfNotNullOrEmpty(2, "class", CssClass); | |
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue)); | |
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)); | |
builder.AddElementReferenceCapture(5, __inputReference => Element = __inputReference); | |
builder.CloseElement(); | |
} |