Day 7 - 复盘与巩固
建议用时:120-150 分钟
你将学会什么
- 怎么把 Week 10 的内容串成项目主线
- 怎么检查需求有没有落到代码
- 怎么用一个流程验证新增、编辑、搜索、删除
- 怎么写本周项目验收清单
- 怎么判断主列表项目是否可以进入下一阶段
今天不新增控件。今天把 Week 10 做成一条完整路线:需求定义字段和规则,数据层负责存取,服务层负责校验,页面负责显示和交互,最后用验收清单判断是否完成。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“搜索与过滤”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“复盘与巩固”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。Week 10 的关键不是记住某个控件,而是理解一个项目从需求到页面怎么落地。
Week 10 的项目主线
| 日期 | 做了什么 | 作用 |
|---|---|---|
| Day 1 | 写需求范围 | 确定做什么、不做什么 |
| Day 2 | 写数据层 | 确定商品怎么保存和读取 |
| Day 3 | 写服务层 | 集中处理业务规则 |
| Day 4 | 写主列表页 | 显示多条,选中一条 |
| Day 5 | 写详情编辑 | 修改一条,并处理保存和取消 |
| Day 6 | 写搜索过滤 | 从全部商品里筛出可见商品 |
| Day 7 | 复盘收束 | 检查整条链路是否清楚 |
从需求到代码的对应关系
| 需求文档内容 | 代码位置 |
|---|---|
| 商品字段 | Product / ProductItem |
| 名称不能为空 | ProductService 或 SaveEdit |
| 价格必须大于 0 | ProductService 或 SaveEdit |
| 商品列表 | ObservableCollection + ListBox |
| 选中商品 | SelectedProduct |
| 编辑商品 | EditName、EditPriceText、EditStockText |
| 搜索商品 | SearchText + ApplyFilter |
| 空结果提示 | IsEmpty + EmptyMessage |
需求不是写完就放一边,它应该能直接指向代码。
一个项目功能的标准结构
本周每个功能都可以拆成 5 步:
- 输入:用户输入什么,或选中了什么。
- 校验:输入是否合法。
- 处理:新增、编辑、搜索、删除。
- 输出:列表变化、详情变化、提示变化。
- 失败:错误输入、未选中、无结果怎么处理。
例如编辑商品:
| 步骤 | 内容 |
|---|---|
| 输入 | EditName、EditPriceText、EditStockText |
| 校验 | 名称、价格、库存 |
| 处理 | 替换 Products[index] |
| 输出 | 列表刷新,提示保存成功 |
| 失败 | 显示错误提示,不保存 |
本周最容易混乱的点
| 混乱点 | 正确理解 |
|---|---|
| Repository 和 Service | Repository 管存取,Service 管规则 |
| Products 和 VisibleProducts | Products 是原数据,可见列表是过滤结果 |
| SelectedProduct 和编辑字段 | SelectedProduct 是原对象,编辑字段是草稿 |
| 搜索无结果 | 正常情况,不是异常 |
| 取消编辑 | 恢复草稿,不删除商品 |
为什么复盘要写可运行程序
只看页面容易觉得都懂。
可运行程序会强迫你把流程写完整:
- 成功输入要有结果。
- 错误输入要被拦住。
- 搜索要真的返回数量。
- 删除后数量要变化。
能跑出来,说明流程不是只停留在文字上。
Week 10 商品项目常用操作总表
| 层 | 必须会的操作 |
|---|---|
| 需求 | MVP、字段、业务规则、验收标准 |
| 数据层 | Repository、JSON 保存、CRUD |
| 服务层 | 请求对象、结果对象、业务校验 |
| 主列表 | ObservableCollection、SelectedProduct |
| 编辑 | 草稿字段、保存、取消 |
| 搜索 | 原始列表、可见列表、关键词过滤 |
| 错误处理 | ErrorMessage、ServiceResult.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检查:
- 合法新增成功。
- 空名称失败。
- 搜索
key得到 1 条。 - 仅显示有库存得到 1 条。
- 编辑成功。
- 搜索
pro得到 1 条。 - 删除成功。
- 删除后总数是 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
};
}
}验收结果
运行后必须看到:
- 合法新增成功。
- 空名称被拦住。
- 搜索
key得到 1 条。 - 仅显示有库存得到 1 条。
- 编辑
Keyboard为Keyboard Pro成功。 - 搜索
pro得到 1 条。 - 删除成功。
- 删除后总数是 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 页面,就能更清楚每个按钮应该调用哪一段逻辑。