Day 7 - 复盘与巩固
建议用时:180-210 分钟
你将学会什么
- 把 Week 6 的知识连成一条完整开发路线。
- 能说清
App.axaml、MainWindow.axaml、MainWindow.axaml.cs分别负责什么。 - 能从 XAML 的按钮点击追踪到 C# 事件方法。
- 能解释布局、控件、资源样式、表单校验、列表详情之间的关系。
- 能重新敲出一个商品管理窗口,并完成基本排错。
复盘不是把前 6 天标题重新看一遍。复盘要能回答三件事:这个知识解决什么问题、代码放在哪个文件、出错时先查哪里。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“列表 + 详情”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“复盘与巩固”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分把 Week 6 的知识重新排成路线。先看清“每个知识点用来解决什么问题”,再去第三部分动手。
1. 本周知识地图
| 日期 | 主题 | 解决的问题 | 最重要的判断 |
|---|---|---|---|
| Day 1 | 项目结构 | 知道项目怎么启动 | 入口在 Program.cs,窗口在 MainWindow.axaml |
| Day 2 | XAML 布局 | 控件放在哪里 | 简单排队用 StackPanel,行列结构用 Grid |
| Day 3 | 常用控件 | 用户看、输、点、选 | 显示用 TextBlock,输入用 TextBox,动作靠 Button |
| Day 4 | 资源与样式 | 重复外观集中管理 | 重复的值提资源,重复的外观提样式 |
| Day 5 | 表单页面 | 输入、校验、反馈 | 保存前先校验,不合格就停止 |
| Day 6 | 列表详情 | 多条数据和当前一条数据 | ItemsSource 是数据来源,SelectedItem 是当前项 |
2. 文件分别负责什么
| 文件 | 负责什么 | 不应该负责什么 |
|---|---|---|
Program.cs | 启动 Avalonia 应用 | 具体页面控件 |
App.axaml | 应用级资源和主题 | 某个按钮点击逻辑 |
MainWindow.axaml | 当前窗口的结构、布局、控件 | 复杂业务规则 |
MainWindow.axaml.cs | 当前窗口的事件处理 | 大量跨页面业务逻辑 |
ViewModels/*.cs | 后面学习 MVVM 时放界面状态和命令 | 当前周还不强行使用 |
Week 6 先用 MainWindow.axaml.cs 处理事件,是为了看清控件和 C# 代码如何连接。后面迁移到 ViewModel 时,处理流程不变,只是代码位置更清晰。
3. XAML 和 C# 如何连接
XAML:
<Button Content="保存"
Click="OnSaveClick" />C#:
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ResultText.Text = "已保存。";
}这就是连接点:
用户点击按钮
-> Avalonia 找到 Click 指定的方法
-> 执行 OnSaveClick
-> C# 修改界面控件如果按钮点击没有反应,先查这三件事:
- XAML 里有没有写
Click="OnSaveClick"。 - C# 方法名是不是也叫
OnSaveClick。 - C# 方法参数是不是
object? sender, RoutedEventArgs e。
4. 布局怎么选
| 场景 | 推荐容器 | 原因 |
|---|---|---|
| 几个控件从上到下排 | StackPanel | 简单直接 |
| 标签和输入框左右对齐 | Grid | 行列清楚 |
| 顶部、左侧、主内容区 | DockPanel 或 Grid | 适合页面骨架 |
| 一块内容要边框和内边距 | Border | 区域感清楚 |
不要一开始就想“这个按钮写在哪里”。先想页面大区域,再想每个区域内部怎么排。
5. 表单保存为什么必须校验
表单里的输入都是用户输入的文本,程序不能直接相信。
例如价格输入框可能出现:
空
abc
-1
0
199
199.00所以保存顺序必须是:
读取文本
-> 去掉前后空格
-> 必填校验
-> 数字解析
-> 范围校验
-> 保存
-> 显示结果只要中间某一步失败,就 return 停止,不要继续保存。
6. 列表详情为什么要判断 SelectedItem
ListBox.SelectedItem 可能没有值。比如列表为空,或者用户取消选择。
所以要先写:
Product? product = ProductList.SelectedItem as Product;
if (product is null)
{
ResultText.Text = "请先选择一个商品。";
return;
}这不是多余代码。它是在防止“没有选中任何东西,却继续读取商品字段”的错误。
7. 本周最常见错误和直接修法
| 现象 | 常见原因 | 直接修法 |
|---|---|---|
| XAML 编译报错 | 标签没闭合或属性写错 | 先看第一条错误的行号 |
| 点击按钮没反应 | Click 方法名不一致 | 让 XAML 和 C# 方法名完全一致 |
| C# 找不到控件名 | XAML 没有 x:Name | 给需要读取的控件加 x:Name |
| 列表显示类型名 | 对象没重写 ToString() | 在 Product 里重写 ToString() |
| 价格输入字母后崩掉 | 直接转换数字 | 改用 decimal.TryParse |
| 保存后列表不刷新 | 普通 List<T> 不自动通知界面 | 重新设置一次 ItemsSource |
Week 6 Avalonia 常用写法总表
| 主题 | 常用写法 |
|---|---|
| 创建项目 | dotnet new avalonia.mvvm |
| 运行窗口 | dotnet run |
| 布局 | StackPanel、Grid、DockPanel |
| 输入控件 | TextBox Text="{Binding ..., Mode=TwoWay}" |
| 按钮命令 | Button Command="{Binding SaveCommand}" |
| 列表 | ListBox ItemsSource="{Binding Products}" |
| 选中项 | SelectedItem="{Binding SelectedProduct}" |
| 列表刷新 | ObservableCollection<T> |
| 错误提示 | ErrorMessage 绑定到 TextBlock |
| 空状态 | IsVisible="{Binding IsEmpty}" |
| 样式资源 | StaticResource、Classes |
Week 6 重点不是把界面做复杂,而是把 XAML、绑定、命令、列表和详情这条线跑通。
第二部分:把知识组合成完整例子
Week 6 的目标不是只会写一个按钮,而是能写出一个小型桌面窗口:左侧列表、右侧详情、能选择、能编辑、能校验、能保存反馈。
本周最终闭环
Avalonia 项目启动
-> MainWindow.axaml 描述窗口结构
-> Grid / StackPanel / Border 摆布局
-> TextBox / Button / ListBox 做输入和操作
-> Click / SelectionChanged 连接到 C# 方法
-> C# 读取控件值
-> C# 校验数据
-> C# 修改当前商品
-> 界面显示结果先看本周综合效果
综合窗口包含:
- 顶部标题。
- 左侧商品列表。
- 右侧商品详情表单。
- 保存按钮。
- 错误或成功提示。
真正要记住的是:界面不是孤立控件,而是一条从用户操作到 C# 处理,再回到界面反馈的数据流。
第三部分:跟着敲代码
从这里开始动手。今天只做两个练习:先跑通按钮事件链,再跑通本周综合窗口。
例子 1:按钮事件链复盘
这个例子用最短路径复习 XAML 和 C# 的连接。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="420"
Height="240"
Title="事件链复盘">
<StackPanel Margin="20"
Spacing="12">
<TextBlock Text="点击按钮,C# 会修改下面的文字。" />
<Button Content="保存"
Click="OnSaveClick" />
<TextBlock x:Name="ResultText"
Text="还没有点击。" />
</StackPanel>
</Window>MainWindow.axaml.cs
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
ResultText.Text = "按钮点击事件已经执行。";
}
}运行后你要能说清:
Button 的 Click 找到 OnSaveClick
-> OnSaveClick 修改 ResultText.Text
-> 界面文字变化例子 2:本周综合窗口
这个练习把布局、控件、资源样式、表单校验、列表详情合在一起。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="780"
Height="480"
Title="Week 6 综合练习">
<Window.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#DDDDDD" />
</Window.Resources>
<Window.Styles>
<Style Selector="TextBlock.title">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}" />
</Style>
<Style Selector="Border.card">
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="12" />
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
</Style>
</Window.Styles>
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="230,*"
RowSpacing="12"
ColumnSpacing="16"
Margin="20">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Classes="title"
Text="商品管理综合练习" />
<Border Grid.Row="1"
Grid.Column="0"
Classes="card">
<StackPanel Spacing="8">
<TextBlock Text="商品列表"
FontWeight="Bold" />
<ListBox x:Name="ProductList"
SelectionChanged="OnProductSelectionChanged" />
</StackPanel>
</Border>
<Border Grid.Row="1"
Grid.Column="1"
Classes="card">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="商品详情"
FontSize="18"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="名称"
VerticalAlignment="Center" />
<TextBox x:Name="NameBox"
Grid.Row="1"
Grid.Column="1" />
<TextBlock Grid.Row="2"
Grid.Column="0"
Text="价格"
VerticalAlignment="Center" />
<TextBox x:Name="PriceBox"
Grid.Row="2"
Grid.Column="1" />
<TextBlock Grid.Row="3"
Grid.Column="0"
Text="分类"
VerticalAlignment="Center" />
<TextBox x:Name="CategoryBox"
Grid.Row="3"
Grid.Column="1" />
<TextBlock Grid.Row="4"
Grid.Column="0"
Text="状态"
VerticalAlignment="Center" />
<CheckBox x:Name="EnabledBox"
Grid.Row="4"
Grid.Column="1"
Content="启用商品" />
<Button Grid.Row="5"
Grid.Column="1"
Classes="primary"
Content="保存当前商品"
HorizontalAlignment="Right"
Click="OnSaveClick" />
<TextBlock x:Name="ResultText"
Grid.Row="6"
Grid.Column="0"
Grid.ColumnSpan="2"
TextWrapping="Wrap"
Text="请选择左侧商品。" />
</Grid>
</Border>
</Grid>
</Window>MainWindow.axaml.cs
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
private readonly List<Product> _products = new()
{
new Product { Name = "Keyboard", Price = 199, Category = "普通商品", Enabled = true },
new Product { Name = "Mouse", Price = 99, Category = "普通商品", Enabled = true },
new Product { Name = "Online Course", Price = 299, Category = "虚拟商品", Enabled = false }
};
public MainWindow()
{
InitializeComponent();
ProductList.ItemsSource = _products;
ProductList.SelectedIndex = 0;
}
private void OnProductSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
Product? product = ProductList.SelectedItem as Product;
if (product is null)
{
ClearDetail();
ResultText.Text = "没有选中商品。";
return;
}
ShowProduct(product);
}
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
Product? product = ProductList.SelectedItem as Product;
if (product is null)
{
ResultText.Text = "请先选择一个商品。";
return;
}
string name = NameBox.Text?.Trim() ?? "";
if (name.Length == 0)
{
ResultText.Text = "商品名称不能为空。";
return;
}
string priceText = PriceBox.Text?.Trim() ?? "";
if (!decimal.TryParse(priceText, out decimal price))
{
ResultText.Text = "价格必须是数字。";
return;
}
if (price <= 0)
{
ResultText.Text = "价格必须大于 0。";
return;
}
product.Name = name;
product.Price = price;
product.Category = CategoryBox.Text?.Trim() ?? "";
product.Enabled = EnabledBox.IsChecked == true;
int selectedIndex = ProductList.SelectedIndex;
ProductList.ItemsSource = null;
ProductList.ItemsSource = _products;
ProductList.SelectedIndex = selectedIndex;
ResultText.Text = $"已保存:{product.Name}";
}
private void ShowProduct(Product product)
{
NameBox.Text = product.Name;
PriceBox.Text = product.Price.ToString();
CategoryBox.Text = product.Category;
EnabledBox.IsChecked = product.Enabled;
ResultText.Text = $"当前选中:{product.Name}";
}
private void ClearDetail()
{
NameBox.Text = "";
PriceBox.Text = "";
CategoryBox.Text = "";
EnabledBox.IsChecked = false;
}
private sealed class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public string Category { get; set; } = "";
public bool Enabled { get; set; }
public override string ToString()
{
return $"{Name} - {Price}";
}
}
}例子 3:故意制造错误并修复
复盘时要主动练排错。下面这些错误都很常见。
错误 1:按钮方法名不一致
错误写法:
<Button Content="保存" Click="SaveClick" />C# 实际方法:
private void OnSaveClick(object? sender, RoutedEventArgs e)
{
}修法:让 XAML 和 C# 名字完全一致。
<Button Content="保存" Click="OnSaveClick" />错误 2:控件没有名字
错误写法:
<TextBox />C# 却写:
string name = NameBox.Text ?? "";修法:XAML 里补上 x:Name。
<TextBox x:Name="NameBox" />错误 3:价格直接转换
错误写法:
decimal price = decimal.Parse(PriceBox.Text);用户输入 abc 时,程序会抛异常。
修法:用 TryParse。
if (!decimal.TryParse(PriceBox.Text, out decimal price))
{
ResultText.Text = "价格必须是数字。";
return;
}错误 4:没有判断选中项
错误写法:
Product product = (Product)ProductList.SelectedItem!;
NameBox.Text = product.Name;如果没有选中项,这段代码会出问题。
修法:先判断。
Product? product = ProductList.SelectedItem as Product;
if (product is null)
{
ResultText.Text = "请先选择一个商品。";
return;
}小白重复敲写训练
复盘日从三个空窗口片段重新敲,不看完整答案。
训练 1:表单布局
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8">
<TextBlock Text="名称" />
<TextBox Grid.Column="1" />
<Button Grid.Row="1" Grid.Column="1" Content="保存" />
</Grid>训练 2:统一按钮样式
<Style Selector="Button.action">
<Setter Property="Padding" Value="16,8" />
<Setter Property="Background" Value="#2563EB" />
</Style>训练 3:按钮点击闭环
private void Save_Click(object? sender, RoutedEventArgs e)
{
MessageText.Text = string.IsNullOrWhiteSpace(NameInput.Text)
? "请输入名称"
: $"已保存 {NameInput.Text.Trim()}";
}把三个训练组合到一个窗口,运行成功后再关页面独立重写。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:复盘与巩固。今天至少要掌握的 3 个点是:
1. 把 Week 6 的知识连成一条完整开发路线。
2. 能说清 `App.axaml`、`MainWindow.axaml`、`MainWindow.axaml.cs` 分别负责什么。
3. 能从 XAML 的按钮点击追踪到 C# 事件方法。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:把 Week 6 的知识连成一条完整开发路线。。
上位机专项练习
本周过关标准是能独立搭窗口、分区布局、使用常用控件并完成设备配置页。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:复习窗口骨架
<Grid RowDefinitions="Auto,*" Margin="20">
<TextBlock Text="上位机控制台" FontSize="28" />
<ContentControl Grid.Row="1" Margin="0,16,0,0" />
</Grid>运行结果或界面效果:
上方标题,下方内容区改动任务: 增加底部状态栏。
专项例子 2:复习监控卡片
<Border Padding="16" CornerRadius="8" Background="#F5F7FA">
<StackPanel>
<TextBlock Text="PLC-01" FontWeight="Bold" />
<TextBlock Text="在线" Foreground="Green" />
<TextBlock Text="温度 26.8 C" />
</StackPanel>
</Border>运行结果或界面效果:
一张设备状态卡改动任务: 再复制一张 PLC-02 卡片并显示报警。
专项例子 3:复习点击事件
private void Start_Click(object? sender, RoutedEventArgs e)
{
StatusText.Text = "正在采集";
StartButton.IsEnabled = false;
StopButton.IsEnabled = true;
}运行结果或界面效果:
点击开始后状态和按钮可用性一起改变改动任务: 补写 Stop_Click,恢复两个按钮。
第四部分:作业完整答案
这一部分给出本周复盘作业的完整答案。代码答案就是第三部分的“本周综合窗口”,下面补齐概念题、排错题和验收答案。
作业 1:写出 Week 6 主线
完整答案:
Week 6 主线:
1. 创建 Avalonia 项目,知道应用怎么启动。
2. 用 XAML 描述窗口结构。
3. 用 Grid、StackPanel、Border 管理布局。
4. 用 TextBlock、TextBox、Button、ListBox 做显示、输入、点击、选择。
5. 用 Click 和 SelectionChanged 把用户操作连接到 C# 方法。
6. 在 C# 方法里读取控件值,做校验。
7. 校验通过后修改数据。
8. 最后把处理结果显示回界面。作业 2:解释 8 个关键词
完整答案:
| 关键词 | 解释 |
|---|---|
Window | 一个桌面窗口,里面放布局和控件 |
Grid | 按行列切分空间的布局容器 |
StackPanel | 按水平或垂直方向排列子控件的布局容器 |
TextBox | 接收用户文本输入的控件 |
Button | 触发动作的控件 |
Click | 按钮点击时触发的事件 |
ItemsSource | ListBox 要显示的数据来源 |
SelectedItem | ListBox 当前选中的那一项 |
作业 3:写出保存按钮的数据流
完整答案:
用户点击保存按钮
-> Avalonia 执行 OnSaveClick
-> C# 从 NameBox、PriceBox、CategoryBox、EnabledBox 读取值
-> 名称做空值校验
-> 价格用 decimal.TryParse 解析
-> 价格做大于 0 校验
-> 读取当前 ProductList.SelectedItem
-> 把输入值写回当前 Product
-> 刷新 ProductList.ItemsSource
-> ResultText 显示保存结果作业 4:排错题完整答案
| 现象 | 原因 | 修法 |
|---|---|---|
OnSaveClick 找不到 | XAML 的 Click 名字和 C# 方法名不一致 | 保持完全一致 |
NameBox 找不到 | TextBox 没写 x:Name="NameBox" | 补上 x:Name |
RoutedEventArgs 找不到 | 缺少命名空间 | 添加 using Avalonia.Interactivity; |
| 列表显示类型名 | Product 没有重写 ToString() | 添加 public override string ToString() |
输入 abc 后价格处理失败 | 没用 TryParse | 改成 decimal.TryParse |
| 保存后列表文字没变化 | 普通 List<Product> 不自动通知界面 | 保存后重新设置 ItemsSource |
作业 5:验收清单完整答案
你的 Week 6 综合窗口必须满足:
- 项目能启动,没有 XAML 编译错误。
- 左侧列表显示三个商品。
- 启动后默认选中第一个商品。
- 点击左侧其他商品,右侧详情跟着变化。
- 名称清空后点击保存,显示
商品名称不能为空。。 - 价格输入
abc后点击保存,显示价格必须是数字。。 - 价格输入
0后点击保存,显示价格必须大于 0。。 - 修改名称和价格后保存,左侧列表显示新名称和新价格。
- 你能说清
MainWindow.axaml负责界面,MainWindow.axaml.cs负责事件处理。 - 你能从一个按钮点击追踪到 C# 方法执行。
本周真正要带走的能力
Week 6 学完后,应该能做到:
看到一个桌面窗口需求
-> 先拆页面区域
-> 选择布局容器
-> 放入控件
-> 给需要读取的控件命名
-> 给按钮或列表选择加事件
-> 在 C# 中读取输入
-> 做必要校验
-> 更新界面反馈这条路线比单独记住某个控件属性更重要。