Day 6 - 列表 + 详情
建议用时:240-270 分钟
你将学会什么
- 会用
ListBox显示一组商品。 - 知道
ItemsSource是列表的数据来源。 - 知道
SelectedItem是当前选中的那一项。 - 会处理
SelectionChanged事件。 - 会把左侧选中的商品显示到右侧详情区。
- 会修改右侧输入框,再把结果保存回当前商品。
本页先不用数据绑定做列表详情,而是用 code-behind 把数据流走一遍:列表数据从 C# 来,选中项从 ListBox 来,详情显示由 C# 更新。这样能先看清底层流程。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“表单页面”,今天继续学习“列表 + 详情”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“列表 + 详情”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。列表详情页的重点是“多条数据”和“当前一条数据”的关系。
1. 列表 + 详情解决什么问题
很多软件都有这种结构:
- 左边显示很多条数据。
- 用户点一条。
- 右边显示这一条的详细信息。
- 用户可以编辑右边内容。
- 保存后左边列表同步更新。
商品管理、客户管理、订单管理、文件管理都能用这个结构。
2. ListBox 是什么
ListBox 用来显示一组数据,并允许用户选择其中一项。
它有两个重要概念:
| 名称 | 含义 |
|---|---|
ItemsSource | 列表要显示的数据来源 |
SelectedItem | 当前选中的那一项 |
代码里这样写:
ProductList.ItemsSource = _products;意思是:让 ProductList 这个列表显示 _products 里面的数据。
3. _products 是什么
private readonly List<Product> _products = new()
{
new Product { Name = "Keyboard", Price = 199, Category = "普通商品", Enabled = true },
new Product { Name = "Mouse", Price = 99, Category = "普通商品", Enabled = true }
};_products 是一个 C# 列表,里面放着多个 Product 对象。每个 Product 对象代表一个商品。
ListBox 不负责保存业务数据。它只是把 _products 显示出来。
4. Product 类为什么要重写 ToString
ListBox 显示对象时,如果没有专门告诉它怎么显示,它会调用对象的 ToString()。
所以写:
public override string ToString()
{
return $"{Name} - {Price}";
}这样列表里看到的是:
Keyboard - 199
Mouse - 99如果不重写,列表可能显示类似 ProductApp.Views.MainWindow+Product 这种类型名称。
5. SelectionChanged 是什么
当用户选择列表中的另一项时,SelectionChanged 事件会触发。
XAML:
<ListBox x:Name="ProductList"
SelectionChanged="OnProductSelectionChanged" />C#:
private void OnProductSelectionChanged(object? sender, SelectionChangedEventArgs e)这表示:列表选中项变化时,执行 OnProductSelectionChanged 方法。
6. SelectedItem 为什么要判断类型
ProductList.SelectedItem 的类型是 object?,因为 ListBox 可以显示任何类型的数据。
所以要写:
Product? product = ProductList.SelectedItem as Product;这句话的意思是:尝试把选中项当成 Product。如果选中的确是商品,就得到一个 Product;如果没有选中或类型不对,就得到 null。
接着必须判断:
if (product is null)
{
ClearDetail();
ResultText.Text = "没有选中商品。";
return;
}没有选中项时不能继续读取 product.Name,否则会报错。
7. 详情区为什么不是自动更新
本页没有使用绑定,所以右侧详情区不会自动知道左侧选中了什么。必须由 C# 代码主动把商品数据填进去:
private void ShowProduct(Product product)
{
NameBox.Text = product.Name;
PriceBox.Text = product.Price.ToString();
CategoryBox.Text = product.Category;
EnabledBox.IsChecked = product.Enabled;
}这一步就是把一个 Product 对象拆开,放到右侧控件里。
8. 保存时为什么要改回 product
右侧输入框只是临时编辑区。用户改了输入框,不等于 _products 里的商品已经变了。
保存时要把输入框的值写回当前商品:
product.Name = name;
product.Price = price;
product.Category = CategoryBox.Text?.Trim() ?? "";
product.Enabled = EnabledBox.IsChecked == true;这一步完成后,内存里的商品对象才真正被修改。
9. 为什么保存后要刷新 ListBox
本页使用普通 List<Product>。修改对象后,ListBox 不一定马上重新显示最新的 ToString() 结果。
所以保存后做一次简单刷新:
int selectedIndex = ProductList.SelectedIndex;
ProductList.ItemsSource = null;
ProductList.ItemsSource = _products;
ProductList.SelectedIndex = selectedIndex;这三句做了什么:
- 记住当前选中的位置。
- 先断开列表数据。
- 再重新绑定列表数据。
- 恢复原来的选中位置。
以后学 ObservableCollection 和数据绑定后,刷新方式会更优雅。本页先保证数据流能看清楚。
10. 列表详情页的稳定顺序
写列表详情时按这个顺序:
- 先准备数据列表。
- 把列表放进
ListBox.ItemsSource。 - 处理选中事件。
- 选中后显示详情。
- 保存前校验详情输入。
- 校验通过后写回当前对象。
- 刷新列表显示。
不要一开始就把新增、删除、搜索、排序全加进去。先把“选中一条并编辑保存”走通。
列表和详情常用绑定速查
| 需求 | XAML 写法 | ViewModel 写法 |
|---|---|---|
| 显示列表 | ItemsSource="{Binding Products}" | ObservableCollection<ProductItem> |
| 选中项 | SelectedItem="{Binding SelectedProduct}" | SelectedProduct 属性 |
| 列表模板 | ListBox.ItemTemplate | 显示每项字段 |
| 详情名称 | Text="{Binding SelectedProduct.Name}" | 选中项不为空时显示 |
| 删除按钮 | Command="{Binding DeleteCommand}" | 删除选中项 |
| 空列表提示 | IsVisible="{Binding IsEmpty}" | Products.Count == 0 |
常见错误:
ItemsSource 绑定错 -> 列表不显示
SelectedItem 没绑定 -> 详情不知道选中谁
SelectedProduct 为 null -> 删除前要判断
修改 ObservableCollection -> 界面会自动刷新
修改普通 List -> 界面不一定刷新第二部分:把知识组合成完整例子
今天要做的是一个“左侧商品列表 + 右侧商品详情”的窗口。选中左侧商品,右侧显示名称、价格、分类、状态。修改右侧输入框后点击保存,列表里的商品也会更新。
MainWindow.axaml
把下面内容放到 Views/MainWindow.axaml。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="760"
Height="460"
Title="列表和详情">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="220,*"
RowSpacing="12"
ColumnSpacing="16"
Margin="20">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="商品管理"
FontSize="24"
FontWeight="Bold" />
<Border Grid.Row="1"
Grid.Column="0"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<StackPanel Spacing="8">
<TextBlock Text="商品列表"
FontWeight="Bold" />
<ListBox x:Name="ProductList"
SelectionChanged="OnProductSelectionChanged" />
</StackPanel>
</Border>
<Border Grid.Row="1"
Grid.Column="1"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="16">
<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"
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
把下面内容放到 Views/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}";
}
}
}先用一句话理解
左侧 ListBox 只负责显示和选择;真正的商品数据在 C# 的 _products 列表里;右侧详情区显示的是当前 SelectedItem 对应的商品。
第三部分:跟着敲代码
从这里开始动手。涉及列表事件时,MainWindow.axaml 和 MainWindow.axaml.cs 要配套替换。
例子 1:先做静态左右布局
先只摆出左侧列表和右侧详情,不写 C# 数据。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="700"
Height="420"
Title="静态列表详情">
<Grid ColumnDefinitions="220,*"
ColumnSpacing="16"
Margin="20">
<ListBox Grid.Column="0">
<ListBoxItem Content="Keyboard" />
<ListBoxItem Content="Mouse" />
<ListBoxItem Content="Monitor" />
</ListBox>
<StackPanel Grid.Column="1"
Spacing="10">
<TextBlock Text="商品详情"
FontSize="20"
FontWeight="Bold" />
<TextBlock Text="名称:Keyboard" />
<TextBlock Text="价格:199" />
</StackPanel>
</Grid>
</Window>这一步只看布局。右侧内容还不会跟着左侧选择变化。
例子 2:用 C# 数据填充 ListBox
这一步让列表数据从 C# 来。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="320"
Title="C# 数据列表">
<StackPanel Margin="20"
Spacing="10">
<TextBlock Text="商品列表"
FontSize="20"
FontWeight="Bold" />
<ListBox x:Name="ProductList" />
</StackPanel>
</Window>MainWindow.axaml.cs
using System.Collections.Generic;
using Avalonia.Controls;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
private readonly List<Product> _products = new()
{
new Product { Name = "Keyboard", Price = 199 },
new Product { Name = "Mouse", Price = 99 },
new Product { Name = "Monitor", Price = 899 }
};
public MainWindow()
{
InitializeComponent();
ProductList.ItemsSource = _products;
}
private sealed class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public override string ToString()
{
return $"{Name} - {Price}";
}
}
}运行后,ListBox 显示的是 _products 里的三个商品。
例子 3:选中列表项后显示详情
这一步加 SelectionChanged。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="700"
Height="420"
Title="选中显示详情">
<Grid ColumnDefinitions="220,*"
ColumnSpacing="16"
Margin="20">
<ListBox x:Name="ProductList"
Grid.Column="0"
SelectionChanged="OnProductSelectionChanged" />
<StackPanel Grid.Column="1"
Spacing="10">
<TextBlock Text="商品详情"
FontSize="20"
FontWeight="Bold" />
<TextBlock x:Name="NameText"
Text="名称:" />
<TextBlock x:Name="PriceText"
Text="价格:" />
<TextBlock x:Name="ResultText"
Text="请选择左侧商品。" />
</StackPanel>
</Grid>
</Window>MainWindow.axaml.cs
using System.Collections.Generic;
using Avalonia.Controls;
namespace ProductApp.Views;
public partial class MainWindow : Window
{
private readonly List<Product> _products = new()
{
new Product { Name = "Keyboard", Price = 199 },
new Product { Name = "Mouse", Price = 99 },
new Product { Name = "Monitor", Price = 899 }
};
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)
{
ResultText.Text = "没有选中商品。";
return;
}
NameText.Text = $"名称:{product.Name}";
PriceText.Text = $"价格:{product.Price}";
ResultText.Text = $"当前选中:{product.Name}";
}
private sealed class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public override string ToString()
{
return $"{Name} - {Price}";
}
}
}这里的关键是:
Product? product = ProductList.SelectedItem as Product;它把当前选中的列表项转换成 Product,然后右侧才能显示商品字段。
例子 4:把详情改成可编辑输入框
这一步把右侧 TextBlock 换成 TextBox,准备保存。
MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="760"
Height="460"
Title="编辑详情">
<Grid ColumnDefinitions="220,*"
ColumnSpacing="16"
Margin="20">
<ListBox x:Name="ProductList"
Grid.Column="0"
SelectionChanged="OnProductSelectionChanged" />
<Grid Grid.Column="1"
RowDefinitions="Auto,Auto,Auto,Auto,*"
ColumnDefinitions="Auto,*"
RowSpacing="10"
ColumnSpacing="12">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="商品详情"
FontSize="20"
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" />
<Button Grid.Row="3"
Grid.Column="1"
Content="保存"
HorizontalAlignment="Right"
Click="OnSaveClick" />
<TextBlock x:Name="ResultText"
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="2"
TextWrapping="Wrap"
Text="请选择左侧商品。" />
</Grid>
</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 },
new Product { Name = "Mouse", Price = 99 },
new Product { Name = "Monitor", Price = 899 }
};
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)
{
NameBox.Text = "";
PriceBox.Text = "";
ResultText.Text = "没有选中商品。";
return;
}
NameBox.Text = product.Name;
PriceBox.Text = product.Price.ToString();
ResultText.Text = $"当前选中:{product.Name}";
}
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;
int selectedIndex = ProductList.SelectedIndex;
ProductList.ItemsSource = null;
ProductList.ItemsSource = _products;
ProductList.SelectedIndex = selectedIndex;
ResultText.Text = $"已保存:{product.Name}";
}
private sealed class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
public override string ToString()
{
return $"{Name} - {Price}";
}
}
}例子 5:完整列表详情页面
这一步加入分类和启用状态,就是本页作业的完整版本。
小白重复敲写训练
列表加详情先用固定数据,确认选中逻辑后再接输入。
训练 1:显示字符串列表
<ListBox Name="ProductList" SelectionChanged="ProductList_SelectionChanged">
<ListBoxItem Content="Keyboard" />
<ListBoxItem Content="Mouse" />
<ListBoxItem Content="Monitor" />
</ListBox>先确认三项都能点击选中。
训练 2:显示选中内容
private void ProductList_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (ProductList.SelectedItem is ListBoxItem item)
{
DetailText.Text = item.Content?.ToString();
}
}改动任务:没有选中项时显示“请选择商品”。
训练 3:左右布局
<Grid ColumnDefinitions="220,*" ColumnSpacing="16" Margin="20">
<ListBox Grid.Column="0" Name="ProductList" />
<StackPanel Grid.Column="1" Spacing="8">
<TextBlock Text="商品详情" FontSize="24" />
<TextBlock Name="DetailText" />
</StackPanel>
</Grid>第三遍调整左栏宽度,观察详情区域变化。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:列表 + 详情。今天至少要掌握的 3 个点是:
1. 会用 `ListBox` 显示一组商品。
2. 知道 `ItemsSource` 是列表的数据来源。
3. 知道 `SelectedItem` 是当前选中的那一项。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:会用 ListBox 显示一组商品。。
上位机专项练习
把设备列表、选中项和详情区组合成最小监控页面。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:左侧设备列表
<ListBox Name="DeviceList" SelectionChanged="DeviceList_SelectionChanged">
<ListBoxItem Content="PLC-01" />
<ListBoxItem Content="温控器-01" />
<ListBoxItem Content="变频器-01" />
</ListBox>运行结果或界面效果:
左侧可以选择三台设备改动任务: 增加“电表-01”。
专项例子 2:左右分栏页面
<Grid ColumnDefinitions="240,*" Margin="20">
<ListBox Grid.Column="0" ItemsSource="{Binding Devices}" />
<StackPanel Grid.Column="1" Margin="24,0">
<TextBlock Text="设备详情" FontSize="28" />
<TextBlock Name="SelectedNameText" Text="请选择设备" />
</StackPanel>
</Grid>运行结果或界面效果:
左边列表,右边详情改动任务: 把左栏宽度改成 300。
专项例子 3:选择后显示详情
private void DeviceList_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DeviceList.SelectedItem is ListBoxItem item)
{
SelectedNameText.Text = $"当前设备: {item.Content}";
}
}运行结果或界面效果:
点击 PLC-01 后显示“当前设备: PLC-01”改动任务: 选择设备后再显示“状态: 在线”。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个商品列表详情页面:
- 左侧显示至少三个商品。
- 右侧显示当前选中商品的名称、价格、分类、启用状态。
- 切换左侧选中项时,右侧内容跟着变化。
- 修改右侧内容后,点击保存能更新当前商品。
- 保存前检查名称不能为空。
- 保存前检查价格必须是数字,并且大于 0。
- 保存后左侧列表文字要刷新。
MainWindow.axaml 完整答案
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="760"
Height="460"
Title="列表和详情">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="220,*"
RowSpacing="12"
ColumnSpacing="16"
Margin="20">
<TextBlock Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Text="商品管理"
FontSize="24"
FontWeight="Bold" />
<Border Grid.Row="1"
Grid.Column="0"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="12">
<StackPanel Spacing="8">
<TextBlock Text="商品列表"
FontWeight="Bold" />
<ListBox x:Name="ProductList"
SelectionChanged="OnProductSelectionChanged" />
</StackPanel>
</Border>
<Border Grid.Row="1"
Grid.Column="1"
BorderBrush="#DDDDDD"
BorderThickness="1"
Padding="16">
<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"
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}";
}
}
}必须自己验证的操作
| 操作 | 应该看到 |
|---|---|
| 启动窗口 | 左侧自动选中第一个商品,右侧显示它的详情 |
点击 Mouse | 右侧名称和价格变成 Mouse 的数据 |
| 把名称改成空,点击保存 | 显示 商品名称不能为空。 |
把价格改成 abc,点击保存 | 显示 价格必须是数字。 |
把价格改成 0,点击保存 | 显示 价格必须大于 0。 |
把名称改成 Gaming Keyboard,价格改成 299,点击保存 | 左侧列表文字刷新,结果显示已保存 |
如果报错,按这个顺序检查
- 如果提示找不到
ProductList,检查ListBox是否写了x:Name="ProductList"。 - 如果选中列表没有反应,检查
SelectionChanged="OnProductSelectionChanged"。 - 如果提示找不到
SelectionChangedEventArgs,检查是否有using Avalonia.Controls;。 - 如果点击保存没反应,检查按钮是否写了
Click="OnSaveClick"。 - 如果列表显示类型名,检查
Product类是否重写了ToString()。 - 如果保存后左侧列表不刷新,检查是否重新设置了
ItemsSource。
今天真正要掌握的闭环
列表详情页的数据流是:
C# 准备商品列表
-> ListBox 显示商品列表
-> 用户选中一项
-> SelectionChanged 读取 SelectedItem
-> C# 把商品字段放到右侧详情控件
-> 用户修改详情
-> 保存按钮读取右侧输入
-> 校验通过后写回当前 Product
-> 刷新 ListBox 显示理解这条线之后,以后换成 MVVM 和绑定时,仍然是在处理同一件事:一组数据、当前选中项、详情编辑区。