跳到主要内容

9.3 练习答案与学习资源

学习目标​

  • 对照验收标准解释自己的实现,而不是只核对界面截图。
  • 将基础练习串联到 TaskDesk 的真实代码路径。
  • 根据具体问题选择官方资料,形成持续学习方法。

如何使用答案​

下面给出各阶段典型练习的参考思路,不要求变量名和布局完全一致。先完成原章节练习,再展开解法;重点检查状态来源、通知时机、线程和错误恢复。只有代码或截图不足以证明请求取消、状态恢复和关闭清理正确。

基础与布局:为什么列表区要使用星号行?

标题、输入和按钮通常用 Auto 根据内容分配高度,列表使用 * 获得剩余空间。固定高度会在窗口缩放时留下空白或遮挡列表;垂直 StackPanel 不适合承担需要明确剩余高度的整个页面。TaskDesk 的窗口采用四行 Grid,第三行是星号。验收应改变窗口大小,并观察列表随之伸缩,按钮与状态栏仍可见。

复习:布局容器、常用控件。

绑定与通知:修改值后为什么界面还没变化?

先确认正确的 DataContext 和绑定路径,再确认属性通知;不能只更改私有字段。下面是无需 Toolkit 的完整计数 ViewModel,适用于已设置其为 DataContext、绑定 Count 的 WPF 窗口。调用 Increment() 后会通知界面,但该类尚未提供 ICommand,按钮事件或后续命令负责调用它。

ViewModels/CounterViewModel.cs:完整参考类
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TaskDesk.Client.ViewModels;

public sealed class CounterViewModel : INotifyPropertyChanged
{
private int _count;
public int Count
{
get => _count;
private set
{
if (_count == value) return;
_count = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;
public void Increment() => Count++;
private void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

集合练习还要区分“增加行”与“改行属性”。ObservableCollection<T> 负责前者;后者由数据项通知,或像 TaskDesk 一样替换不可变 DTO。输入实时校验需要按字符更新绑定;服务器仍要验证标题,UI 验证不能代替服务端规则。

复习:属性通知、转换器与校验。

MVVM 与命令:按钮状态怎样跟输入和选择同步?

CanExecute 读取当前状态,状态变化时发送 CanExecuteChanged。TaskDesk 中标题变化通知 AddCommand,选择变化通知 CompleteCommand 和 DeleteCommand,忙碌变化通知所有操作及取消。完成后替换 DTO 并更新选择,使完成按钮重新判断为不可执行。

源生成器的名称规则也要正确:newTitle 生成 NewTitle;返回 Task 的 AddAsync 生成 AddCommand。类需要 partial,文件需要引用 Toolkit 命名空间。验收不仅点击成功,还要观察空输入、已完成选择和忙碌时按钮的状态。

导航或对话框练习则应由 ViewModel 请求一个抽象服务,由 UI 实现真正打开窗口;业务层不直接创建 Window。复习:MVVM 分层、Toolkit、导航与对话框。

样式与模板:统一按钮外观与修改行显示应放哪里?

统一按钮间距、字体和颜色适合 Style;描述一个任务项的视觉结构适合 DataTemplate;重做控件内部结构才考虑 ControlTemplate。资源范围应与复用范围一致:仅一个窗口使用放在窗口,多页面共享考虑应用资源或字典。不要为了显示一行任务重做 DataGrid 的整体模板。

验收:多个按钮使用同一样式,修改资源后同步改变;模板中的绑定源确实是当前任务项。复习:资源样式、模板、集合控件。

异步与生命周期:请求失败、取消和关闭怎样验收?

HTTP 直接 await,绑定集合在 UI 线程更新,忙碌恢复放进 finally。取消 token 需要经过命令、服务、HTTP 和内容读取;只在 UI 改成“已取消”不是真正取消。服务内部不捕获 UI 上下文,也不修改 UI。

客户端 token 已取消时显示取消,未取消但 HTTP 等待被中止时本例按超时提示。写操作可能已经提交,先刷新核对。窗口关闭先取消、等待命令,再关闭窗口;App 最后停止 Host 并显式 Shutdown。实际验收按 联调步骤 暂时加入慢 GET,而不是只检查一条快速请求。

桌面没有自动请求作用域,弹窗若创建 scope 就要在结束时释放。不要在客户端配置里嵌入服务端密钥。复习:依赖注入、HTTP 与认证。

实战:不用客户端如何新增并核对服务器结果?

在仓库根目录启动 API 后执行下面命令。UTF-8 字节明确中文请求编码;首尾空格应由服务端去掉。

$payload = @{ title = ' 学习绑定 ' } | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
$created = Invoke-RestMethod -Uri http://localhost:5080/api/todos -Method Post -ContentType 'application/json; charset=utf-8' -Body $bytes
$created
Invoke-RestMethod http://localhost:5080/api/todos

验收:获得有效 ID,标题为“学习绑定”,完成状态为 false,列表出现同一 ID。选中项属于窗口状态,不应加入列表 GET 的契约。完整的 400/404 检查见本地 examples/TaskDesk/Verify-Api.ps1。

Prism:未保存离开和任务事件怎样验证?

先运行 PrismStudy。设置页修改名称后切换到任务页,取消确认必须留在设置页并保留草稿;确认放弃必须恢复已保存值,随后返回设置页也不能出现旧草稿。仅观察弹窗出现不足以证明导航回调和状态恢复正确。

新增任务时,TasksViewModel 先调用任务服务并更新列表,再发布 TaskAddedEvent。Shell 通过订阅更新最后消息和新增计数,不需要持有 TasksViewModel。切换页面后再新增,计数每次只加一;关闭 Shell 时释放订阅 token。事件是通知,不能用它替代需要结果的保存请求。

模块练习可以增加独立报表 View 和 ViewModel,在模块的 RegisterTypes 中注册导航名,再让 Shell 的命令导航到它。页面导航应发生在 Region 存在后。无需立即拆成多个项目;先证明注册与初始化职责清楚。复习:Region、事件聚合、模块。

常见问题​

和参考解法不同是否错误? 只要责任清楚并满足验收,替代实现可以成立。例如可编辑模型实现属性通知也是合理方案,但必须再设计编辑、保存、取消和失败回滚,不能只解除只读限制。

所有练习做完后学什么? 先给 TaskDesk 加可控服务测试和持久化,再学习认证、冲突和发布。遇到机制问题先读官方概述与 API 文档,用最小示例确认;不必先换一套框架。

官方资源与后续练习​

资源适合解决的问题
WPF 文档控件、绑定、资源、线程、生命周期
MVVM Toolkit属性、命令、验证与消息机制
Prism 9 文档容器、Region、对话框、事件和模块
Prism.DryIoc 9.0.537核对本站的依赖版本与包元数据
HttpClient 指南长期连接、连接池、DNS 与寿命
桌面身份验证场景公开客户端、交互登录与令牌
.NET 应用发布发布模式、运行时依赖与分发

选择一项后续练习:用 ITodoService 测试桩模拟延迟和失败,或增加数据库持久化。验收记录必须包含正常与失败场景,并能解释请求完成后 UI 状态为何正确恢复。测试设计与发布注意事项继续参考 调试、测试与发布。