Skip to content

[Blazor] Input* - allow correct async handling of ValueChanged callbacks (incl. @bind-Value:after) #44105

@hakenr

Description

@hakenr

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):

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).

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();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor ComponentsenhancementThis issue represents an ask for new feature or an enhancement to an existing one

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions