【问题标题】:How to focus an element when making it visible in the same method call?在同一方法调用中使其可见时如何聚焦元素?
【发布时间】:2021-05-02 19:39:07
【问题描述】:

我有这个(简化的)组件,用户输入一个值并按下回车键。这应该会导致输入消失,直到一个过程完成,然后输入应该再次显示。

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

然而,这会导致以下错误:

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.

所以输入尚未在聚焦点创建。

通过在焦点调用之前添加 StateHasChanged() 来更改代码使其工作。

commandRunning = true;
await Task.Delay(1000);
commandRunning = false;
StateHasChanged();
await input.FocusAsync();

这是预期的行为吗?是否在执行该方法之后才在剃刀代码中评估更改的布尔值?

【问题讨论】:

  • 不是“隐藏”文本框,而是让您的观众看到闪烁的文本框,可能会改变颜色;} 现在说真的,您真的要隐藏文本框吗?那么恢复呢?这真是太糟了。不要那样做。只需禁用文本框。
  • 将您的输入放在另一个组件中。使用新的组件循环来管理其焦点。
  • 我应该解释代码背后的原因。它用于模拟控制台终端。用户输入一些东西,按下回车键,输入被隐藏,命令被执行,当它完成后,用户现在可以再次输入。

标签: blazor


【解决方案1】:

这是预期的行为吗?

是的。

在 KeyPressed 等事件之前和之后都有一个隐含的 StateHasChanged()。 StateHasChanged 将渲染排队,它不会立即执行它。代码路径需要await 才能进行实际渲染。

因此,当您需要在方法中进行多次“更新”时,您确实需要自己调用它。

在您的解决方案中,渲染发生在input.FocusAsync() 的某处期间。我会选择安全的:

commandRunning = false;
StateHasChanged();
await Task.Delay(1);   // allow the rendering to happen
await input.FocusAsync();

【讨论】:

  • 所以如果在 StateHasChanged() 调用之后立即进行任何等待,我可以确定输入在那里?
  • 不,等待的方法可能是“假”异步。你必须知道。 Task.Delay() 更可靠,但我不确定,也许它会不堪重负。始终测试输入引用。
  • 您不能像this SO answer 建议的那样简单地将焦点设置在OnAfterRenderAsync 中吗?
  • @UweKeim - 也许取决于用户对在其他地方设置焦点的控制程度。 OnAfterRender 可以在不需要时移动焦点。
【解决方案2】:

UI 中的每个事件都有两个步骤,调用事件处理程序,然后调用StateHasChanged。如果事件处理程序返回一个Task,那么调用者会在调用StateHasChanged 之前等待它的完成。如果它返回 void,则在事件处理程序产生时调用 StateHasChanged(完成时可能会也可能不会)。

让我们看看你设置了什么。

<input @ref="input" class="in" type="text" @bind-value="@_command" @bind-value:event="oninput" @onkeypress="@(e => KeyPressed(e))">
  1. input 值连接到 _command 并设置为在用户每次输入值时更新(在每次击键时,包括 enter)。
  2. onkeypress 连接到 KeyPressed 并在每次按键时触发。

每当用户输入一个值时,就会启动两个事件。

现在到KeyPressed

    commandRunning = true;
    await Task.Delay(1000);
    commandRunning = false;

    await input.FocusAsync();

第一个await 将控制权交还给UI 线程,该线程几乎可以肯定会更新_command 并在该事件完成时触发StateHasChanged。将Commandrunning 设置为false 时,html 元素将被清除并不复存在。

TaskDelay 完成后,commandRunning 被设置回true。然后您尝试将焦点设置为不存在的input(组件尚未重新渲染并创建输入)。在FocusAsync 之前添加StateHasChanged 会重新渲染组件,设置包括input 在内的所有html 元素。 KeyPressed 完成后,StateHasChanged 被发起事件调用(它没有做任何新的事情)。

为了确保您想要做的事情正常工作:

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

[礼貌] 不知道为什么要隐藏然后重新显示输入,但是....这是您的代码。

【讨论】:

  • 感谢您的回答!我应该解释代码背后的原因。它用于模拟控制台终端。用户输入一些东西,按下回车键,输入被隐藏,命令被执行,当它完成后,用户现在可以再次输入。
  • 没有理由调用 StatehasChanged() 调用。而且没有效果,是同步的。
  • @HenkHolterman - 在这种情况下同意。但是,有时您需要在事件处理程序未在 UI 线程上运行时调用它。这是一个公共领域的答案,有人可能会复制这段代码并在这种情况下运行它,所以我在答案中采用了带大括号的方法。
猜你喜欢
  • 2016-10-12
  • 2019-01-13
  • 1970-01-01
  • 2012-02-27
  • 2018-01-14
  • 1970-01-01
  • 1970-01-01
  • 2015-04-30
  • 2021-05-02
相关资源
最近更新 更多