9.1 常见问题与排查
学习目标
- 从可验证的现象缩小问题范围。
- 找到最短检查路径,不把 UI 错误和 API 错误混在一起。
- 用一次明确复现确认修复,而不是凭感觉判断。
常见问题:从哪里开始查
先判断失败发生在构建、窗口创建、绑定更新还是网络请求。能独立调用 API 但界面不显示时,重点检查客户端;连独立请求也失败时,先核对 API 和地址。每次只改变一个原因,再重复原来的操作确认结果。
项目无法构建或启动
有 dotnet,为什么不能 build? dotnet --list-runtimes 只显示运行能力。执行 dotnet --list-sdks,TaskDesk 需要 10.x SDK;WPF 本身仅支持 Windows。NuGet 还原失败则检查错误中的包源、网络和版本,不能把它当成 XAML 错误。参见 开发环境。
构造函数已经注入,为什么窗口打不开? 如果 StartupUri 仍指向窗口,WPF 会尝试按默认方式创建它,绕开 DI。TaskDesk 删除 StartupUri,在 App 中注册并解析窗口;所有构造依赖都必须已注册。不要为了启动成功又增加一个不设置 ViewModel 的无参构造函数。
改了配置但地址没变? 检查实际运行目录的 appsettings.json,确认项目复制设置生效、程序重新构建,并检查环境变量是否覆盖 Api:BaseUrl。读取目录不是你打开终端时的目录,TaskDesk 使用 AppContext.BaseDirectory。
绑定没有效果
控件空白,但 ViewModel 有值。 先检查窗口的 DataContext,然后看 Visual Studio 调试输出中的绑定路径错误。绑定路径区分大小写,Title 和 title 不等价;DataTemplate 内通常绑定当前数据项,不能假设那里仍是窗口 ViewModel。参见 DataContext 和 模板。
修改属性后界面不刷新。 普通 CLR 属性没有自动通知。确认实现 INotifyPropertyChanged,或通过 Toolkit 生成属性;赋值应使用 NewTitle,不要直接改 newTitle。ObservableCollection<T> 通知行增删,不会自动让普通行对象通知属性变化。TaskDesk 用不可变 DTO,并在完成后替换该行。
输入后新增按钮还没启用。 TextBox.Text 默认常在失焦时写回源。TaskDesk 使用下面属性,片段适用于已有 NewTitle 属性和窗口 DataContext:
<TextBox Text="{Binding NewTitle, UpdateSourceTrigger=PropertyChanged}" />
命令或源生成器异常
找不到 AddCommand。 检查包是否还原、类是否 partial、方法是否标记 RelayCommand。AddAsync 生成 AddCommand,不是 AddAsyncCommand。生成的代码可以在 IDE 的分析器或生成文件视图查看;不要编辑生成文件。参见 Toolkit。
CanExecute 正确,但按钮没更新。 判断返回值变化还需要发送 CanExecuteChanged。TaskDesk 给输入、选择和忙碌字段设置 NotifyCanExecuteChangedFor。检查是否漏掉某个命令;异步命令自带防重入也不能代替四个命令共享的 IsBusy 门禁。
操作失败后所有按钮禁用。 在 RunOperationAsync 的入口、异常处理和 finally 下断点,确认 IsBusy 最终复位。不要在多个 catch 分支分别恢复状态,这样容易遗漏取消和响应解析异常。
窗口卡顿、取消或退出异常
请求时窗口无法移动。 搜索 UI 路径中的 .Result、.Wait()、Thread.Sleep 和长计算。HTTP 直接 await;只有真正耗时的 CPU 工作才考虑后台计算,结果再回到 UI 线程。不能用 Task.Run 盲目包住所有代码。
出现跨线程访问集合错误。 确认集合修改发生在 UI 线程。TaskDesk 的 ViewModel 保留同步上下文;服务层使用 ConfigureAwait(false) 是为了独立 I/O,不在那里修改列表。后台事件需要通过 Dispatcher 显式回到 UI。参见 异步线程。
取消了,任务却已经新增。 客户端取消不能撤销服务端提交。刷新核对结果,再决定重试。超时与用户取消分别显示:命令 token 已取消时属于用户取消,否则本例按 HTTP 超时提示。
窗口关闭但进程仍在。 查看 ShutdownMode、隐藏窗口、活动任务和 Host 停止流程。TaskDesk 设置显式退出,以便在 Closed 后等待 Host;如果移除了最终 Shutdown(),应用就不会结束。不要用强制结束进程代替正常清理。
API 错误
连接失败先在第二个终端独立查询;400 关注输入和验证响应;404 关注资源是否已删除;401/403 关注身份与授权。HTTPS 错误应修复证书或地址,不能禁用验证。WPF 的 HttpClient 无需为了桌面访问而配置浏览器 CORS。
Invoke-RestMethod http://localhost:5080/api/todos
Prism 启动、导航与对话框
Shell 能打开,但页面没有出现。 先查看 MainRegion 是否已经创建,再核对 RegisterForNavigation 的名称。PrismStudy 在 Shell 的 Loaded 中首次导航;模块初始化时不能假设 Shell 的 Region 已加载。给导航回调下断点,查看 Prism 9 的 Success、Exception 和 Cancelled。参见 Region 导航。
ViewModel 构造函数没执行或 DataContext 为空。 确认 prism:ViewModelLocator.AutoWireViewModel="True"、View 与 ViewModel 命名符合约定,或已经显式注册配对。继续检查构造依赖是否注册;不能只确认 ViewModel 类存在。PrismApplication 的 App.xaml 不设置 StartupUri。参见 启动与容器。
旧教程的 RequestClose 事件无法实现。 本站固定 Prism 9.0.537。它的 IDialogAware.RequestClose 是 DialogCloseListener 属性,关闭用 RequestClose.Invoke(...);监听器由对话框服务初始化。导航结果用 Success/Exception,对话框结果仍用 Result。不要跨版本拼接接口。参见 对话框。
修改设置后切换页面没有反应。 先观察未保存确认是否显示。IConfirmNavigationRequest 每个分支都要调用一次 continuationCallback;返回 false 会取消本次导航。PrismStudy 只在选择“放弃并离开”时返回 true;取消或关闭确认窗口会保留草稿。导航取消是预期交互,查看 Shell 的导航状态可以区分失败。
9.0.537 的 WPF Region 在确认返回 false 时,可能得到 Success=false、Exception=null、Cancelled=false。不要只根据 Cancelled 判断用户是否拒绝;本例先判断成功,再显示异常,最后处理无异常的未继续结果。
切换几次页面后,同一事件被处理多次。 搜索重复 Subscribe,确认不会每次 OnNavigatedTo 都累加订阅。KeepAlive=false 不会自动调用你写的 Dispose;谁持有订阅就要设计明确的退订时机。PrismStudy 的 Shell 保存 token,并在窗口关闭时退订。ThreadOption.UIThread 还要求 EventAggregator 最初在 UI 线程创建。参见 事件聚合。
练习与验收
临时将窗口的一处绑定改成不存在的属性,记录调试输出、控件现象与正确路径,然后恢复。验收:能指出错误来自绑定,不修改 API 即恢复。再模拟 API 停止,检查失败后 IsBusy 和按钮状态;记录真实结果。