Skip to Content
Week 06Day 6 - 列表 + 详情

Day 6 - 列表 + 详情

建议用时:240-270 分钟

你将学会什么

  • 会用 ListBox 显示一组商品。
  • 知道 ItemsSource 是列表的数据来源。
  • 知道 SelectedItem 是当前选中的那一项。
  • 会处理 SelectionChanged 事件。
  • 会把左侧选中的商品显示到右侧详情区。
  • 会修改右侧输入框,再把结果保存回当前商品。

本页先不用数据绑定做列表详情,而是用 code-behind 把数据流走一遍:列表数据从 C# 来,选中项从 ListBox 来,详情显示由 C# 更新。这样能先看清底层流程。

本页固定顺序

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

学习衔接

上一页学习的是“表单页面”,今天继续学习“列表 + 详情”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 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;

这三句做了什么:

  1. 记住当前选中的位置。
  2. 先断开列表数据。
  3. 再重新绑定列表数据。
  4. 恢复原来的选中位置。

以后学 ObservableCollection 和数据绑定后,刷新方式会更优雅。本页先保证数据流能看清楚。

10. 列表详情页的稳定顺序

写列表详情时按这个顺序:

  1. 先准备数据列表。
  2. 把列表放进 ListBox.ItemsSource
  3. 处理选中事件。
  4. 选中后显示详情。
  5. 保存前校验详情输入。
  6. 校验通过后写回当前对象。
  7. 刷新列表显示。

不要一开始就把新增、删除、搜索、排序全加进去。先把“选中一条并编辑保存”走通。

列表和详情常用绑定速查

需求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.axamlMainWindow.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”

改动任务: 选择设备后再显示“状态: 在线”。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。

作业要求

做一个商品列表详情页面:

  1. 左侧显示至少三个商品。
  2. 右侧显示当前选中商品的名称、价格、分类、启用状态。
  3. 切换左侧选中项时,右侧内容跟着变化。
  4. 修改右侧内容后,点击保存能更新当前商品。
  5. 保存前检查名称不能为空。
  6. 保存前检查价格必须是数字,并且大于 0。
  7. 保存后左侧列表文字要刷新。

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,点击保存左侧列表文字刷新,结果显示已保存

如果报错,按这个顺序检查

  1. 如果提示找不到 ProductList,检查 ListBox 是否写了 x:Name="ProductList"
  2. 如果选中列表没有反应,检查 SelectionChanged="OnProductSelectionChanged"
  3. 如果提示找不到 SelectionChangedEventArgs,检查是否有 using Avalonia.Controls;
  4. 如果点击保存没反应,检查按钮是否写了 Click="OnSaveClick"
  5. 如果列表显示类型名,检查 Product 类是否重写了 ToString()
  6. 如果保存后左侧列表不刷新,检查是否重新设置了 ItemsSource

今天真正要掌握的闭环

列表详情页的数据流是:

C# 准备商品列表 -> ListBox 显示商品列表 -> 用户选中一项 -> SelectionChanged 读取 SelectedItem -> C# 把商品字段放到右侧详情控件 -> 用户修改详情 -> 保存按钮读取右侧输入 -> 校验通过后写回当前 Product -> 刷新 ListBox 显示

理解这条线之后,以后换成 MVVM 和绑定时,仍然是在处理同一件事:一组数据、当前选中项、详情编辑区。