Skip to Content
Week 10Day 7 - 复盘与巩固

Day 7 - 复盘与巩固

建议用时:120-150 分钟

你将学会什么

  • 怎么把 Week 10 的内容串成项目主线
  • 怎么检查需求有没有落到代码
  • 怎么用一个流程验证新增、编辑、搜索、删除
  • 怎么写本周项目验收清单
  • 怎么判断主列表项目是否可以进入下一阶段

今天不新增控件。今天把 Week 10 做成一条完整路线:需求定义字段和规则,数据层负责存取,服务层负责校验,页面负责显示和交互,最后用验收清单判断是否完成。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“搜索与过滤”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“复盘与巩固”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。Week 10 的关键不是记住某个控件,而是理解一个项目从需求到页面怎么落地。

Week 10 的项目主线

日期做了什么作用
Day 1写需求范围确定做什么、不做什么
Day 2写数据层确定商品怎么保存和读取
Day 3写服务层集中处理业务规则
Day 4写主列表页显示多条,选中一条
Day 5写详情编辑修改一条,并处理保存和取消
Day 6写搜索过滤从全部商品里筛出可见商品
Day 7复盘收束检查整条链路是否清楚

从需求到代码的对应关系

需求文档内容代码位置
商品字段Product / ProductItem
名称不能为空ProductServiceSaveEdit
价格必须大于 0ProductServiceSaveEdit
商品列表ObservableCollection + ListBox
选中商品SelectedProduct
编辑商品EditNameEditPriceTextEditStockText
搜索商品SearchText + ApplyFilter
空结果提示IsEmpty + EmptyMessage

需求不是写完就放一边,它应该能直接指向代码。

一个项目功能的标准结构

本周每个功能都可以拆成 5 步:

  1. 输入:用户输入什么,或选中了什么。
  2. 校验:输入是否合法。
  3. 处理:新增、编辑、搜索、删除。
  4. 输出:列表变化、详情变化、提示变化。
  5. 失败:错误输入、未选中、无结果怎么处理。

例如编辑商品:

步骤内容
输入EditNameEditPriceTextEditStockText
校验名称、价格、库存
处理替换 Products[index]
输出列表刷新,提示保存成功
失败显示错误提示,不保存

本周最容易混乱的点

混乱点正确理解
Repository 和 ServiceRepository 管存取,Service 管规则
Products 和 VisibleProductsProducts 是原数据,可见列表是过滤结果
SelectedProduct 和编辑字段SelectedProduct 是原对象,编辑字段是草稿
搜索无结果正常情况,不是异常
取消编辑恢复草稿,不删除商品

为什么复盘要写可运行程序

只看页面容易觉得都懂。

可运行程序会强迫你把流程写完整:

  • 成功输入要有结果。
  • 错误输入要被拦住。
  • 搜索要真的返回数量。
  • 删除后数量要变化。

能跑出来,说明流程不是只停留在文字上。

Week 10 商品项目常用操作总表

必须会的操作
需求MVP、字段、业务规则、验收标准
数据层Repository、JSON 保存、CRUD
服务层请求对象、结果对象、业务校验
主列表ObservableCollectionSelectedProduct
编辑草稿字段、保存、取消
搜索原始列表、可见列表、关键词过滤
错误处理ErrorMessageServiceResult.Fail

第二部分:把知识组合成完整例子

今天用一个控制台程序复盘商品管理项目主线。

它会模拟:

  • 新增合法商品。
  • 新增失败商品。
  • 编辑商品。
  • 搜索商品。
  • 只显示有库存商品。
  • 删除商品。

这个程序不替代 Avalonia 页面,它用来验证业务流程是否清楚。

完整 Program.cs

using System; using System.Collections.Generic; using System.Linq; var service = new ProductService(); ServiceResult keyboard = service.Create("Keyboard", "199", "10"); void Print(ServiceResult result) { Console.WriteLine($"{result.Success} - {result.Message}"); } Print(keyboard); ServiceResult emptyName = service.Create("", "199", "10"); Print(emptyName); ServiceResult monitor = service.Create("Monitor", "899", "0"); Print(monitor); Console.WriteLine($"搜索 key:{service.Search("key", false).Count} 条"); Console.WriteLine($"仅显示有库存:{service.Search("", true).Count} 条"); if (keyboard.ProductId is Guid keyboardId) { ServiceResult updated = service.Update(keyboardId, "Keyboard Pro", "209", "8"); Print(updated); } Console.WriteLine($"搜索 pro:{service.Search("pro", false).Count} 条"); if (keyboard.ProductId is Guid productId) { ServiceResult deleted = service.Delete(productId); Print(deleted); } Console.WriteLine($"删除后总数:{service.Search("", false).Count} 条"); public sealed class ProductService { private readonly List<Product> products = new(); public ServiceResult Create(string nameText, string priceText, string stockText) { ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } DateTime now = DateTime.Now; var product = new Product { Id = Guid.NewGuid(), Name = name, Price = price, Stock = stock, CreatedAt = now, UpdatedAt = now }; products.Add(product); return ServiceResult.Ok("商品新增成功。", product.Id); } public ServiceResult Update(Guid? id, string nameText, string priceText, string stockText) { if (id is null) { return ServiceResult.Fail("请先选择要编辑的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要编辑的商品不存在。"); } ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } product.Name = name; product.Price = price; product.Stock = stock; product.UpdatedAt = DateTime.Now; return ServiceResult.Ok("商品编辑成功。", product.Id); } public ServiceResult Delete(Guid? id) { if (id is null) { return ServiceResult.Fail("请先选择要删除的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要删除的商品不存在。"); } products.Remove(product); return ServiceResult.Ok("商品删除成功。"); } public List<Product> Search(string keyword, bool onlyInStock) { string trimmedKeyword = keyword.Trim(); IEnumerable<Product> query = products; if (!string.IsNullOrWhiteSpace(trimmedKeyword)) { query = query.Where(product => product.Name.Contains(trimmedKeyword, StringComparison.OrdinalIgnoreCase)); } if (onlyInStock) { query = query.Where(product => product.Stock > 0); } return query.ToList(); } private static ServiceResult? ValidateInput( string nameText, string priceText, string stockText, out string name, out decimal price, out int stock) { name = nameText.Trim(); price = 0; stock = 0; if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Fail("商品名称不能为空。"); } if (name.Length > 50) { return ServiceResult.Fail("商品名称不能超过 50 个字符。"); } if (!decimal.TryParse(priceText, out price)) { return ServiceResult.Fail("价格必须是数字。"); } if (price <= 0) { return ServiceResult.Fail("价格必须大于 0。"); } if (!int.TryParse(stockText, out stock)) { return ServiceResult.Fail("库存必须是整数。"); } if (stock < 0) { return ServiceResult.Fail("库存不能小于 0。"); } return null; } } public sealed class Product { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } public sealed class ServiceResult { public bool Success { get; init; } public string Message { get; init; } = ""; public Guid? ProductId { get; init; } public static ServiceResult Ok(string message, Guid? productId = null) { return new ServiceResult { Success = true, Message = message, ProductId = productId }; } public static ServiceResult Fail(string message) { return new ServiceResult { Success = false, Message = message }; } }

正常运行结果

True - 商品新增成功。 False - 商品名称不能为空。 True - 商品新增成功。 搜索 key:1 条 仅显示有库存:1 条 True - 商品编辑成功。 搜索 pro:1 条 True - 商品删除成功。 删除后总数:1 条

这段程序把本周核心流程压缩到一个文件里:输入、校验、创建、编辑、搜索、过滤、删除。

第三部分:跟着敲代码

从这里开始动手。今天用控制台项目做 Week 10 的流程复盘。

第 1 步:创建控制台项目

dotnet new console -n Week10Review cd Week10Review

打开 Program.cs,清空原内容。

第 2 步:写服务调用流程

var service = new ProductService(); ServiceResult keyboard = service.Create("Keyboard", "199", "10"); Print(keyboard); ServiceResult emptyName = service.Create("", "199", "10"); Print(emptyName); ServiceResult monitor = service.Create("Monitor", "899", "0"); Print(monitor);

解释:

  • 第一条是合法新增。
  • 第二条是错误输入。
  • 第三条是无库存商品,用来测试库存过滤。

第 3 步:写搜索和过滤验证

Console.WriteLine($"搜索 key:{service.Search("key", false).Count} 条"); Console.WriteLine($"仅显示有库存:{service.Search("", true).Count} 条");

解释:

  • 搜索 key 应该找到 Keyboard。
  • 仅显示有库存时,Monitor 不应该出现。

第 4 步:写编辑验证

if (keyboard.ProductId is Guid keyboardId) { ServiceResult updated = service.Update(keyboardId, "Keyboard Pro", "209", "8"); Print(updated); } Console.WriteLine($"搜索 pro:{service.Search("pro", false).Count} 条");

解释:

  • 编辑成功后,名称变成 Keyboard Pro
  • 再搜索 pro 应该能找到 1 条。

第 5 步:写删除验证

if (keyboard.ProductId is Guid productId) { ServiceResult deleted = service.Delete(productId); Print(deleted); } Console.WriteLine($"删除后总数:{service.Search("", false).Count} 条");

解释:

  • 删除 Keyboard Pro 后,只剩 Monitor。

第 6 步:写 ProductService

先写方法清单:

public sealed class ProductService { private readonly List<Product> products = new(); public ServiceResult Create(string nameText, string priceText, string stockText) { // 新增商品 } public ServiceResult Update(Guid? id, string nameText, string priceText, string stockText) { // 编辑商品 } public ServiceResult Delete(Guid? id) { // 删除商品 } public List<Product> Search(string keyword, bool onlyInStock) { // 搜索过滤 } }

解释:

  • 方法名对应业务动作。
  • 这就是 Week 10 的最小项目流程。

第 7 步:运行后检查

dotnet run

检查:

  1. 合法新增成功。
  2. 空名称失败。
  3. 搜索 key 得到 1 条。
  4. 仅显示有库存得到 1 条。
  5. 编辑成功。
  6. 搜索 pro 得到 1 条。
  7. 删除成功。
  8. 删除后总数是 1。

常见错误和修法

错误为什么错修法
复盘只写“学完了”看不出项目能力按需求、数据层、服务层、页面、搜索逐项检查
只检查能运行运行不代表规则正确用具体输入验证新增、编辑、删除、搜索
不记录未完成项后续无法继续改写清剩余功能、原因、下一步
不回看代码分层项目会越改越乱检查 View、ViewModel、Service、Repository 分工
没有重做练习只跟着敲容易遗忘关掉文档后独立重做一遍商品 MVP

小白重复敲写训练

复盘日重写数据层、服务层、界面层各一个最小闭环。

训练 1:数据层

public IReadOnlyList<Product> GetAll() => _items; public void Add(Product product) => _items.Add(product);

训练 2:服务层

public void Create(string name) { if (string.IsNullOrWhiteSpace(name)) return; _repository.Add(new Product { Name = name.Trim() }); }

训练 3:ViewModel

[RelayCommand] private void Add() { _service.Create(NewName); Load(); }

关掉答案,自己画出 View -> ViewModel -> Service -> Repository 的调用顺序。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:复盘与巩固。今天至少要掌握的 3 个点是:

1. 怎么把 Week 10 的内容串成项目主线 2. 怎么检查需求有没有落到代码 3. 怎么用一个流程验证新增、编辑、搜索、删除

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:怎么把 Week 10 的内容串成项目主线。

上位机专项练习

本周过关标准是从需求出发,打通模型、轮询、报警、仪表盘、详情和筛选。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:复习数据流

设备 -> IDeviceReader -> PollingService -> ReadingCache -> AlarmService -> ViewModel -> View

运行结果或界面效果:

每一步职责清楚

改动任务: 指出哪一步可以替换成模拟实现。

专项例子 2:复习报警闭环

double value = 85; double limit = 80; if (value >= limit) { Alarms.Add(new AlarmItem("PLC-01 高温", DateTime.Now)); }

运行结果或界面效果:

采集值越限后进入报警集合

改动任务: 增加报警恢复记录。

专项例子 3:复习页面验收

验收步骤: 1. 启动模拟采集 2. 首页数值更新 3. 制造高温值 4. 查看报警 5. 进入设备详情 6. 搜索 PLC-01

运行结果或界面效果:

形成可重复的手工验收流程

改动任务: 为每一步写预期结果。

第四部分:作业完整答案

作业要求:用一个可运行程序复盘 Week 10,覆盖新增、错误输入、编辑、搜索、库存过滤、删除。

答案文件:Program.cs

using System; using System.Collections.Generic; using System.Linq; var service = new ProductService(); ServiceResult keyboard = service.Create("Keyboard", "199", "10"); void Print(ServiceResult result) { Console.WriteLine($"{result.Success} - {result.Message}"); } Print(keyboard); ServiceResult emptyName = service.Create("", "199", "10"); Print(emptyName); ServiceResult monitor = service.Create("Monitor", "899", "0"); Print(monitor); Console.WriteLine($"搜索 key:{service.Search("key", false).Count} 条"); Console.WriteLine($"仅显示有库存:{service.Search("", true).Count} 条"); if (keyboard.ProductId is Guid keyboardId) { ServiceResult updated = service.Update(keyboardId, "Keyboard Pro", "209", "8"); Print(updated); } Console.WriteLine($"搜索 pro:{service.Search("pro", false).Count} 条"); if (keyboard.ProductId is Guid productId) { ServiceResult deleted = service.Delete(productId); Print(deleted); } Console.WriteLine($"删除后总数:{service.Search("", false).Count} 条"); public sealed class ProductService { private readonly List<Product> products = new(); public ServiceResult Create(string nameText, string priceText, string stockText) { ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } DateTime now = DateTime.Now; var product = new Product { Id = Guid.NewGuid(), Name = name, Price = price, Stock = stock, CreatedAt = now, UpdatedAt = now }; products.Add(product); return ServiceResult.Ok("商品新增成功。", product.Id); } public ServiceResult Update(Guid? id, string nameText, string priceText, string stockText) { if (id is null) { return ServiceResult.Fail("请先选择要编辑的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要编辑的商品不存在。"); } ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } product.Name = name; product.Price = price; product.Stock = stock; product.UpdatedAt = DateTime.Now; return ServiceResult.Ok("商品编辑成功。", product.Id); } public ServiceResult Delete(Guid? id) { if (id is null) { return ServiceResult.Fail("请先选择要删除的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要删除的商品不存在。"); } products.Remove(product); return ServiceResult.Ok("商品删除成功。"); } public List<Product> Search(string keyword, bool onlyInStock) { string trimmedKeyword = keyword.Trim(); IEnumerable<Product> query = products; if (!string.IsNullOrWhiteSpace(trimmedKeyword)) { query = query.Where(product => product.Name.Contains(trimmedKeyword, StringComparison.OrdinalIgnoreCase)); } if (onlyInStock) { query = query.Where(product => product.Stock > 0); } return query.ToList(); } private static ServiceResult? ValidateInput( string nameText, string priceText, string stockText, out string name, out decimal price, out int stock) { name = nameText.Trim(); price = 0; stock = 0; if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Fail("商品名称不能为空。"); } if (name.Length > 50) { return ServiceResult.Fail("商品名称不能超过 50 个字符。"); } if (!decimal.TryParse(priceText, out price)) { return ServiceResult.Fail("价格必须是数字。"); } if (price <= 0) { return ServiceResult.Fail("价格必须大于 0。"); } if (!int.TryParse(stockText, out stock)) { return ServiceResult.Fail("库存必须是整数。"); } if (stock < 0) { return ServiceResult.Fail("库存不能小于 0。"); } return null; } } public sealed class Product { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } public sealed class ServiceResult { public bool Success { get; init; } public string Message { get; init; } = ""; public Guid? ProductId { get; init; } public static ServiceResult Ok(string message, Guid? productId = null) { return new ServiceResult { Success = true, Message = message, ProductId = productId }; } public static ServiceResult Fail(string message) { return new ServiceResult { Success = false, Message = message }; } }

验收结果

运行后必须看到:

  1. 合法新增成功。
  2. 空名称被拦住。
  3. 搜索 key 得到 1 条。
  4. 仅显示有库存得到 1 条。
  5. 编辑 KeyboardKeyboard Pro 成功。
  6. 搜索 pro 得到 1 条。
  7. 删除成功。
  8. 删除后总数是 1。

Week 10 最终检查清单

需求: 1. 第一版范围明确。 2. 暂不做内容明确。 3. 字段和规则明确。 数据层: 1. Product 字段完整。 2. Repository 能新增、查询、修改、删除。 3. JSON 保存和读取能跑通。 服务层: 1. 名称、价格、库存有校验。 2. 成功和失败都有结果对象。 3. 删除前检查是否选中。 页面: 1. 列表能显示商品。 2. 选中商品能显示详情。 3. 编辑表单使用草稿字段。 4. 保存后列表刷新。 5. 取消后恢复原数据。 6. 搜索和过滤不会破坏原数据。

为什么这个答案是对的

这个答案把 Week 10 的主线收束成一个完整流程:

项目能力程序里的体现
需求规则名称、价格、库存校验
数据模型Product
服务层ProductService
返回结果ServiceResult
编辑Update
搜索过滤Search
删除Delete

能把这段程序跑通,再回到 Avalonia 页面,就能更清楚每个按钮应该调用哪一段逻辑。