.NET MAUI 中的所有列表 .NET MAUI 中的所有列表

2025-06-04

.NET MAUI 中的所有列表

.NET MAUI 中的所有列表

本博客是.NET MAUI UI 2024 年 7 月系列的一部分,每月每天都会发布新文章。查看完整时间表了解更多信息。

在任何应用项目中,你不可避免地会列出需要显示的内容,并面临选择最佳控件的问题。在这里,我将以移动应用为例,分享我是如何做出这些决策的。

我调查了手机上的所有应用程序,并收集了各种不同的体验。对于数据,我编写了一个程序MockDataService来生成有用但随机的内容。对于图片,我结合使用了Lorem Picsum和我用ChatGPT制作的图片

我认为结果相当不错,尽管我警告说它们还不是经过生产完善且功能齐全的。

各种布局的特色图片

跳转到下面的每个示例:

GitHub 徽标 davidortinau / AllTheLists

列表用户体验示例集锦




在介绍每个样本之前,我想先说一下一些一般性的想法。

包罗万象的东西终究是做不好的。为了使通用控件足够灵活,能够满足各种需求,其实现过程中必然会做出一些妥协。当它无法满足你的期望时,你可能会感到沮丧。而一个只做你需要的专用控件,才能最好地满足你的场景需求。然而,另一方面,你的知识和技能也需要从通用升级到专用。

扁平比臃肿更快。这是真的。如果速度对您的场景很重要,那么避免大量 UI 和控件嵌套的布局在大规模情况下会表现更好,因为它需要更少的测量和布局调用。当性能至关重要时,请尽量避免测量;尽可能明确地提供 UI 尺寸。

用户体验 > 用户界面我发现很多应用在列表场景下表现不佳,因为它们为了完成任务而塞入了大量用户界面,而不是遵循良好的用户体验原则。你真的需要在列表的每一行都提供完整的聊天体验吗?或者你可以导航到其他页面吗?或许你可以使用模态框体验或底部表单?如果你的移动用户界面包含多个明确的行动号召,那么你就有可能降低用户界面的效率,而不是提高用户的使用效率。先解决用户体验问题,再解决用户界面问题。

.NET MAUI 列表控件概述

在我的示例中,我使用了三个内置控件和两个社区控件,它们都展示了不同的方法,各有优缺点。.NET MAUI 提供了CollectionViewListViewBindableLayout。从社区中,我选择了VirtualListViewVirtualizeListView。还有许多其他选项,我在最后列出了其中一些,供您自行评估。

CollectionView 列表视图 可绑定布局 虚拟列表视图 虚拟化列表视图
虚拟化 是的 是的 是的 是的
下拉刷新 是的 - 使用 RefreshView 是的 是的 - 使用 RefreshView 是的 是的
单选 是的 是的 是的 是的
多项选择 是的 是的
加载更多(阈值) 是的 是的
布局 - 垂直 是的 是的 是的 是的 是的
布局 - 水平 是的 是的 是的
布局 - 网格 是的 是的
布局 - 自定义 是的 是的
行为 特定于平台 特定于平台 跨平台 特定于平台 跨平台
分组数据 是的 是的 是的 是的
上下文菜单项 是的 - 使用 SwipeView 是的 是的 - 使用 SwipeView
页眉/页脚 是的 是的 是的 是的
预定义模板 是的
空视图模板 是的 是的 - 使用社区工具包 是的

我将主要关注CollectionView后者ListView,除非有令人信服的理由选择后者。

附加性能说明

如果渲染和滚动的速度对于您的场景至关重要,那么这些注释适合您。

  • 布局生命周期- 当你尝试诊断和提升复杂 UI 的渲染性能时,了解布局的测量和排列过程至关重要。通常,如果你知道某个元素的尺寸,就提供它。

  • 编译绑定将告知编译器正在使用的类型,从而改进 XAML 数据绑定控件的渲染和更新。在任何包含 BindingContext 的 XAML 元素上,请使用 指定类型,例如x:DataType="model:Sample"

  • 绑定模式- 可绑定属性的默认绑定模式因控件和属性而异。大多数属性的默认绑定模式OneWayView.RotationView.Scale,而常用于捕获用户输入的属性为TwoWayEntry.TextListView.IsRefreshing在大多数情况下,默认模式将满足您的预期和需求,但请记住,您可以更改这些模式,并使用其他选项,例如OneTimeOneWayToSource文档

  • ObservableCollection 与 List如果您的数据不会动态更新,并且也许它是一个OneTime绑定,那么使用List

  • 图片- 确保您的图片尺寸适合在屏幕上使用。在运行时缩小图片尺寸可能会对资源造成巨大需求,导致内存不足和崩溃。在几乎所有情况下,光栅图像的渲染速度都比矢量图像快。如果您从远程源加载图片,请确保不会阻塞 UI 的加载。使用 FFImage 之类的控件显示占位符图片并延迟加载远程图片。另外,请注意,您可以在 .NET MAUI 中自定义图片缓存策略

  • 发布版本 vs 调试版本- 评估性能时,必须使用发布版本。调试版本中有很多操作会降低应用速度,因此判断发布版本毫无意义。请生成发布版本并进行测量。同时,了解 AOT(提前编译)的选项。.NET 9 为 iOS 提供了预览版原生 AOT;然而,它非常严格,大多数库都不兼容。我们在 .NET MAUI 本身上做了很多工作,以使其兼容。Android 有部分(启动跟踪)和完整 AOT 两种版本可供选择。

  • 在设备上测试- 务必在设备上审核发布版本。如果您知道用户的目标设备和操作系统版本,最好在该设备上进行测试。我使用过 iPhone 15 Pro 和 Pixel 5。在 99.9999% 的情况下,iOS 系统不会出现性能问题。

  • 布局压缩(已过时)是 Xamarin.Forms 中的一项运行时优化,旨在从可视化树中移除包装布局。如果布局没有背景色或未通过手势接收用户输入,则可以安全地将其从实际渲染到屏幕的 UI 中移除。这在 Xamarin.Forms 中非常有用,因为几乎所有视图(渲染器)都包裹在视图中。后来,Xamarin.Forms 引入了一组更新的渲染器,巧妙地命名为“快速渲染器”,它们移除了这些包装视图。在 .NET MAUI 中,这种冗余已被消除,并且布局压缩功能也未实现。该 API 保留,但应该弃用,您也应该如此对待它。

布局 1:基本列表

这是列表最简单、最常见的用法,所以没什么好说的。所有行的高度和布局都完全相同。为了满足这一需求,虚拟化控件绝对不会出错。它们都能很好地处理这种情况,即使显示 10,000 行也是如此。



<CollectionView ItemsSource="{Binding Products}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <v:ProductListItem />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>


Enter fullscreen mode Exit fullscreen mode


<ListView ItemsSource="{Binding Products}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <v:ProductListItem />
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>


Enter fullscreen mode Exit fullscreen mode

你可能想知道为什么我没有将上面的任何东西绑定到ProductListItemBindingContext在这种情况下(以及大多数情况下),它会自动传播给子级。这里提供的BindingContext是单个Product



<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:ffimageloading="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Maui"
             xmlns:m="clr-namespace:AllTheLists.Models"
             xmlns:vm="clr-namespace:AllTheLists.ViewModels"
             x:DataType="m:Product"
             x:Class="AllTheLists.Views.ProductListItem">
    <Grid Padding="16" ColumnDefinitions="80,*,40" ColumnSpacing="16">
        <ffimageloading:CachedImage 
            Source="{Binding ImageUrl}" 
            HeightRequest="80" 
            WidthRequest="80" 
            LoadingPlaceholder="https://via.placeholder.com/80" 
            ErrorPlaceholder="error.png">
        </ffimageloading:CachedImage>
        <VerticalStackLayout Grid.Column="1" Padding="10">
            <Label Text="{Binding Name}" FontSize="16" />
            <Label Text="{Binding Price, StringFormat='Price: {0:C}'}" FontSize="14" />
            <Label Text="{Binding Description}" FontSize="12" LineBreakMode="TailTruncation" />
        </VerticalStackLayout>      
        <CheckBox Grid.Column="2" VerticalOptions="Center" />  
    </Grid>
</ContentView>


Enter fullscreen mode Exit fullscreen mode

ListView除了和的示例之外CollectionView,我还查看了VirtualListViewRedth 和VirtualizeListViewMPowerKit 的示例。后者是一个完全跨平台的虚拟化控件,这是一种很有意思的方法。如果您的目标是跨平台一致性,那么它可能是一个不错的选择。

参考:

布局 2:评论 [行距不均匀]

PlugShare移动应用中的电动汽车充电站评论列表模拟了下一个示例。虽然模板并不复杂,但它确实包含一个长度可变的字符串,该字符串被包裹在 中Label。这在 .NET MAUI 的早期版本中存在问题,文本可能会被截断或溢出屏幕。默认情况下,ItemSizingStrategy仅测量第一个项目,并假设所有其余项目的大小相同。出于显而易见的原因,这样做性能更高。

为了适应可变的尺寸,我需要使用一种策略来测量所有项目或单独测量每个项目。实践中,这种方法效果很好,滚动非常流畅。



<CollectionView ItemsSource="{Binding Reviews}" ItemSizingStrategy="MeasureAllItems">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <v:ReviewListItem />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>


Enter fullscreen mode Exit fullscreen mode


<Grid ColumnDefinitions="40,*" 
    RowDefinitions="Auto,Auto"
    ColumnSpacing="8"
    Margin="16">
    <Image 
        Source="{Binding StatusImage}"
        Grid.Column="0" 
        Grid.RowSpan="2" 
        HeightRequest="20"
        WidthRequest="20"
        VerticalOptions="Start" 
        HorizontalOptions="Center"/>

    <VerticalStackLayout Grid.Column="1" Spacing="8">
        <Label 
            Text="{Binding Author}"
            FontSize="18"
            FontAttributes="Bold" />
        <Label Text="{Binding Comment}" MaxLines="5" Margin="0,0,0,8" />
        <Label Text="{Binding Car}" TextColor="Gray"/>
        <Label Text="{Binding ChargerType}" TextColor="Gray"/>
    </VerticalStackLayout>

    <Label Text="{Binding CreatedAt, StringFormat='{0:MM/dd/yyyy}'}" 
        Grid.Row="0" 
        Grid.Column="1" 
        FontSize="10"
        TextColor="Gray"
        HorizontalOptions="End" 
        VerticalOptions="Start" />

    <BoxView 
        HeightRequest="1" 
        BackgroundColor="LightGray"
        VerticalOptions="End" 
        Grid.Column="1"
        TranslationY="16" />
</Grid>


Enter fullscreen mode Exit fullscreen mode

参考:

布局 3:社交签到区 [行列不均,布局复杂]

在本示例中,我的灵感源自一款啤酒爱好者社交应用Untapped。它的“活动动态”会显示好友的啤酒签到记录,包括评分和可选的照片。如果显示照片,模板会略高一些,因此我再次需要处理行间距不均匀的问题。

在这种情况下,CollectionView具有明显的优势,ListView因为我能够通过调用来指定项目之间的间距LinearItemsLayout



<CollectionView 
    ItemSizingStrategy="MeasureAllItems"
    ItemsSource="{Binding CheckIns}">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <v:CheckInListItem />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>


Enter fullscreen mode Exit fullscreen mode

为了适应不同的外观,我可以选择DataTemplateSelectorHasImage ,但我选择向模型添加只读属性,以显示/隐藏Image控件以及调整内容的 Y 位置。



public class Product
{
    ///...

    public bool HasImage => !string.IsNullOrWhiteSpace(ImageUrl);
}


Enter fullscreen mode Exit fullscreen mode


<Border 
    Grid.Row="1"
    TranslationY="{Binding Product.HasImage, Converter={StaticResource BoolToIntConverter}}"


Enter fullscreen mode Exit fullscreen mode

我之前没用过BoolToObjectConverter.NET MAUI 社区工具包。真是个惊喜的发现!



<mct:BoolToObjectConverter 
    x:Key="BoolToIntConverter" 
    TrueObject="-60" 
    FalseObject="0"/>


Enter fullscreen mode Exit fullscreen mode

也非常适合翻转颜色。



<mct:BoolToObjectConverter 
    x:Key="BoolToColorBrushConverter" 
    TrueObject="#FFFFFF" 
    FalseObject="#000000"/>


Enter fullscreen mode Exit fullscreen mode

参考:

布局4:学习课程[扩展和收缩]

认识我的人都知道我热爱语言学习。我用过一款叫TEUIDA的应用,它界面简洁,以单元和课时的形式呈现课程。点击一个单元,会展开显示不同的课时,并以目录的形式呈现,就像路线图一样。

最初,我尝试使用CollectionViewListView,但这证实了 iOS 上 .NET MAUI 的一个错误,即运行时调整大小不会触发列表控件的其余部分按预期调整大小。从 8.0.60 版本开始,此功能在 Android 上可以正常使用。

当我评估要显示的内容时,我意识到我的数据并不多。在应用程序的每个页面上,我通常有四个单元,每个单元的章节和课程数量不定,但不会超过 10 个。

出于这些原因,我选择使用BindableLayout。事实上,这个示例使用了三个嵌套的BindableLayout。😲 这有问题吗?没有。

BindableLayout有点奇怪,回想起来,它应该像其他控件一样是一个独立的控件。但它实际上是一个附加属性,可以添加到任何其他布局中。因此,与其像 那样从控件开始并指定布局CollectionView,不如从您喜欢的布局开始,并在项目源和数据模板上进行标记。很简单。



<ScrollView>
    <VerticalStackLayout Spacing="10" 
        BindableLayout.ItemsSource="{Binding Items}">
        <BindableLayout.ItemTemplate>
            <DataTemplate>
                <v:LearningUnitListItem />
            </DataTemplate>
        </BindableLayout.ItemTemplate>
    </VerticalStackLayout>
</ScrollView>


Enter fullscreen mode Exit fullscreen mode

显示LearningUnitListItem主框和循环遍历章节和课程的隐藏列表。

为了扩大和缩小章节和课程列表,我只需使用点击处理程序并切换VerticalStackLayout包含该内容的可见性。

参考:

布局 5:谁在观看 [弹性布局]

受 Netflix、Disney+ 以及“插入其他流媒体服务”的启发,我制作了一个“谁在看”的示例。这个非常简单,它FlexLayout带有BindableLayout……



<FlexLayout 
    Direction="Row" 
    JustifyContent="Center" 
    Wrap="Wrap"
    BindableLayout.ItemsSource="{Binding WhoIsWatching}" 
    VerticalOptions="Center">
    <BindableLayout.ItemTemplate>
        <DataTemplate x:DataType="m:Contact">
            <VerticalStackLayout 
                HorizontalOptions="Center" 
                Spacing="8" 
                FlexLayout.Basis="40%" 
                FlexLayout.AlignSelf="Start">
                <Image 
                    Source="{Binding ProfilePicture}" 
                    WidthRequest="80" 
                    HeightRequest="80" 
                    Aspect="AspectFill"
                    BackgroundColor="Transparent">
                    <Image.Clip>
                        <EllipseGeometry Center="40, 40" RadiusX="40" RadiusY="40" />
                    </Image.Clip>
                </Image>
                <Label 
                    Text="{Binding FirstName}" 
                    HorizontalOptions="Center" />
            </VerticalStackLayout>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</FlexLayout>


Enter fullscreen mode Exit fullscreen mode

参考:

布局 6:邮箱 [扩展和收缩]

为了重现 iOS 版 Mail 中显示的邮箱 UI,我选择了.NET MAUI 社区工具包中BindableLayoutExpander。虽然用户最终可能会拥有多个邮件帐户,这些帐户会从虚拟化中受益,但从这里开始并在CollectionView必要时逐步扩展似乎是合理的。

由于我已经介绍过 的使用BindableLayout,现在我将重点介绍Expander。该控件主要包含两个部分:标题和内容。标题始终可见,而内容则根据用户交互显示/隐藏。

为了切换 V 形指示器的打开/关闭状态,我首先使用了两个Label控件来显示字体图标,并使用了相对源绑定来监视IsExpanded父控件的属性。由于我位于控件内部,因此我可以通过这种方式引用它,而不是通过名称。我将其重构为单个控件Label,并使用了 magnificent BoolToObjectConverter。如果没有它,我该如何编写代码呢?!



<mct:Expander>
    <mct:Expander.Header>
        <Grid ColumnDefinitions="*,100,50" RowDefinitions="40">
            <Label 
                Text="Ortinau" 
                Grid.Column="0" 
                FontSize="Subtitle" 
                VerticalOptions="Center" />
            <Label Text="38386" Grid.Column="1" 
                Style="{StaticResource SecondaryLabel}"
                HorizontalOptions="End" HorizontalTextAlignment="End" 
                IsVisible="{Binding Path=IsExpanded, Source={RelativeSource AncestorType={x:Type mct:Expander}}, Converter={StaticResource InvertedBoolConverter}}" />
            <Label 
                Text="{Binding Path=IsExpanded, Source={RelativeSource AncestorType={x:Type mct:Expander}},Converter={StaticResource BoolToChevronConverter}}" 
                FontSize="14" 
                FontFamily="FluentUI" 
                Style="{StaticResource SecondaryLabel}" 
                TextColor="{StaticResource ActionColor}"
                Grid.Column="2" 
                VerticalOptions="Center"                            
                HorizontalOptions="Center" />    
        </Grid>                    
    </mct:Expander.Header>

    <mct:Expander.Content>
        <Border>
            <VerticalStackLayout>
                <BindableLayout.ItemsSource>
                    ...
                </BindableLayout.ItemsSource>
                <BindableLayout.ItemTemplate>
                    <DataTemplate x:DataType="m:Mailbox">
                        <Grid 
                            ColumnDefinitions="60,*,100,50" 
                            RowDefinitions="40,1">
                            <Image 
                                Aspect="Center" 
                                HorizontalOptions="Center" 
                                VerticalOptions="Center">
                                <Image.Source>
                                    <FontImageSource 
                                        Glyph="{Binding Icon}" 
                                        FontFamily="FluentUI" 
                                        Size="18" 
                                        Color="{StaticResource ActionColor}" />
                                </Image.Source>
                            </Image> 
                            <Label 
                                Text="{Binding Name}" 
                                Grid.Column="1" 
                                FontSize="14" 
                                VerticalOptions="Center" />
                            <Label 
                                Text="{Binding UnreadCount}" 
                                Grid.Column="2" 
                                Style="{StaticResource SecondaryLabel}"
                                HorizontalOptions="End" 
                                HorizontalTextAlignment="End" />
                            <Label 
                                Text="{x:Static f:FluentUI.chevron_right_12_regular}" 
                                Grid.Column="3" 
                                Style="{StaticResource SecondaryLabel}"
                                VerticalOptions="Center"
                                FontSize="14" 
                                FontFamily="FluentUI" 
                                HorizontalOptions="Center" />

                            <BoxView 
                                Grid.ColumnSpan="4" 
                                Grid.Row="1" 
                                Margin="16,0,0,0" 
                                HeightRequest="1" 
                                Color="{AppThemeBinding Light=#f3f3f4, Dark=#333333}" />
                        </Grid>
                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </VerticalStackLayout>
        </Border>
    </mct:Expander.Content>
</mct:Expander>


Enter fullscreen mode Exit fullscreen mode

参考:

布局 7:联系人 [分组、搜索]

回到需要虚拟化、分组和搜索的样本,我重新创建了一个联系人列表。

标题

我的联系人需要出现在列表顶部,并在其他内容之前滚动显示。为此,我在 中添加了一个标题ListView。注意,它不需要 ,DataTemplate因为 只能有一个,而且不需要延迟实例化它。



<ListView.Header>
    <HorizontalStackLayout Spacing="16" Padding="16">
        <Border StrokeShape="RoundRectangle 40"
            StrokeThickness="0">
            <Image Source="avatar_01.png" 
            WidthRequest="80" 
            HeightRequest="80" 
            Aspect="AspectFill" 
            VerticalOptions="Center" 
            />
        </Border>
        <Label Text="David Ortinau" 
            FontSize="20" 
            FontAttributes="Bold" 
            VerticalOptions="Center" />
    </HorizontalStackLayout>        
</ListView.Header>


Enter fullscreen mode Exit fullscreen mode

分组

第一步是准备好可分组和可搜索的数据源。我的做法是,将所有联系人放入一个有序的平面列表中,按姓氏的首字母进行分组,然后将它们添加到已分组联系人列表中。最后一步是将分组联系人添加到一个未经筛选的新列表中,以便进行搜索。



_contacts = MockDataService.GenerateContacts().OrderBy(c => c.LastName).ThenBy(c => c.FirstName).ToList();

ContactsGroups = new List<ContactsGroup>();

var groupedContacts = _contacts.GroupBy(c => c.LastName[0]).OrderBy(g => g.Key);

foreach (var group in groupedContacts)
{
    var contactsGroup = new ContactsGroup(group.Key.ToString(), group.ToList());
    ContactsGroups.Add(contactsGroup);
}

_unfilteredContactsGroups = new List<ContactsGroup>(ContactsGroups);


Enter fullscreen mode Exit fullscreen mode

为了显示分组列表,我ListView主要选择了这个场景,因为这个场景是它设计的基本场景之一。为了分组,我设置IsGroupingEnabled="True"并提供了一个组标题的模板。



<ListView.GroupHeaderTemplate>
    <DataTemplate>
        <ViewCell>
            <Label Text="{Binding GroupName}" 
            FontSize="18" 
            FontAttributes="Bold"
            Padding="12,0,0,0"
            VerticalOptions="Center"
            Background="Transparent" />
        </ViewCell>
    </DataTemplate>
</ListView.GroupHeaderTemplate>


Enter fullscreen mode Exit fullscreen mode

就这样,我有了基本的分组列表。

搜索

.NET MAUI 提供了一个SearchBar控件,所以我把它添加到了ListView页面的上方。当​​用户输入时,SearchCommand就会执行。该Text属性默认为绑定,所以我不需要指定它,但直到我为了写这篇文章阅读了绑定模式的文档TwoWay后才确定。;)



<SearchBar 
    x:Name="SearchBar" 
    Placeholder="Search" 
    Text="{Binding SearchText, Mode=TwoWay}"
    SearchCommand="{Binding SearchCommand}" 
    VerticalOptions="Start" 
    BackgroundColor="{AppThemeBinding Light=White, Dark=Black}"
    />


Enter fullscreen mode Exit fullscreen mode

搜索命令过滤未过滤的列表并重新填充ContactsGroups与 绑定的ListView



[RelayCommand]
void Search()
{
    if (string.IsNullOrWhiteSpace(SearchText))
    {
        // If the search text is empty, show all contacts
        ContactsGroups = _unfilteredContactsGroups;
    }
    else
    {
        // If the search text is not empty, show only contacts that contain the search text
        ContactsGroups = _unfilteredContactsGroups
            .Where(g => g.Any(c => 
                c.FirstName.Contains(SearchText, StringComparison.InvariantCultureIgnoreCase) 
                || c.LastName.Contains(SearchText, StringComparison.InvariantCultureIgnoreCase)))
            .ToList();
    }
}


Enter fullscreen mode Exit fullscreen mode

但我遇到了一个问题,因为我输入了内容,列表也过滤了,但我还是得到了意想不到的结果。为什么?!

我向 Copilot 解释了我的情况,它(正如我所料)解释说,我只搜索了群组,而不是我预想的群组内的联系人。Copilot 提供了解决方案。



ContactsGroups = _unfilteredContactsGroups
    .Select(g => new ContactsGroup(g.GroupName, g.Where(c =>
        c.FirstName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
        || c.LastName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)).ToList()))
    .Where(g => g.Any())
    .ToList();


Enter fullscreen mode Exit fullscreen mode

参考:

布局 8:购物 [标题、数据模板选择器、无限滚动]

阿迪达斯应用的启发,我做这个应用时也挺有意思的。除了标题和用 ChatGPT 制作产品图片外,它的显示模式也很独特。一开始你以为它会是一个两列的网格布局,但过了四行之后,你发现一个产品横跨了两列。好吧,所以先是 4,然后是 1,对吧?错了。从那以后,就变成了 2 和 1。🤯

因为我需要在用户到达列表末尾时批量加载数据,所以我选择了CollectionView内置此功能的。

过滤器标头

因此标题很简单:一组水平滚动的按钮来过滤列表。



<CollectionView.Header>
    <v:FilterView />
</CollectionView.Header>


Enter fullscreen mode Exit fullscreen mode

FilterView.xaml



<Grid ColumnDefinitions="Auto,*" ColumnSpacing="16" Margin="16,16,-16,16">
    <Image 
        HeightRequest="24" 
        WidthRequest="24" 
        Aspect="Center"
        Background="Transparent">
        <Image.Source>
            <FontImageSource FontFamily="FontAwesome" 
                Glyph="{x:Static f:FontAwesome.Filter}" 
                Size="14" 
                Color="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray300}}"/>
        </Image.Source>
    </Image>
    <ScrollView Orientation="Horizontal" Grid.Column="1" HorizontalScrollBarVisibility="Never">
        <HorizontalStackLayout Spacing="8">
            <Button Text="705" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="SAMBA" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="GAZELLE" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="ULTRABOOST" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="ADIZERO" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="FORUM" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="SUPERSTAR" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="CAMPUS" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="LITE RACER" Style="{StaticResource FilterButtonStyle}" />
            <Button Text="2000S" Style="{StaticResource FilterButtonStyle}" />                
        </HorizontalStackLayout>
    </ScrollView>
</Grid>


Enter fullscreen mode Exit fullscreen mode

当然,在真正的应用程序中,按钮将来自某个集合,我会对BindableLayout它们使用。

时髦的布局模式

我该如何实现这种布局模式?我选择修改数据来表示它的显示方式。反正这就是 ViewModel 的用途。在 Copilot 的帮助下,我告诉它我需要实现的模式,然后观察代码的运行!我懂功夫!!!



_productDisplays = new List<ProductDisplay>();
for (int i = 0; i < count; i++)
{
    if (i < 4)
    {
        _productDisplays.Add(new ProductDisplay
        {
            Products = GenerateProducts().GetRange(i * 2, 2)
        });
    }
    else if (i % 3 == 1)
    {
        _productDisplays.Add(new ProductDisplay
        {
            Products = GenerateProducts().GetRange(i * 2 - 1, 1)
        });
    }
    else
    {
        _productDisplays.Add(new ProductDisplay
        {
            Products = GenerateProducts().GetRange(i * 2 - 2, 2)
        });
    }


Enter fullscreen mode Exit fullscreen mode

重复看起来GenerateProducts()就像是在一遍又一遍地重新生成数据,但实际上我返回的是缓存的数据集,一旦它被填充。我承认,这读起来不太顺畅。

现在我有了代表我需要的模式的数据,即 4:1:2:1:2:1:2:1 等,我可以转到数据模板。

默认情况下,它CollectionView实现了线性项目布局,这很好。使用数据模板选择器,我可以根据需要显示的项目数量选择两个模板:Mono 和 Duo。



public class ShopTemplateSelector : DataTemplateSelector
{
    public DataTemplate MonoTemplate { get; set; }
    public DataTemplate DuoTemplate { get; set; }
    public DataTemplate LoadingMoreTemplate { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        ProductDisplay productDisplay = (ProductDisplay)item;
        if(productDisplay.IsLoading)
        {
            return LoadingMoreTemplate;
        }

        return ((ProductDisplay)item).Products.Count < 2 ? MonoTemplate : DuoTemplate;
    }
}


Enter fullscreen mode Exit fullscreen mode

这个DuoTemplate更有趣,因为它只是MonoTemplate并排显示两个。



<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:v="clr-namespace:AllTheLists.Views"
             xmlns:m="clr-namespace:AllTheLists.Models"
             x:DataType="m:ProductDisplay"
             x:Class="AllTheLists.Views.DuoProductListItem">
    <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
        <v:MonoProductListItem Grid.Column="0" BindingContext="{Binding Products[0]}" />
        <v:MonoProductListItem Grid.Column="1" BindingContext="{Binding Products[1]}" />
    </Grid>
</ContentView>


Enter fullscreen mode Exit fullscreen mode

就这样,我得到了我需要的显示,而且我并不觉得它过于复杂。

无限滚动

当用户接近列表末尾时,我需要开始获取更多数据,并向用户显示一个指示。该指示应该显示在列表底部。

具有CollectionView有助于第一部分操作的属性。RemainingItemsThreshold当剩余项目数量达到指定数量时,它会通知控件,然后调用事件RemainingItemsThresholdReached并执行命令RemainingItemsThresholdReachedCommand。就我而言,我会同时使用后两者,但您可能只需要命令。下文将详细介绍我这样做的原因。



RemainingItemsThreshold="4"
RemainingItemsThresholdReached="CollectionView_RemainingItemsThresholdReached"
RemainingItemsThresholdReachedCommand="{Binding OnThresholdReachedCommand}"


Enter fullscreen mode Exit fullscreen mode

获取OnThresholdReachedCommand更多数据并将其附加到的末尾ObservableCollection



[RelayCommand]
async Task OnThresholdReached()
{
    if(IsLoadingMore) return;

    IsLoadingMore = true;

    VisibleProducts.Add(new ProductDisplay { IsLoading = true });

    await Task.Delay(4000); // fake a server call delay, allows the loading template to show

    VisibleProducts.Remove(VisibleProducts.Last());

    var newProducts = Products.Skip(VisibleProducts.Count).Take(16);
    foreach (var product in newProducts)
    {
        VisibleProducts.Add(product);
    }

    await Task.Delay(200); // tiny delay for a ui refresh
    IsLoadingMore = false;
}


Enter fullscreen mode Exit fullscreen mode

细心的读者会注意到上一节中数据模板选择器中的一些代码,这些代码现在与上面的命令相连接。一旦调用获取更多数据,就会创建一个空白ProductDisplay对象,该对象只有一个任务,用于告知用户IsLoading=true。在数据模板选择器中,我选择显示这个特殊模板并将其添加到列表底部。



if(productDisplay.IsLoading)
{
    return LoadingMoreTemplate;
}


Enter fullscreen mode Exit fullscreen mode

我的数据一到达,我就从集合中删除最后一项,并继续添加要显示的真实数据。

布尔IsLoadingMore值可以防止在方法执行过程中调用该方法。也许有更好的方法,但还是老习惯……

总而言之,为什么我还要用 来处理事件CollectionView_RemainingItemsThresholdReached?这是为了解决某个平台上命令无法执行的错误。



private void CollectionView_RemainingItemsThresholdReached(object sender, EventArgs e)
{
((ProductDisplaysViewModel)BindingContext).ThresholdReachedCommand.Execute(null);
}

Enter fullscreen mode Exit fullscreen mode




结论

总而言之,在为您的应用场景选择合适的控件时,您有很多选择!请考虑您的具体需求以及列表或布局所需的自定义级别。优先考虑CollectionViewListView不要忽略BindableLayout

写到这里,我不断看到有更多事情需要添加和尝试,比如编辑和整理清单。我想这就是明天该做的事情。

我的所有开发工作都是在 .NET 9 预览版上完成的,使用了VS Code Insiders以及 Macbook Pro M1 上VS Code 的 .NET MAUI 扩展的预发布版本。XAML IntelliSense 和 XAML/C# Hot Reload 的加入非常出色。

我最后要分享的建议是,在解决一个场景时,要考虑所有选项。选择控件只是其中之一,调整数据是另一个,调整用户体验模式又是另一个。虽然技术可能不够灵活,有时会对你不利,但与其试图强行取得成功,不如记住,你是灵活的!我发现,无论我使用什么语言或技术,这都是成功的关键。

希望这篇文章读起来有趣,并且您能从中获得一些启发。也许您有更好的方法,或者您不喜欢我的做法。代码本身可能非常个人化。无论您的反应如何,请充满能量地去创造一些精彩的东西,与世界分享。

文章来源:https://dev.to/davidortinau/all-the-lists-in-net-maui-33bd
PREV
5 个 PHP 工具让您的生活更加愉快。
NEXT
XState:4.7 版及未来 XState 的未来