似乎任务没有完全取消

It seems that the task doesn't get canceled completely

我有一个文本框,可以防止用户写入我用正则表达式指定的不允许的字符,如果用户写入其中任何一个,就会出现一个弹出窗口并在那里停留 5 秒钟,我做了这个使用 Task.Delay(5000, cts.Token) 的持续时间也有一个取消标记,如果用户输入允许的字符,弹出窗口会出现在屏幕上,然后弹出窗口消失,并且该延迟任务被取消和处理,但是如果我写一个不允许的字符,然后立即写一个允许的字符,如果我写得非常快,那么当我写一个不允许的字符打开弹出窗口时,弹出窗口会在不到 5 秒的时间内消失(在随机的一秒内,我认为这是因为延迟任务在那一刻被取消,然后导致弹出窗口消失),这与我没有使用取消令牌取消延迟任务时的问题相同。但是对于我编写的代码,我不知道任务是如何不被完全取消的,或者它可能会被取消但问题是其他的......

    CancellationTokenSource cts;
    protected async override void OnPreviewTextInput(TextCompositionEventArgs e)
    {
        if (txtName.IsFocused)
        {
            if (cts != null && !regex.IsMatch(e.Text))
            {
                cts.Cancel();
                cts.Dispose();
            }

            cts = new CancellationTokenSource();

            if (regex.IsMatch(e.Text))
            {
                e.Handled = true;

                txtName.CaretIndex = txtName.Text.Length;

                if (AlertPopup.IsOpen == false)
                {
                    AlertPopup.IsOpen = true;
                    txtName.Focus();
                    try
                    {
                        await Task.Delay(5000, cts.Token);
                    }
                    catch (TaskCanceledException) { }
                    finally
                    {
                        AlertPopup.IsOpen = false;
                    }
                }
            }
            else
            {
                AlertPopup.IsOpen = false;
            }

            base.OnPreviewTextInput(e);
        }
    }

OnPreviewTextInput() 方法仅在 UI 线程中执行。同样,该方法中 await 之后的延续仅在 UI 线程中执行。 UI 线程一次只能做一件事。这意味着在错误弹出窗口已经显示的情况下,当有新的用户输入并且代码尝试取消该显示(即通过取消延迟的取消标记)时,await 随后导致 AlertPopup.IsOpen 属性 设置为 false 在对 OnPreviewTextInput() 的新调用已 returned 之前不会实际执行继续。

因此,当有新的用户输入时,该输入要么有效,要么无效。如果有效,则将 IsOpen 设置为 false 和 return。稍后再次隐藏弹出窗口是没有问题的。但如果输入无效,弹出窗口的当前状态是 IsOpen 为真,您将跳过尝试显示它、方法 returns 和 then 已取消任务的继续执行,隐藏弹出窗口。

请注意,这里的重要部分是延续发生在方法 return 之后。即使您尝试无条件地显示弹出窗口(即在执行该部分之前不检查 IsOpen),弹出窗口也会显示然后立即隐藏。

您可以通过使用不同的计时机制(即您可以延长计时器而不必取消和重新启动,例如 System.Threading.Timer)或使用您现在拥有的机制但保持每次触发无效状态时递增的计数器,每次计时器的等待完成时递减,并且仅当计数器再次达到零时才隐藏弹出窗口。

但实际上,我认为这都不是很好的用户交互。作为用户,我真的开始讨厌定时工具提示,因为它们永远不会显示足够长的时间。 WPF 中一种更常见的技术是使用一种内置验证机制(WPF 中至少有三种不同的验证变体,each with their own pros and cons),然后将元素的常规工具提示值设置为您的错误消息,以便用户只要将鼠标悬停在元素上就可以看到它。

任何类型的用户输入验证都存在障碍和麻烦。但是,如果您使用这些标准之一,至少您不必处理这个计时问题,并且您的用户可能会发现它也更适合他们。 :)