关于 maui-collectionview
maui-collectionview 是面向 .NET MAUI 开发者的 CollectionView 参考技能,用于构建可滚动列表和网格。内容强调按需查阅 API、规避绑定与布局问题,并在适用时从 ListView 迁移。
主要功能
- 01列表与网格布局指南
- 02选择、分组和刷新模式
- 03增量加载与滑动操作
- 04绑定和性能常见问题
- 05ListView 迁移指导
适用场景
- 01创建数据绑定的可滚动列表或网格
- 02添加选择、分组或下拉刷新
- 03实现增量加载或滑动操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill maui-collectionview安装后在 Claude Code 中输入/maui-collectionview确认已加载。
附带文件references/collectionview-api.md
详细介绍
CollectionView — .NET MAUI
CollectionView 是 .NET MAUI 中用于显示可滚动数据列表和网格的主要控件。它取代了 ListView,性能更佳、布局更灵活,而且不需要 ViewCell。
适用场景
- 显示可滚动的数据项列表或网格
- 将对象集合绑定到模板化的项目布局
- 添加选择(单选或多选)、分组或下拉刷新
- 实现无限滚动/增量加载
- 在列表项上显示滑动操作
- 没有可用数据时显示空状态
不适用场景
- 项目数量固定的静态布局 — 直接使用
Grid或StackLayout - 地图图钉列表 — 使用
Microsoft.Maui.Controls.MapsNuGet 包 - 基于表格的数据录入表单 — 使用标准表单控件
- 无交互的简单纯文本列表 — 可考虑在
StackLayout上使用BindableLayout
控制范围 — 只回答用户所问的内容
这项技能是供你查阅的参考资料,不是要逐项执行的清单。大多数请求只需要其中一两个部分。把其余内容也一并纳入,只会让回答变得更差。
停止条件 — 遇到以下情况时请勿采取行动:
- 用户提出了范围明确的问题。 只回答该问题。不要额外补充用户未询问的分组、滑动操作、空视图、吸附点或性能建议。
- 用户现有的代码已经能正常运行。 不要为了匹配这里的示例而重写可正常工作的标记。指出具体缺陷;如果没有缺陷,就说明这一点并回答用户的问题。
- 改动只是风格上的。 重命名、调整属性顺序,或重构本来就运行正常的模板,这些都只是徒增改动,并非修复。
- 控件不是
CollectionView。CarouselView、BindableLayout和维护中的ListView代码有不同的规则。不要重写用户未要求修改的ListView代码 — 但如果用户询问该使用哪种控件,或正在从 Xamarin.Forms 迁移,请推荐CollectionView(参见从 ListView 迁移)。 - 问题本质上是绑定、DI 或导航问题,只是恰好涉及列表。 请转而参考
maui-data-binding、maui-dependency-injection或maui-shell-navigation。
以下 API 章节是参考资料,并非清单 — 仅在相关时提供。 有四条规则不可妥协,因为违反它们会导致代码无法运行,或在不知不觉中失去编译时检查:
- 在
CollectionView中,绝不可将ViewCell用作DataTemplate的根元素。 - 如果列表在首次渲染后会发生变化,请使用
ObservableCollection<T>。 - 在 UI 线程上修改绑定的集合。
- 为每个
DataTemplate(以及页面根元素)设置x:DataType,以启用编译绑定。
其他所有内容 — 大小策略、吸附点、页眉/页脚、空视图 — 都是可选的,仅在能解决用户的实际问题时才应提供。
输入
- 绑定到
ItemsSource的数据源(通常为ObservableCollection<T>) - 用于定义每个项目呈现方式的
DataTemplate - 可选:布局配置、选择模式、分组模型、空视图
基本设置
这是一个完整、可直接复制粘贴的页面。有两处至关重要:本技能中每个 x:DataType="models:Item" 都依赖的 xmlns:models
声明,以及根级 x:DataType——没有它,外层的 ItemsSource 绑定就无法编译:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MyApp.Models"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:DataType="vm:ItemsViewModel"
x:Class="MyApp.ItemsPage">
<ContentPage.BindingContext>
<vm:ItemsViewModel />
</ContentPage.BindingContext>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<HorizontalStackLayout Padding="8" Spacing="8">
<Image Source="{Binding Icon}" WidthRequest="40" HeightRequest="40" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
后续代码片段只展示 CollectionView 元素。将代码片段提供给用户时,请为其中使用的每个前缀添加对应的 xmlns: 声明,否则 XAML 将无法编译。
上面的内联 <ContentPage.BindingContext> 让示例可以独立运行。在使用依赖注入的应用中,请改为注册 ViewModel,并通过构造函数注入来设置它(BindingContext = vm;)——请参阅 maui-dependency-injection 技能。
关键规则:
- 将
ItemsSource绑定到ObservableCollection<T>,以便 UI 在添加或移除项目时更新。 - 每个项模板的根元素都必须是
Layout或View——切勿使用ViewCell。 - 始终在
DataTemplate上设置x:DataType,以启用编译绑定。
布局
设置 ItemsLayout 以控制排列方式。默认值为 VerticalList。
| 布局 | XAML 值 |
|---|---|
| 垂直列表 | VerticalList(默认) |
| 水平列表 | HorizontalList |
| 垂直网格 | GridItemsLayout,且 Orientation="Vertical" |
| 水平网格 | GridItemsLayout,且 Orientation="Horizontal" |
网格布局
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
VerticalItemSpacing="8"
HorizontalItemSpacing="8" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Border Padding="8" StrokeThickness="0">
<VerticalStackLayout>
<Image Source="{Binding Image}" HeightRequest="120" Aspect="AspectFill" />
<Label Text="{Binding Name}" FontAttributes="Bold" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
水平列表
<CollectionView ItemsSource="{Binding Items}"
ItemsLayout="HorizontalList" />
选择
选择模式
| 模式 | 要绑定的属性 | 绑定模式 |
|---|---|---|
None | — | — |
Single | SelectedItem | TwoWay |
Multiple | SelectedItems | OneWay |
<CollectionView ItemsSource="{Binding Items}"
SelectionMode="Single"
SelectedItem="{Binding CurrentItem, Mode=TwoWay}"
SelectionChangedCommand="{Binding ItemSelectedCommand}" />
对于 Multiple 选择,请绑定 SelectedItems(类型为 IList<object>):
<CollectionView SelectionMode="Multiple"
SelectedItems="{Binding ChosenItems, Mode=OneWay}" />
选中项视觉状态
使用 VisualStateManager 高亮显示选中项:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Grid Padding="8">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding Name}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
分组
- 创建一个继承自
List<T>的分组类:
public class AnimalGroup : List<Animal>
{
public string Name { get; }
public AnimalGroup(string name, List<Animal> animals) : base(animals)
{
Name = name;
}
}
- 绑定到
ObservableCollection<AnimalGroup>,并设置IsGrouped="True":
<CollectionView ItemsSource="{Binding AnimalGroups}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="models:AnimalGroup">
<Label Text="{Binding Name}"
FontAttributes="Bold"
BackgroundColor="{StaticResource Gray100}"
Padding="8" />
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Animal">
<Label Text="{Binding Name}" Padding="16,4" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
下拉刷新
将 CollectionView 包装在 RefreshView 中。完成后将 IsRefreshing 设回 false:
<RefreshView IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView ItemsSource="{Binding Items}" />
</RefreshView>
增量加载(无限滚动)
<CollectionView ItemsSource="{Binding Items}"
RemainingItemsThreshold="5"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}" />
⚠️ 请勿与不支持虚拟化的布局搭配使用。
LinearItemsLayout和GridItemsLayout支持虚拟化。将BindableLayout用于StackLayout,作为CollectionView的替代方案时不支持虚拟化,这会触发无限次阈值到达事件。
SwipeView — 从 DataTemplate 内进行绑定
DataTemplate 中的命令无法直接访问 ViewModel。请使用 RelativeSource AncestorType:
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="Red"
Command="{Binding BindingContext.DeleteCommand, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="8">
<Label Text="{Binding Name}" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
空视图
当 ItemsSource 为空或为 null 时显示。
<CollectionView ItemsSource="{Binding SearchResults}"
EmptyView="No items found." />
要自定义空视图,请使用 ContentView 包装:
<CollectionView ItemsSource="{Binding SearchResults}">
<CollectionView.EmptyView>
<ContentView>
<VerticalStackLayout HorizontalOptions="Center" VerticalOptions="Center">
<Image Source="empty_state.png" WidthRequest="120" />
<Label Text="Nothing here yet" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</ContentView>
</CollectionView.EmptyView>
</CollectionView>
页眉和页脚
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.Header>
<Label Text="Header" FontAttributes="Bold" Padding="8" />
</CollectionView.Header>
<CollectionView.Footer>
<Label Text="Footer" FontAttributes="Italic" Padding="8" />
</CollectionView.Footer>
</CollectionView>
页眉或页脚需要进行数据绑定时,请使用 HeaderTemplate / FooterTemplate。
滚动
ScrollTo
按索引或项目以编程方式滚动:
// Scroll to index
collectionView.ScrollTo(index: 10, position: ScrollToPosition.Center, animate: true);
// Scroll to item
collectionView.ScrollTo(item: myItem, position: ScrollToPosition.MakeVisible, animate: true);
| ScrollToPosition | 行为 |
|---|---|
MakeVisible | 只滚动到刚好使项目可见 |
Start | 将项目滚动到视口起始位置 |
Center | 将项目滚动到视口中央 |
End | 将项目滚动到视口末尾 |
吸附点
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>
SnapPointsType:None、Mandatory、MandatorySingleSnapPointsAlignment:Start、Center、End
从 ListView 迁移
ListView 仍可编译,但自 .NET 10 起已标记为 [Obsolete]
(“ListView 已弃用。请改用 CollectionView。”)。在 .NET 9 及更早版本中,它并未
弃用,因此将其描述为已弃用之前,请先检查项目的目标框架。如果用户询问
该使用哪个控件,或正在从 Xamarin.Forms 迁移,请推荐
CollectionView——它速度更快,无需 ViewCell,并支持灵活的
布局。应避免的是,擅自改写用户未要求修改的 ListView 代码。
ListView | CollectionView 对应项 |
|---|---|
ViewCell 模板根元素 | 任意 View/Layout 根元素——不支持 ViewCell |
ItemSelected 事件 | SelectionChanged 事件或 SelectionChangedCommand |
ItemTapped 事件 | 项模板中的 TapGestureRecognizer——SelectionChanged 仅在选择项发生变化时触发,因此再次点击已选中的项不会再次触发 |
IsPullToRefreshEnabled + Refreshing | 将 CollectionView 包装在 RefreshView 中 |
IsGroupingEnabled | IsGrouped |
HasUnevenRows="True" | 默认的 ItemSizingStrategy="MeasureAllItems" |
RowHeight(固定高度) | 在项模板中设置高度。MeasureFirstItem 只会复用第一个项测得的尺寸——它不是显式的行高 |
SeparatorVisibility / SeparatorColor | 没有对应项——在项模板中绘制 BoxView/Border |
缺少分隔线 API 是迁移时最常见的意外:CollectionView
没有内置分隔线,因此需要自行在模板中添加。
性能提示
只有在用户报告性能问题或明确询问性能时才应用这些建议——它们不是默认检查清单。
- 对于尺寸一致的项,使用
MeasureFirstItem——这比默认的MeasureAllItems快得多,后者会逐项测量。将其设置在CollectionView本身上(它声明在StructuredItemsView中),而不是LinearItemsLayout/GridItemsLayout上:
不适合使用<CollectionView ItemsSource="{Binding Items}" ItemSizingStrategy="MeasureFirstItem"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="models:Item"> <Grid Padding="8" ColumnDefinitions="44,*" ColumnSpacing="8"> <Image WidthRequest="44" HeightRequest="44" /> <Label Grid.Column="1" Text="{Binding Name}" VerticalOptions="Center" /> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView>MeasureFirstItem的情况——以下情况应保留默认的MeasureAllItems:- 项的高度各不相同(文本换行、可选行、纵横比不同的图像)——第一个项的尺寸会应用到所有项,因此其余项会被裁切或拉伸。
DataTemplateSelector返回不同的模板——第一个项无法代表其他项。- 第一个项并不典型(类似标题或“精选”行)——所有项都会沿用其尺寸。通过调整数据顺序来解决是个不妥的做法;应改用
MeasureAllItems。 - 项的尺寸取决于测量第一个项时尚未加载的运行时数据。
- **如果列表在首次渲染后会发生变化,请使用
ObservableCollection<T>。**它实现了INotifyCollectionChanged,因此原地执行Add/Remove/Insert会增量更新 UI。 对于绑定后不会再变化的列表,使用List<T>即可。请注意,替换ItemsSource会导致所有内容重新渲染,与集合类型无关——因此应原地修改已绑定的 集合,而不是重新赋值。 - 在 UI 线程上更新集合——
MainThread.BeginInvokeOnMainThread(() => Items.Add(item))。
常见问题
| 问题 | 解决方法 |
|---|---|
| 项目变化时 UI 不更新 | 使用 ObservableCollection<T>,而不是 List<T>。 |
| 应用崩溃或项目为空白 | 切勿使用 ViewCell——请使用 Grid、StackLayout 或任意 View 作为模板根元素。 |
| 项目消失或布局错乱 | 始终在 UI 线程(MainThread.BeginInvokeOnMainThread)上更新 ItemsSource 和集合。 |
| 增量加载持续触发 | 不要使用 StackLayout 作为布局;请使用 LinearItemsLayout 或 GridItemsLayout。 |
| EmptyView 未正确呈现 | 使用 ContentView 包裹自定义空视图。 |
| 滚动性能差 | 对大小一致的项目使用 MeasureFirstItem 大小调整策略。 |
ItemSizingStrategy 无法编译 | 它在 StructuredItemsView 上声明——请在 <CollectionView> 上设置,而不是在 <LinearItemsLayout> / <GridItemsLayout> 上设置。 |
| 项目被裁切或拉伸 | MeasureFirstItem 假定项目大小一致。对于高度不一的项目,请使用默认的 MeasureAllItems。 |
| 选中状态不可见 | 在项目模板根元素中添加 VisualState Name="Selected"。 |
| SwipeView 命令中的绑定错误 | 使用 RelativeSource AncestorType,从项目模板内部访问 ViewModel。 |
验证
返回你编写或编辑的 CollectionView 标记前,请确认:
-
DataTemplate的根元素是View/Layout,而不是ViewCell。 -
DataTemplate声明了x:DataType,以使用编译绑定。 - 如果列表会发生变化,
ItemsSource绑定到ObservableCollection<T>。 - 如果使用了
ItemSizingStrategy,它设置在<CollectionView>上,而不是布局上。 -
Multiple选择模式绑定到SelectedItems;Single选择模式绑定到SelectedItem(TwoWay)。 - 刷新完成时,
RefreshView.IsRefreshing会重置为false。 - 答案只涵盖用户所问的内容,不包含用户未要求的部分。