I have this (simplified) component where the user input a value and presses enter. This should cause the input to disappear until a process has completed and then the input should show again.
@if (!commandRunning)
{
<div class="action">
<div class="command">
<span class="symbol">></span>
<input @ref="input" class="in" type="text" @bind-value="@_command" @bind-value:event="oninput" @onkeypress="@(e => KeyPressed(e))">
</div>
</div>
}
@code {
private string _command { get; set; }
private bool commandRunning;
private ElementReference input;
private async Task KeyPressed(KeyboardEventArgs args)
{
if (args.Key == "Enter")
{
commandRunning = true;
await Task.Delay(1000);
commandRunning = false;
await input.FocusAsync();
}
}
}
This however causes the following error:
blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Unable to focus an invalid element.
Error: Unable to focus an invalid element.
So the input has not yet been created at the point of focusing.
Altering the code by adding StateHasChanged() before the focus call makes it work however.
commandRunning = true;
await Task.Delay(1000);
commandRunning = false;
StateHasChanged();
await input.FocusAsync();
Is this the expected behaviour? Is the changed bool not evaluated in the razor code until after the method has been executed?
Is this the expected behaviour?
Yes.
There is an implied StateHasChanged() before and after an event like KeyPressed. StateHasChanged queues a render, it does not immediately execute it. The code path needs to await
something for the actual rendering to happen.
So when you need multiple 'updates' in your method you do need to call it yourself.
And in your solution the Rendering is happening somewhere during the input.FocusAsync()
.
I would go for safe with:
commandRunning = false;
StateHasChanged();
await Task.Delay(1); // allow the rendering to happen
await input.FocusAsync();
Every event in the UI has two steps, the call to the event handler followed by a call to StateHasChanged
. If the event handler returns a Task
then the caller waits on it's completion before calling StateHasChanged
. If it returns a void
then StateHasChanged
is called when the event handler yields (which may or may not be when it completes).
Let's look at what you have set up.
<input @ref="input" class="in" type="text" @bind-value="@_command" @bind-value:event="oninput" @onkeypress="@(e => KeyPressed(e))">
input
value is wired to _command
and set to update every time the user enters a value (at every keystroke including an enter).onkeypress
is wired up KeyPressed
and fire on every keypress.You have two events kicking off whenever the user enters a value.
Now to KeyPressed
commandRunning = true;
await Task.Delay(1000);
commandRunning = false;
await input.FocusAsync();
The first await
yields control back to the UI thread which almost certainly updates _command
and fires StateHasChanged
at the completion of that event. With Commandrunning
set to false
the html elements are wiped out and cease to exist.
After the TaskDelay
completes, commandRunning
is set back to true
. You then try to set the focus to input
which doesn't exist (the component hasn't been re-rendered and input created). Adding the StateHasChanged
before FocusAsync
re-renders the component, setting up all the html elements including input
.
After KeyPressed
completes StateHasChanged
is called by the orginating event (which doesn't do anything new).
To ensure what you want to do works correctly:
private async Task KeyPressed(KeyboardEventArgs args)
{
if (args.Key == "Enter")
{
commandRunning = true;
// make sure the UI has re-rendered and the html is destroyed
await InvokeAsync(StateHasChanged);
// Do whatever you're doing
await Task.Delay(1000);
commandRunning = false;
// make sure the UI has re-rendered and the html is re-built
await InvokeAsync(StateHasChanged);
await input.FocusAsync();
}
}
[Polite] Not sure why you should want to hide and then re-display the input, but.... it's your code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With