mcpField
M
maui-collectionview

maui-collectionview

github.com/dotnet开发5.8k3.3k 次安装1 天前更新
代码仓库

关于 maui-collectionview

maui-collectionview 是面向 .NET MAUI 开发者的 CollectionView 参考技能,用于构建可滚动列表和网格。内容强调按需查阅 API、规避绑定与布局问题,并在适用时从 ListView 迁移。

主要功能

  1. 01列表与网格布局指南
  2. 02选择、分组和刷新模式
  3. 03增量加载与滑动操作
  4. 04绑定和性能常见问题
  5. 05ListView 迁移指导

适用场景

  1. 01创建数据绑定的可滚动列表或网格
  2. 02添加选择、分组或下拉刷新
  3. 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.Maps NuGet 包
  • 基于表格的数据录入表单 — 使用标准表单控件
  • 无交互的简单纯文本列表 — 可考虑在 StackLayout 上使用 BindableLayout

控制范围 — 只回答用户所问的内容

这项技能是供你查阅的参考资料,不是要逐项执行的清单。大多数请求只需要其中一两个部分。把其余内容也一并纳入,只会让回答变得更差。

停止条件 — 遇到以下情况时请勿采取行动:

  • 用户提出了范围明确的问题。 只回答该问题。不要额外补充用户未询问的分组、滑动操作、空视图、吸附点或性能建议。
  • 用户现有的代码已经能正常运行。 不要为了匹配这里的示例而重写可正常工作的标记。指出具体缺陷;如果没有缺陷,就说明这一点并回答用户的问题。
  • 改动只是风格上的。 重命名、调整属性顺序,或重构本来就运行正常的模板,这些都只是徒增改动,并非修复。
  • 控件不是 CollectionView。 CarouselView、BindableLayout 和维护中的 ListView 代码有不同的规则。不要重写用户未要求修改的 ListView 代码 — 但如果用户询问该使用哪种控件,或正在从 Xamarin.Forms 迁移,请推荐 CollectionView(参见从 ListView 迁移)。
  • 问题本质上是绑定、DI 或导航问题,只是恰好涉及列表。 请转而参考 maui-data-binding、maui-dependency-injection 或 maui-shell-navigation。

以下 API 章节是参考资料,并非清单 — 仅在相关时提供。 有四条规则不可妥协,因为违反它们会导致代码无法运行,或在不知不觉中失去编译时检查:

  1. 在 CollectionView 中,绝不可将 ViewCell 用作 DataTemplate 的根元素。
  2. 如果列表在首次渲染后会发生变化,请使用 ObservableCollection<T>。
  3. 在 UI 线程上修改绑定的集合。
  4. 为每个 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——
SingleSelectedItemTwoWay
MultipleSelectedItemsOneWay
<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>

分组

  1. 创建一个继承自 List<T> 的分组类:
public class AnimalGroup : List<Animal>
{
    public string Name { get; }
    public AnimalGroup(string name, List<Animal> animals) : base(animals)
    {
        Name = name;
    }
}
  1. 绑定到 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、MandatorySingle
  • SnapPointsAlignment:Start、Center、End

从 ListView 迁移

ListView 仍可编译,但自 .NET 10 起已标记为 [Obsolete] (“ListView 已弃用。请改用 CollectionView。”)。在 .NET 9 及更早版本中,它并未 弃用,因此将其描述为已弃用之前,请先检查项目的目标框架。如果用户询问 该使用哪个控件,或正在从 Xamarin.Forms 迁移,请推荐 CollectionView——它速度更快,无需 ViewCell,并支持灵活的 布局。应避免的是,擅自改写用户未要求修改的 ListView 代码。

ListViewCollectionView 对应项
ViewCell 模板根元素任意 View/Layout 根元素——不支持 ViewCell
ItemSelected 事件SelectionChanged 事件或 SelectionChangedCommand
ItemTapped 事件项模板中的 TapGestureRecognizer——SelectionChanged 仅在选择项发生变化时触发,因此再次点击已选中的项不会再次触发
IsPullToRefreshEnabled + Refreshing将 CollectionView 包装在 RefreshView 中
IsGroupingEnabledIsGrouped
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。
  • 答案只涵盖用户所问的内容,不包含用户未要求的部分。

参考资料

评论

支持 Markdown