Skip to content

Latest commit

 

History

History
286 lines (223 loc) · 6.7 KB

File metadata and controls

286 lines (223 loc) · 6.7 KB

安装界面卡住问题修复

问题描述

安装过程中,进度条有时会卡在某个百分比(通常是 99% 或 100%),但实际上安装已经成功完成。用户需要手动关闭窗口才能发现应用已经安装。

根本原因

1. UI 线程调度问题

DispatcherQueue.TryEnqueue() 方法在某些情况下可能返回 false,导致 UI 更新失败:

  • 队列已满
  • 调度器正在关闭
  • 系统资源紧张

原代码:

DispatcherQueue.TryEnqueue(() =>
{
    ShowResult(success: true);
});

问题:如果 TryEnqueue 返回 false,结果界面永远不会显示。

2. 异步操作未正确等待

安装完成后直接调用 ShowResult,没有确保在 UI 线程上执行。

解决方案

1. 创建可靠的异步调度扩展方法

新增文件: InstallationSolution/Helpers/DispatcherQueueExtensions.cs

public static Task EnqueueAsync(this DispatcherQueue dispatcher, Action action)
{
    var tcs = new TaskCompletionSource<bool>();

    bool enqueued = dispatcher.TryEnqueue(() =>
    {
        try
        {
            action();
            tcs.SetResult(true);
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    });

    if (!enqueued)
    {
        tcs.SetException(new InvalidOperationException("无法将操作加入 UI 线程队列"));
    }

    return tcs.Task;
}

优势:

  • 返回 Task,可以 await 等待完成
  • 如果入队失败,抛出异常而不是静默失败
  • 捕获执行过程中的异常

2. 使用 await 确保 UI 更新完成

修改前:

var result = await op.AsTask();

if (result.ExtendedErrorCode == null || result.ExtendedErrorCode.HResult == 0)
    ShowResult(success: true);
else
    ShowResult(success: false, errorMessage: result.ErrorText);

修改后:

var result = await op.AsTask();
installCompleted = true;

// 确保在 UI 线程上显示结果
await DispatcherQueue.EnqueueAsync(() =>
{
    if (result.ExtendedErrorCode == null || result.ExtendedErrorCode.HResult == 0)
        ShowResult(success: true);
    else
        ShowResult(success: false, errorMessage: result.ErrorText);
});

3. 添加超时保护机制

finally
{
    // 安全措施:如果安装完成但 UI 没有更新(超时 2 秒),强制显示结果
    if (installCompleted)
    {
        await Task.Delay(2000);
        if (ResultContainer.Visibility != Visibility.Visible)
        {
            await DispatcherQueue.EnqueueAsync(() =>
            {
                ShowResult(success: true);
            });
        }
    }
}

工作原理:

  1. 安装完成后设置 installCompleted = true
  2. 等待 2 秒
  3. 检查结果界面是否已显示
  4. 如果没有显示,强制显示结果界面

4. 改进进度回调

修改前:

op.Progress = (_, progress) =>
{
    DispatcherQueue.TryEnqueue(() =>
    {
        InstallProgressBar.Value = progress.percentage;
        // ...
    });
};

修改后:

op.Progress = (_, progress) =>
{
    // 使用优先级队列确保 UI 更新
    _ = DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
    {
        InstallProgressBar.Value = progress.percentage;
        // ...
    });
};

改进:

  • 明确指定优先级为 Normal
  • 进度更新失败不会影响整体流程(使用 _ 丢弃返回值)

5. 简化 ShowResult 方法

修改前:

private void ShowResult(bool success, string? errorMessage = null)
{
    DispatcherQueue.TryEnqueue(() =>
    {
        // 更新 UI
    });
}

修改后:

private void ShowResult(bool success, string? errorMessage = null)
{
    // 直接更新 UI(已经在 UI 线程上)
    InstallingContainer.Visibility = Visibility.Collapsed;
    ResultContainer.Visibility = Visibility.Visible;
    // ...
}

原因: 现在 ShowResult 总是从 UI 线程调用(通过 EnqueueAsync),不需要再次调度。

测试场景

1. 正常安装

  • ✅ 进度条正常更新
  • ✅ 安装完成后立即显示结果

2. 快速安装(< 1 秒)

  • ✅ 即使安装很快完成,结果界面也能正确显示

3. 系统资源紧张

  • ✅ 即使 UI 线程繁忙,超时机制确保结果显示

4. 安装失败

  • ✅ 错误信息正确显示
  • ✅ 不会卡在进度界面

技术细节

DispatcherQueue 优先级

WinUI 3 的 DispatcherQueue 支持三个优先级:

  • High: 高优先级,立即执行
  • Normal: 正常优先级(默认)
  • Low: 低优先级,在空闲时执行

我们使用 Normal 优先级平衡响应性和性能。

TaskCompletionSource 模式

TaskCompletionSource<T> 是将回调式 API 转换为异步 API 的标准模式:

var tcs = new TaskCompletionSource<bool>();

// 在回调中设置结果
callback(() => tcs.SetResult(true));

// 等待完成
await tcs.Task;

超时保护的权衡

优点:

  • 确保 UI 不会永久卡住
  • 用户体验更好

缺点:

  • 增加 2 秒延迟(仅在 UI 更新失败时)
  • 轻微增加内存占用(Task 对象)

结论: 优点远大于缺点,2 秒延迟是可接受的。

相关问题

为什么不使用 Dispatcher.Invoke?

WinUI 3 使用 DispatcherQueue 而不是 WPF 的 Dispatcher

  • DispatcherQueue 是异步优先的设计
  • 没有同步的 Invoke 方法
  • 更符合现代异步编程模式

为什么不使用 ConfigureAwait(false)?

在 UI 代码中,我们需要回到 UI 线程:

// ❌ 错误:可能在后台线程执行
await op.AsTask().ConfigureAwait(false);
ShowResult(success: true); // 崩溃!

// ✅ 正确:确保在 UI 线程执行
await op.AsTask();
await DispatcherQueue.EnqueueAsync(() => ShowResult(success: true));

性能影响

内存

  • 每次安装增加约 1KB 内存(TaskCompletionSource 对象)
  • 可忽略不计

CPU

  • 超时检查增加一个 2 秒的 Task.Delay
  • 仅在 UI 更新失败时触发
  • 影响极小

响应性

  • ✅ 改进:UI 更新更可靠
  • ✅ 改进:错误处理更完善
  • ⚠️ 轻微延迟:最多 2 秒(仅在异常情况)

总结

通过以下改进,彻底解决了安装界面卡住的问题:

  1. 可靠的 UI 调度 - EnqueueAsync 扩展方法
  2. 异步等待 - 确保 UI 更新完成
  3. 超时保护 - 2 秒后强制显示结果
  4. 优先级控制 - 使用 Normal 优先级
  5. 简化代码 - 移除不必要的嵌套调度

用户体验改进:

  • 安装完成后立即看到结果
  • 不再需要手动关闭窗口
  • 错误信息及时显示
  • 整体流程更流畅

代码质量改进:

  • 更好的异常处理
  • 更清晰的异步流程
  • 更可靠的 UI 更新
  • 更易于维护和调试