IT评测·应用市场-qidao123.com
标题:
技术速递|调用异步功能 - WinForms 在 .NET 9 中的将来发展
[打印本页]
作者:
络腮胡菲菲
时间:
2025-1-3 19:15
标题:
技术速递|调用异步功能 - WinForms 在 .NET 9 中的将来发展
作者: Klaus Loeffelmann
排版:Alan Wang
随着 .NET 的不断发展,WinForms 开辟者可用的工具也在不断进步,这使得开辟更加高效且应用响应更敏捷。在 .NET 9 中,我们很高兴引入了一系列新的异步 API,这些 API 大大简化了 UI 管理任务。从更新控件到显示窗体和对话框,这些新增功能以全新的方式将异步编程的强盛功能引入到 WinForms 中。在本文中,我们将深入探讨四个关键 API,表明它们的工作原理、实用场景以及如何开始利用它们。
认识新的异步 API
.NET 9 专门为 WinForms 引入了几种异步 API,使得在异步场景中进行 UI 操作变得更加直观和高效。这些新增功能包罗:
Control.InvokeAsync – 在 .NET 9 中全面发布的 API,有助于异步调用调用 UI 线程。
Form.ShowAsync 和 Form.ShowDialogAsync(实验性) – 这些 API 允许开辟者以异步方式显示窗体,在复杂的 UI 场景中极大简化操作。
TaskDialog.ShowDialogAsync(实验性) – 该 API 提供了一种异步显示那些基于任务对话框的消息对话框控件的方法,特殊实用于长时间运行的与 UI 绑定的操作。
接下来,我们将从 InvokeAsync 开始逐一解析这些 API。
Control.InvokeAsync:无缝异步 UI 线程调用
InvokeAsync 提供了一种强盛的,可在不壅闭调用线程的情况下将调用传递给UI线程的方法。此方法允许在 UI 线程上实行同步和异步回调,提供了灵活性,并防止不测的“即发即弃”行为。它通过将操作排入 WinForms 主消息队列来实现,确保它们在 UI 线程上实行。这种行为类似于 Control.Invoke,后者也会将调用调理到 UI 线程,但两者之间有一个紧张区别:InvokeAsync 不会壅闭调用线程,因为它是将委托发布到消息队列中,而不是直接发送。
Wait – 发送与发布?消息队列?
让我们分解这些概念,分析它们的含义,以及为什么 InvokeAsync 的方法可以资助改善应用程序的响应性。
在 WinForms 中,全部的 UI 操作都发生在主 UI 线程上。为了管理这些操作,UI 线程运行一个循环,称为消息循环(message loop),该循环会连续处置惩罚消息——例如按钮点击、屏幕重绘以及其他操作。这个循环是 WinForms 能够在处置惩罚指令的同时对用户操作保持响应的核心。当您利用现代 API 时,大多数应用程序代码并不是运行在这个 UI 线程上的。理想情况下,UI 线程应该仅用于那些必须更新UI的操作。然而,在某些情况下,代码不会主动运行在 UI 线程上。例如,当您启动一个独立的任务以并行实行计算密集型操作时,就会发生这种情况。在这些情况下,您需要将代码实行“调理”到 UI 线程,如许 UI 线程才能更新界面。否则就会出现以下情况:
假设我不被允许进入某个房间取一杯牛奶,而你可以。在这种情况下,只有一个选择:因为我不大概酿成你,所以我只能请求你帮我取那杯牛奶。这与线程调理是一样的。工作线程不能酿成 UI 线程,但代码的实行(取牛奶)可以被调理。换句话说,工作线程可以请求 UI 线程代表它实行某些代码。简朴来说,这通过将一个方法的委托排入消息队列中来实现。
说到这里,让我们办剃头送和发布的困惑:在消息循环中列队操作有两种主要方式:
发送消息(壅闭)
:Control.Invoke 利用这种方式。当调用 Control.Invoke 时,它会将指定的委托同步发送到 UI 线程的消息队列。这是一个壅闭操作,意味着调用线程会等待 UI 线程处置惩罚完该委托后才能继续。这在调用代码依赖于 UI 线程立即返回结果时非常有效,但如果过分利用,尤其是在处置惩罚长时间运行的操作时,大概导致 UI 卡顿。
发布消息(非壅闭)
:InvokeAsync 将委托发布到消息队列,这是一个非壅闭操作。这种方式告诉 UI 线程将操作排入队列,并尽快处置惩罚,但调用线程无需等待操作完成。方法会立即返回,使调用线程可以继续其工作。这种区别在异步场景中尤为紧张,因为它允许应用程序同时处置惩罚其他任务而不产生耽误,从而最大限度地减少 UI 线程的瓶颈。
这里是一个简朴比较:
操作方法壅闭描述发送Control.Invoke是在 UI 线程上调用委托,并等待其完成。发布Control.InvokeAsync否将委托排入 UI 线程的队列,并立即返回。
为什么这很紧张
通过利用 InvokeAsync 发布委托,您的代码如今可以将多个更新列队到控件上,实行背景操作,或等待其他异步任务,而无需壅闭主 UI 线程。这种方法不仅有助于防止“冻结的 UI”体验,还能保持应用程序的响应性,纵然在处置惩罚大量与 UI 绑定的任务时也能保持流畅。
总结:Control.Invoke 会等待 UI 线程完成委托(壅闭),InvokeAsync 会将任务交给 UI 线程,并立即返回(非壅闭)。这种差异使得 InvokeAsync 非常适合异步场景,让开辟者能够构建更流畅、更具响应性的 WinForms 应用程序。
以下是每个 InvokeAsync 重载的工作方式:
public async Task InvokeAsync(Action callback, CancellationToken cancellationToken = default)
public async Task<T> InvokeAsync<T>(Func<T> callback, CancellationToken cancellationToken = default)
public async Task InvokeAsync(Func<CancellationToken, ValueTask> callback, CancellationToken cancellationToken = default)
public async Task<T> InvokeAsync<T>(Func<CancellationToken, ValueTask<T>> callback, CancellationToken cancellationToken = default)
复制代码
每个重载都允许不同的同步和异步方法组合,可以选择是否带有返回值:InvokeAsync(Action callback, CancellationToken cancellationToken = default) 用于没有返回值的同步操作。如果您想在 UI 线程上更新控件的属性——例如设置 Label 的 Text 属性——这个重载允许您做到这一点,而无需等待返回值。回调会被发布到消息队列,并异步实行,返回一个 Task,如果需要,您可以等待该任务的完成。
await control.InvokeAsync(() => control.Text = "Updated Text");
InvokeAsync(Funccallback, CancellationToken cancellationToken = default)
复制代码
用于返回类型为 T 的同步操作。利用它可以在 UI 线程上计算并获取一个值,例如从 ComboBox 中获取 SelectedItem。InvokeAsync 将回调发布到 UI 线程,并返回一个 Task,允许您等待结果的完成。
int itemCount = await control.InvokeAsync(() => comboBox.Items.Count);
InvokeAsync(Funccallback, CancellationToken cancellationToken = default):
复制代码
这个重载用于不返回结果的异步操作。它非常实用于较长时间运行的异步操作,更新 UI 的场景,例如等待数据加载完成后再更新控件。回调吸收一个 CancellationToken 以支持取消,并需要返回一个 ValueTask,InvokeAsync 会(内部)等待该任务完成,同时保持 UI 在操作异步实行时的响应性。因此,现实上有两个“等待”发生:InvokeAsync 被等待(或者说可以被等待),同时您传递的 ValueTask 也会被内部等待。
await control.InvokeAsync(async (ct) =>
{
await Task.Delay(1000, ct); // Simulating a delay
control.Text = "Data Loaded";
});
InvokeAsync(Func<cancellationtoken, valuetask> callback, CancellationToken cancellationToken = default)
复制代码
末了是用于返回类型为 T 的异步操作的重载版本。当一个异步操作必须在 UI 线程上完成并返回一个值时利用,例如在耽误后查询控件的状态或获取数据以更新 UI。回调吸收一个 CancellationToken 并返回一个 ValueTask,InvokeAsync 会等待该任务完成并提供结果。
var itemCount = await control.InvokeAsync(async (ct) =>
{
await Task.Delay(500, ct); // Simulating data fetching delay
return comboBox.Items.Count;
});
复制代码
快速决策:选择正确的重载
对于没有返回值的同步操作,利用 Action。
对于有返回值的同步操作,利用 Func。
对于没有结果的异步操作,利用 Func<CancellationToken, ValueTask>。
对于有结果的异步操作,利用 Func<cancellationtoken, valuetask>。
利用正确的重载有助于在异步 WinForms 应用程序中平滑处置惩罚 UI 任务,克制主线程瓶颈,并提升应用程序的响应性。
以下是一个简朴的例子:
var control = new Control();
// Sync action
await control.InvokeAsync(() => control.Text = "Hello, async world!");
// Async function with return value
var result = await control.InvokeAsync(async (ct) =>
{
control.Text = "Loading...";
await Task.Delay(1000, ct);
control.Text = "Done!";
return 42;
});
复制代码
混淆异步和同步重载——真的会发生吗?
由于有许多重载选项,大概会误将异步方法传递给同步重载,从而导致不测的“即发即弃”行为。为了防止这种情况,WinForms 在 .NET 9 中引入了一种专门的 WinForms 分析器,当将一个异步方法(例如返回 Task 的方法)传递给不带 CancellationToken 的 InvokeAsync 同步重载时,该分析器会检测到并触发告诫。这有助于您在潜伏问题引发运行时错误之前发现并纠正它们。
例如,传递一个不支持 CancellationToken 的异步方法大概会生成如下告诫:
warning WFO2001: Task is being passed to InvokeAsync without a cancellation token.
复制代码
此分析器确保异步操作被正确处置惩罚,从而在您的 WinForms 应用程序中保持可靠且响应敏捷的行为。
实验性 API
除了 InvokeAsync,WinForms 在 .NET 9 中还引入了用于显示窗体和对话框的实验性异步选项。这些 API 仍处于实验阶段,但为开辟者提供了更大的异步 UI 交互灵活性,例如文档管理和窗体生命周期控制。
Form.ShowAsync 和 Form.ShowDialogAsync 是新的方法,允许异步显示窗体。它们简化了多个窗体实例的处置惩罚,尤其实用于需要多个相同学体类型实例的情况,例如在单独窗口中显示不同文档时。
以下是如何利用 ShowAsync 的基本示例:
var myForm = new MyForm();
await myForm.ShowAsync();
复制代码
并且对于模态对话框,您可以利用 ShowDialogAsync:
var result = await myForm.ShowDialogAsync();
if (result == DialogResult.OK)
{
// Perform actions based on dialog result
}
复制代码
这些方法简化了异步窗体显示的管理,并资助您在等待用户交互时克制壅闭 UI 线程。
TaskDialog.ShowDialogAsync
TaskDialog.ShowDialogAsync 是 .NET 9 中的另一个实验性 API,旨在提升对话框交互的灵活性。它提供了一种异步显示任务对话框的方法,非常适合涉及耗时操作或多步调流程的场景。
以下是异步显示任务对话框的方法示例:
var taskDialogPage = new TaskDialogPage
{
Heading = "Processing...",
Text = "Please wait while we complete the task."
};
var buttonClicked = await TaskDialog.ShowDialogAsync(taskDialogPage);
复制代码
此 API 允许开辟者异步显示对话框,从而开释 UI 线程,提供更流畅的用户体验。
异步 API 的现实应用
这些异步 API 为 WinForms 应用程序解锁了新的功能,特殊是在多表单应用程序、MVVM 计划模式和依赖注入场景中。通过利用异步操作处置惩罚表单和对话框,您可以:
在异步场景中简化表单生命周期管理,特殊是当处置惩罚同一表单的多个实例时。
支持 MVVM 和 DI 工作流,在 ViewModel 驱动的架构中,异步表单处置惩罚是有益的。
克制 UI 线程壅闭,纵然在实行密集操作时也能实现更具响应性的界面。
如果您对如何通过 Invoke.Async 彻底改变 WinForms 应用程序的 AI 驱动现代化感到好奇,那么请观看 .NET Conf 2024 的演讲,看看这些功能在现实场景中的实现!
“智能化”你的 WinForms 应用程序
这还不是全部——不要错过我们在另一场出色讲座中深入探讨 .NET 9 中 WinForms 的全部新特性。深入相识并获得灵感!
如何从同步操作启动异步操作
在 UI 场景中,从同步上下文触发异步操作是很常见的。固然,我们都知道,最好克制利用 async void 方法。
为什么要克制这种做法?当你利用 async void 时,调用者无法等待或观察方法的完成。这大概导致未处置惩罚的异常或意生手为。async void 方法现实上是“即发即弃”,它们不受Task提供的尺度错误处置惩罚机制的束缚。这使得在大多数场景中调试和维护更加困难。
但是!这里有一个破例,那就是变乱处置惩罚方法或具有“变乱处置惩罚方法特征”的方法。变乱处置惩罚方法不能返回 Task 或 Task,因此 async void 允许它们触发异步操作,而不会壅闭 UI 线程。然而,由于 async void 方法不可等待,异常很难被捕获。为了办理这个问题,你可以在变乱处置惩罚方法内部的异步操作周围利用错误处置惩罚布局,比如 try-catch。如许,纵然在这些特殊情况下,也能确保异常得到恰当处置惩罚。
例如:
private async void Button_Click(object sender, EventArgs e)
{
try
{
await PerformLongRunningOperationAsync();
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
复制代码
在这里,由于变乱处置惩罚程序的署名,async void 是不可克制的,但通过将等待的代码包装在 try-catch 中,我们可以安全地处置惩罚异步操作过程中大概发生的任何异常。
以下示例利用一个名为 SevenSegmentTimer 的7段显示控件,以典型的7段式显示方式显示一个计时器,精度为非常之一秒。它有几个方法来更新和动画内容:
public partial class TimerForm : Form
{
private SevenSegmentTimer _sevenSegmentTimer;
private readonly CancellationTokenSource _formCloseCancellation = new();
public FrmMain()
{
InitializeComponent();
SetupTimerDisplay();
}
[MemberNotNull(nameof(_sevenSegmentTimer))]
private void SetupTimerDisplay()
{
_sevenSegmentTimer = new SevenSegmentTimer
{
Dock = DockStyle.Fill
};
Controls.Add(_sevenSegmentTimer);
}
override async protected void OnLoad(EventArgs e)
{
base.OnLoad(e);
await RunDisplayLoopAsyncV1();
}
private async Task RunDisplayLoopAsyncV1()
{
// When we update the time, the method will also wait 75 ms asynchronously.
_sevenSegmentTimer.UpdateDelay = 75;
while (true)
{
// We update and then wait for the delay.
// In the meantime, the Windows message loop can process other messages,
// so the app remains responsive.
await _sevenSegmentTimer.UpdateTimeAndDelayAsync(
time: TimeOnly.FromDateTime(DateTime.Now));
}
}
}
复制代码
当我们运行这个程序时,我们可以在屏幕上的窗体中看到这个计时器:
异步方法 UpdateTimeAndDelayAsync 完全按字面意思实行:它更新控件中显示的时间,然后等待我们在前一行通过 UpdateDelay 属性设置的时间。
如您所见,这个异步方法 RunDisplayLoopAsyncV1 在窗体的 OnLoad 中启动。这是典型的做法,即如何从同步 void 方法中启动异步操作。
对于典型的 WinForms 开辟者来说,乍一看这大概会显得有些奇怪。毕竟,我们在 OnLoad 中调用了另一个方法,而谁人方法永远不会返回,因为它终极会进入一个无穷循环。那么,在这种情况下,OnLoad 是否会完成呢?我们不是在这里壅闭应用程序吗?
这就是异步编程的亮点地点。只管 RunDisplayLoopAsyncV1 包含一个无穷循环,但它是以异步方式构造的。当在循环内部碰到 await 关键字时(例如 await _sevenSegmentTimer.UpdateTimeAndDelayAsync()),方法会将控制权返回给调用者,直到等待的任务完成。
在 WinForms 应用程序的上下文中,这意味着 Windows 消息循环可以继续处置惩罚变乱,比如重新绘制 UI、处置惩罚按钮点击或响应键盘输入。由于 await 暂停了 RunDisplayLoopAsyncV1 的实行而没有壅闭 UI 线程,应用程序保持响应。
当 OnLoad 被标记为async时,它会在碰到 RunDisplayLoopAsyncV1 中的第一个 await 时完成。待任务完成后,运行时会从前次暂停的地方恢复实行 RunDisplayLoopAsyncV1。这一切发生时不会壅闭 UI 线程,现实上允许 OnLoad 立即 return,纵然异步操作在背景继续实行。
背景实行?您可以将其视为将方法拆分成几个部分,就像一个假造的 WaitAsync-Initiator,它在第一个 await 办理后被调用。接着它启动一个背景运行的 WaitAsync-Waiter,直到等待期结束。然后,触发 WaitAsync-Callback,现实上要求消息循环重新进入调用并完成全部跟随该异步调用的操作。
因此,现实的代码路径大致如下:
最好的理解方式是将其与连续处置惩罚的两个鼠标点击变乱进行比较,第一个鼠标点击触发了 RunDisplayLoopAsyncV1,而第二个鼠标点击对应于 WaitAsync 回调,进入该方法的“第3部分”,当耽误正好在等待时。
这个过程随后会对每个异步方法中的 await 进行重复。这就是为什么纵然存在无穷循环,应用程序也不会卡住。现实上,技术上来说,OnLoad 现实上是正常完成的,但每个 await 后的部分会被消息循环在稍后的时间回调。
如今,我们仍旧基本上只在 UI 线程上工作。(严格来说,回调会在短暂的时间内运行在线程池线程上,但我们暂时忽略这一点。)是的,我们是异步的,但到目前为止,并没有真正发生并行操作。直到如今,这更像是一个奇妙构造的接力赛,接力棒被无缝地传递给下一个选手,以至于根本不会有卡顿或壅闭。
但是,异步方法随时可以从不同的线程调用。如果我们在当前示例中如许做……
private async Task RunDisplayLoopAsyncV2()
{
// When we update the time, the method will also wait 75 ms asynchronously.
_sevenSegmentTimer.UpdateDelay = 75;
// Let's kick-off a dedicated task for the loop.
await Task.Run(ActualDisplayLoopAsync);
// Local function, which represents the actual loop.
async Task ActualDisplayLoopAsync()
{
while (true)
{
// We update and then wait for the delay.
// In the meantime, the Windows message loop can process other messages,
// so the app remains responsive.
await _sevenSegmentTimer.UpdateTimeAndDelayAsync(
time: TimeOnly.FromDateTime(DateTime.Now));
}
}
}
复制代码
然后…
InvokeAsync 的重载解析的复杂性
如我们之前所学,这是一个很容易办理的问题,对吧?我们只是利用 InvokeAsync 来调用我们本地的函数 ActualDisplayLoopAsync,然后就完成了。那么,让我们这么做吧。我们获取 InvokeAsync 返回的 Task,然后将其传递给 Task.Run。轻松办理。
好吧——看起来并不太好。我们碰到了两个问题。首先,如前所述,我们正在实验调用一个返回 Task 的方法,但没有传递取消令牌。InvokeAsync 正在告诫我们,在这种情况下我们正在设置一个“即发即弃”操作,而这个操作无法被内部等待。第二个问题不仅仅是告诫,它照旧一个错误。InvokeAsync 返回的是一个 Task,我们固然不能将其传递给 Task.Run。我们只能传递一个 Action 或返回 Task 的 Func,但绝不能直接传递一个 Task。不过,我们可以做的是将这一行转换为另一个本地函数,所以从这里……
// Doesn't work. InvokeAsync wants a cancellation token, and we can't pass Task.Run a task.
var invokeTask = this.InvokeAsync(ActualDisplayLoopAsync);
// Let's kick-off a dedicated task for the loop.
await Task.Run(invokeTask);
// Local function, which represents the actual loop.
async Task ActualDisplayLoopAsync(CancellationToken cancellation)
复制代码
改为:
// This is a local function now, calling the actual loop on the UI Thread.
Task InvokeTask() => this.InvokeAsync(ActualDisplayLoopAsync, CancellationToken.None);
await Task.Run(InvokeTask);
async ValueTask ActualDisplayLoopAsync(CancellationToken cancellation=default)
...
复制代码
如今它工作得非常顺利了!
为性能或目标代码流程进行并行化
我们的7段控制器另有一个奇妙的功能:分隔列的渐变动画。我们可以按以下方式利用这个功能:
private async Task RunDisplayLoopAsyncV4()
{
while (true)
{
// We also have methods to fade the separators in and out!
// Note: There is no need to invoke these methods on the UI thread,
// because we can safely set the color for a label from any thread.
await _sevenSegmentTimer.FadeSeparatorsInAsync().ConfigureAwait(false);
await _sevenSegmentTimer.FadeSeparatorsOutAsync().ConfigureAwait(false);
}
}
复制代码
当我们运行它时,结果看起来像如许:
然而,存在一个挑战:我们如何设置代码流程,使得运行时钟和渐变分隔符能够并行实行,并且都在一个连续的循环中?
为了实现这一目标,我们可以利用基于任务的并行性。
具体思路如下:
同时运行时钟更新和分隔符渐变:我们异步实行这两个任务,并等待它们完成。
妥善处置惩罚不同任务的时长:由于时钟更新和渐变动画大概需要不同的时间,我们利用 Task.WhenAny 来确保较快的任务不会耽误较慢的任务。
重置已完成的任务:一旦某个任务完成,我们将其重置为 null,以便下一次迭代时重新启动该任务。
终极结果是:
private async Task RunDisplayLoopAsyncV6()
{
Task? uiUpdateTask = null;
Task? separatorFadingTask = null;
while (true)
{
async Task FadeInFadeOutAsync(CancellationToken cancellation)
{
await _sevenSegmentTimer.FadeSeparatorsInAsync(cancellation).ConfigureAwait(false);
await _sevenSegmentTimer.FadeSeparatorsOutAsync(cancellation).ConfigureAwait(false);
}
uiUpdateTask ??= _sevenSegmentTimer.UpdateTimeAndDelayAsync(
time: TimeOnly.FromDateTime(DateTime.Now),
cancellation: _formCloseCancellation.Token);
separatorFadingTask ??= FadeInFadeOutAsync(_formCloseCancellation.Token);
Task completedOrCancelledTask = await Task.WhenAny(separatorFadingTask, uiUpdateTask);
if (completedOrCancelledTask.IsCanceled)
{
break;
}
if (completedOrCancelledTask == uiUpdateTask)
{
uiUpdateTask = null;
}
else
{
separatorFadingTask = null;
}
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
_formCloseCancellation.Cancel();
}
复制代码
另有这个。在这个动画 GIF 中,您可以看到 UI 始终保持响应性,因为窗口可以通过鼠标平滑拖动。
总结
通过这些新的异步 API,.NET 9 为 WinForms 带来了先进的功能,使得处置惩罚异步 UI 操作变得更加容易。虽然一些 API,如 Control.InvokeAsync,已经准备好投入利用,但针对表单和对话框管理的实验性 API 为响应式 UI 开辟提供了更多令人兴奋的大概性。
您可以在我们的 Extensibility-Repo 中的相应示例子文件夹找到本博客文章的示例代码。
通过 .NET 9 探索 WinForms 中异步编程的潜力,并确保在非关键项目中测试这些实验性功能。像往常一样,您的反馈至关紧张,我们期待听到这些新的异步功能如何提升您的开辟过程!
末了,祝编码愉快!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/)
Powered by Discuz! X3.4