Skip to Content
Week 10Day 6 - 搜索与过滤

Day 6 - 搜索与过滤

建议用时:180-210 分钟

你将学会什么

  • 搜索和过滤有什么区别
  • 为什么要区分全部数据和可见数据
  • 空关键字为什么显示全部
  • 怎么做忽略大小写搜索
  • 怎么做库存过滤
  • 没有结果时怎么显示空状态

今天不是写一个孤立的 Where。要把搜索接到主列表页面:全部商品放在 allProducts,界面列表绑定 VisibleProducts,每次搜索或过滤都重新生成可见列表。

本页固定顺序

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

学习衔接

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

今天的最低通过线

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

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

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

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。搜索过滤的核心是:不要把原始数据改丢,只改变当前显示的数据。

搜索和过滤的区别

名词含义本页例子
搜索按用户输入的文字匹配名称包含 key
过滤按固定条件筛选库存大于 0

搜索通常来自输入框。

过滤通常来自复选框、下拉框、分类按钮。

为什么分成 allProductsVisibleProducts

本页有两份数据:

private readonly List<ProductItem> allProducts = new(); public ObservableCollection<ProductItem> VisibleProducts { get; } = new();

allProducts 是全部商品。

VisibleProducts 是界面当前显示的商品。

这样做的原因是:

  • 搜索不会破坏全部数据。
  • 清空搜索时能恢复全部。
  • 多个过滤条件可以反复重新计算。
  • 界面只绑定可见结果。

空关键字为什么显示全部

用户把搜索框清空,通常表示“不搜索”。

所以代码这样写:

if (!string.IsNullOrWhiteSpace(keyword)) { query = query.Where(product => product.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)); }

如果关键字为空,就不加搜索条件,结果就是全部商品。

为什么要 Trim

用户可能输入:

key

前后空格通常不是搜索内容,所以先写:

string keyword = SearchText.Trim();

这样输入 key key 的结果一样。

为什么要忽略大小写

商品名可能是 Keyboard,用户可能输入 key

如果大小写敏感,就搜不到。

所以本页使用:

StringComparison.OrdinalIgnoreCase

表示不区分大小写。

为什么用 ApplyFilter

搜索文字变了要过滤。

库存勾选变了也要过滤。

所以把逻辑统一放进:

private void ApplyFilter()

然后两个变化都调用它:

partial void OnSearchTextChanged(string value) { ApplyFilter(); } partial void OnOnlyInStockChanged(bool value) { ApplyFilter(); }

这样不会出现搜索一套逻辑、库存过滤另一套逻辑。

空结果不是错误

没有匹配商品是正常情况,不是程序错误。

所以不要抛异常,也不要崩溃。

只要显示:

没有找到符合条件的商品。

并让列表为空即可。

搜索与过滤常用写法速查

需求常用写法说明
原始数据allProducts保存完整列表
可见数据VisibleProducts绑定到界面
搜索文本SearchText输入框绑定
忽略大小写ToLower()两边统一小写
名称包含Name.Contains(keyword)关键词搜索
只看库存OnlyInStock布尔过滤
刷新列表RefreshVisibleProducts()每次条件变化调用
空结果IsEmpty显示无结果提示

搜索固定流程:

从 allProducts 开始 -> 按 SearchText 过滤 -> 按 OnlyInStock 过滤 -> 清空 VisibleProducts -> 把结果加入 VisibleProducts

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

今天做一个搜索过滤页面:

  • 顶部有搜索框。
  • 可以勾选“仅显示有库存”。
  • 列表显示过滤后的商品。
  • 没有结果时显示提示。
  • 点击“清空过滤”后恢复全部商品。

ViewModel

文件位置:

ViewModels/MainWindowViewModel.cs
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { private readonly List<ProductItem> allProducts = new() { new ProductItem { Id = Guid.NewGuid(), Name = "Keyboard", Price = 199m, Stock = 10 }, new ProductItem { Id = Guid.NewGuid(), Name = "Mouse", Price = 59m, Stock = 30 }, new ProductItem { Id = Guid.NewGuid(), Name = "Monitor", Price = 899m, Stock = 0 }, new ProductItem { Id = Guid.NewGuid(), Name = "USB Cable", Price = 19m, Stock = 100 }, new ProductItem { Id = Guid.NewGuid(), Name = "Laptop Stand", Price = 129m, Stock = 0 } }; public ObservableCollection<ProductItem> VisibleProducts { get; } = new(); [ObservableProperty] private string searchText = ""; [ObservableProperty] private bool onlyInStock; [ObservableProperty] private string message = ""; [ObservableProperty] private string emptyMessage = ""; [ObservableProperty] private bool isEmpty; public MainWindowViewModel() { ApplyFilter(); } partial void OnSearchTextChanged(string value) { ApplyFilter(); } partial void OnOnlyInStockChanged(bool value) { ApplyFilter(); } [RelayCommand] private void ClearFilter() { SearchText = ""; OnlyInStock = false; ApplyFilter(); } private void ApplyFilter() { string keyword = SearchText.Trim(); IEnumerable<ProductItem> query = allProducts; if (!string.IsNullOrWhiteSpace(keyword)) { query = query.Where(product => product.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)); } if (OnlyInStock) { query = query.Where(product => product.Stock > 0); } List<ProductItem> result = query.ToList(); VisibleProducts.Clear(); foreach (ProductItem product in result) { VisibleProducts.Add(product); } IsEmpty = VisibleProducts.Count == 0; EmptyMessage = IsEmpty ? "没有找到符合条件的商品。" : ""; Message = $"显示 {VisibleProducts.Count} / {allProducts.Count} 条商品"; } } public sealed class ProductItem { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } }

窗口

文件位置:

Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="840" Height="560" Title="Product Search"> <Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20" RowSpacing="12"> <TextBlock Text="商品搜索与过滤" FontSize="24" FontWeight="Bold" /> <StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"> <TextBox Width="260" PlaceholderText="输入商品名称" Text="{Binding SearchText, Mode=TwoWay}" /> <CheckBox Content="仅显示有库存" IsChecked="{Binding OnlyInStock, Mode=TwoWay}" /> <Button Content="清空过滤" Command="{Binding ClearFilterCommand}" /> </StackPanel> <StackPanel Grid.Row="2" Spacing="4"> <TextBlock Text="{Binding Message}" /> <TextBlock Text="{Binding EmptyMessage}" IsVisible="{Binding IsEmpty}" /> </StackPanel> <ListBox Grid.Row="3" ItemsSource="{Binding VisibleProducts}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Grid ColumnDefinitions="180,100,80" Margin="4"> <TextBlock Text="{Binding Name}" /> <TextBlock Grid.Column="1" Text="{Binding Price}" /> <TextBlock Grid.Column="2" Text="{Binding Stock}" /> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>

第三部分:跟着敲代码

从这里开始动手。打开 Avalonia 项目。

第 1 步:准备全部商品

private readonly List<ProductItem> allProducts = new() { new ProductItem { Id = Guid.NewGuid(), Name = "Keyboard", Price = 199m, Stock = 10 }, new ProductItem { Id = Guid.NewGuid(), Name = "Mouse", Price = 59m, Stock = 30 }, new ProductItem { Id = Guid.NewGuid(), Name = "Monitor", Price = 899m, Stock = 0 }, new ProductItem { Id = Guid.NewGuid(), Name = "USB Cable", Price = 19m, Stock = 100 }, new ProductItem { Id = Guid.NewGuid(), Name = "Laptop Stand", Price = 129m, Stock = 0 } };

解释:

  • 有库存和无库存商品都准备几条。
  • 这样才能验证库存过滤。

第 2 步:准备可见商品集合

public ObservableCollection<ProductItem> VisibleProducts { get; } = new();

解释:

  • 界面列表只绑定 VisibleProducts
  • 不直接绑定 allProducts

第 3 步:写搜索和过滤状态

[ObservableProperty] private string searchText = ""; [ObservableProperty] private bool onlyInStock; [ObservableProperty] private string message = ""; [ObservableProperty] private string emptyMessage = ""; [ObservableProperty] private bool isEmpty;

解释:

  • SearchText 绑定搜索框。
  • OnlyInStock 绑定复选框。
  • IsEmpty 控制空结果提示是否显示。

第 4 步:启动时先显示全部

public MainWindowViewModel() { ApplyFilter(); }

解释:

  • 启动时没有搜索条件,所以会显示全部商品。

第 5 步:搜索文字变化时过滤

partial void OnSearchTextChanged(string value) { ApplyFilter(); }

解释:

  • [ObservableProperty] 会生成 SearchText
  • 同时允许你写 OnSearchTextChanged,在变化时执行逻辑。

第 6 步:库存过滤变化时过滤

partial void OnOnlyInStockChanged(bool value) { ApplyFilter(); }

解释:

  • 勾选或取消勾选时重新计算结果。

第 7 步:写 ApplyFilter

private void ApplyFilter() { string keyword = SearchText.Trim(); IEnumerable<ProductItem> query = allProducts; if (!string.IsNullOrWhiteSpace(keyword)) { query = query.Where(product => product.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)); } if (OnlyInStock) { query = query.Where(product => product.Stock > 0); } List<ProductItem> result = query.ToList();

解释:

  • 先从全部商品开始。
  • 有关键字就加搜索条件。
  • 勾选有库存就加库存条件。
  • 最后得到结果列表。

第 8 步:刷新 VisibleProducts

继续写在 ApplyFilter 里:

VisibleProducts.Clear(); foreach (ProductItem product in result) { VisibleProducts.Add(product); } IsEmpty = VisibleProducts.Count == 0; EmptyMessage = IsEmpty ? "没有找到符合条件的商品。" : ""; Message = $"显示 {VisibleProducts.Count} / {allProducts.Count} 条商品"; }

解释:

  • 先清空旧结果。
  • 再加入新结果。
  • 没结果时显示空状态。

第 9 步:写清空过滤命令

[RelayCommand] private void ClearFilter() { SearchText = ""; OnlyInStock = false; ApplyFilter(); }

解释:

  • 清空搜索文字。
  • 取消库存过滤。
  • 重新显示全部商品。

第 10 步:写顶部搜索区

<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"> <TextBox Width="260" PlaceholderText="输入商品名称" Text="{Binding SearchText, Mode=TwoWay}" /> <CheckBox Content="仅显示有库存" IsChecked="{Binding OnlyInStock, Mode=TwoWay}" /> <Button Content="清空过滤" Command="{Binding ClearFilterCommand}" /> </StackPanel>

解释:

  • 输入框绑定搜索关键字。
  • 复选框绑定库存过滤。
  • 按钮绑定清空命令。

第 11 步:写结果提示和列表

<StackPanel Grid.Row="2" Spacing="4"> <TextBlock Text="{Binding Message}" /> <TextBlock Text="{Binding EmptyMessage}" IsVisible="{Binding IsEmpty}" /> </StackPanel> <ListBox Grid.Row="3" ItemsSource="{Binding VisibleProducts}">

解释:

  • Message 显示当前结果数量。
  • EmptyMessage 只在没有结果时显示。
  • 列表绑定过滤结果。

第 12 步:运行后检查

运行后按顺序检查:

  1. 启动后显示 5 条商品。
  2. 输入 key,只显示 Keyboard
  3. 输入 USB,只显示 USB Cable
  4. 输入 xxx,列表为空并显示空结果提示。
  5. 清空输入后恢复 5 条。
  6. 勾选“仅显示有库存”,隐藏库存为 0 的商品。
  7. 输入 stand 且勾选有库存,结果为空。

常见错误和修法

错误为什么错修法
搜索直接改原始列表清空关键词后数据回不来保留 AllProducts,显示 VisibleProducts
搜索大小写敏感同一商品可能搜不到比较前统一 ToLower()
每输入一个字就读文件性能差,逻辑乱先在内存列表里过滤
多个条件互相覆盖分类、关键词、状态不能组合用一条 LINQ 查询按顺序叠加条件
空关键词没有处理清空搜索后仍然过滤关键词为空时显示全部

小白重复敲写训练

搜索过滤先在纯 C# 集合上练,再接界面输入框。

训练 1:按名称搜索

string keyword = "key"; var result = products.Where(product => product.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase));

分别搜索 keymouse 和不存在的词。

训练 2:组合库存条件

bool onlyInStock = true; var result = products.Where(product => !onlyInStock || product.Stock > 0);

改动任务:切换 onlyInStock 为 false。

训练 3:输入变化后刷新

partial void OnKeywordChanged(string value) { ApplyFilter(); } private void ApplyFilter() { FilteredProducts.Clear(); foreach (Product product in _allProducts.Where(Matches)) FilteredProducts.Add(product); }

第三遍补上空关键词时显示全部商品。

每日小测

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

1. 判断题

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

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

2. 填空题

本页主题是:搜索与过滤。今天至少要掌握的 3 个点是:

1. 搜索和过滤有什么区别 2. 为什么要区分全部数据和可见数据 3. 空关键字为什么显示全部

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

3. 流程题

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

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

4. 找错误题

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

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

5. 改需求题

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

可选改法:

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

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:搜索和过滤有什么区别。

上位机专项练习

设备多了以后必须支持搜索、状态筛选和排序,否则操作者找不到目标。

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

专项例子 1:按名称搜索

IEnumerable<DeviceItem> result = Devices.Where(device => device.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase));

运行结果或界面效果:

输入 plc 只保留名称包含 plc 的设备

改动任务: 同时搜索 IP 地址。

专项例子 2:按状态筛选

if (SelectedStatus == "在线") { result = result.Where(device => device.IsOnline); } else if (SelectedStatus == "离线") { result = result.Where(device => !device.IsOnline); }

运行结果或界面效果:

下拉框切换在线或离线

改动任务: 增加“报警”筛选。

专项例子 3:组合搜索栏

<StackPanel Orientation="Horizontal" Spacing="10"> <TextBox Width="260" Watermark="搜索名称或 IP" Text="{Binding SearchText}" /> <ComboBox ItemsSource="{Binding StatusOptions}" SelectedItem="{Binding SelectedStatus}" /> <Button Content="清空" Command="{Binding ClearFilterCommand}" /> </StackPanel>

运行结果或界面效果:

搜索、状态和清空操作放在一起

改动任务: 增加按名称排序按钮。

第四部分:作业完整答案

作业要求:完成商品搜索过滤页面,支持关键字搜索、仅显示有库存、清空过滤、空结果提示。

答案文件 1:ViewModels/MainWindowViewModel.cs

using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { private readonly List<ProductItem> allProducts = new() { new ProductItem { Id = Guid.NewGuid(), Name = "Keyboard", Price = 199m, Stock = 10 }, new ProductItem { Id = Guid.NewGuid(), Name = "Mouse", Price = 59m, Stock = 30 }, new ProductItem { Id = Guid.NewGuid(), Name = "Monitor", Price = 899m, Stock = 0 }, new ProductItem { Id = Guid.NewGuid(), Name = "USB Cable", Price = 19m, Stock = 100 }, new ProductItem { Id = Guid.NewGuid(), Name = "Laptop Stand", Price = 129m, Stock = 0 } }; public ObservableCollection<ProductItem> VisibleProducts { get; } = new(); [ObservableProperty] private string searchText = ""; [ObservableProperty] private bool onlyInStock; [ObservableProperty] private string message = ""; [ObservableProperty] private string emptyMessage = ""; [ObservableProperty] private bool isEmpty; public MainWindowViewModel() { ApplyFilter(); } partial void OnSearchTextChanged(string value) { ApplyFilter(); } partial void OnOnlyInStockChanged(bool value) { ApplyFilter(); } [RelayCommand] private void ClearFilter() { SearchText = ""; OnlyInStock = false; ApplyFilter(); } private void ApplyFilter() { string keyword = SearchText.Trim(); IEnumerable<ProductItem> query = allProducts; if (!string.IsNullOrWhiteSpace(keyword)) { query = query.Where(product => product.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)); } if (OnlyInStock) { query = query.Where(product => product.Stock > 0); } List<ProductItem> result = query.ToList(); VisibleProducts.Clear(); foreach (ProductItem product in result) { VisibleProducts.Add(product); } IsEmpty = VisibleProducts.Count == 0; EmptyMessage = IsEmpty ? "没有找到符合条件的商品。" : ""; Message = $"显示 {VisibleProducts.Count} / {allProducts.Count} 条商品"; } } public sealed class ProductItem { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } }

答案文件 2:Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="840" Height="560" Title="Product Search"> <Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20" RowSpacing="12"> <TextBlock Text="商品搜索与过滤" FontSize="24" FontWeight="Bold" /> <StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8"> <TextBox Width="260" PlaceholderText="输入商品名称" Text="{Binding SearchText, Mode=TwoWay}" /> <CheckBox Content="仅显示有库存" IsChecked="{Binding OnlyInStock, Mode=TwoWay}" /> <Button Content="清空过滤" Command="{Binding ClearFilterCommand}" /> </StackPanel> <StackPanel Grid.Row="2" Spacing="4"> <TextBlock Text="{Binding Message}" /> <TextBlock Text="{Binding EmptyMessage}" IsVisible="{Binding IsEmpty}" /> </StackPanel> <ListBox Grid.Row="3" ItemsSource="{Binding VisibleProducts}"> <ListBox.ItemTemplate> <DataTemplate x:DataType="vm:ProductItem"> <Grid ColumnDefinitions="180,100,80" Margin="4"> <TextBlock Text="{Binding Name}" /> <TextBlock Grid.Column="1" Text="{Binding Price}" /> <TextBlock Grid.Column="2" Text="{Binding Stock}" /> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>

验收结果

运行后必须看到:

  1. 启动后显示 5 条商品。
  2. 输入 key 后只显示 Keyboard
  3. 输入 usb 后只显示 USB Cable
  4. 输入不存在的关键字后显示空结果提示。
  5. 清空过滤后恢复全部商品。
  6. 勾选有库存后不显示库存为 0 的商品。
  7. 搜索和库存过滤可以组合使用。

为什么这个答案是对的

这个答案把搜索过滤拆成了清楚的数据流:

数据作用
allProducts保存全部商品,不被搜索破坏
SearchText保存用户输入的关键字
OnlyInStock保存库存过滤开关
ApplyFilter统一计算过滤结果
VisibleProducts给界面显示当前结果
IsEmpty控制空结果提示

搜索过滤的关键是:原始数据不丢,显示结果可变,条件变化时重新计算可见列表。