.NET MAUI CollectionView útmutató: Alapoktól a haladó szintig

Átfogó útmutató a .NET MAUI CollectionView-hoz: elrendezések, MVVM, csoportosítás, végtelen görgetés, SwipeView, pull-to-refresh, teljesítménytippek és ListView migrációs lépések kódpéldákkal.

Bevezetés

Ha .NET MAUI-val fejlesztesz, akkor a CollectionView szinte biztosan az egyik legtöbbet használt vezérlőelemed lesz. Lényegében ez teszi lehetővé, hogy adatgyűjteményeket hatékonyan jeleníts meg — legyen szó listás, rácsos vagy akár teljesen egyedi elrendezésről. És ami igazán fontos: a .NET 10 megjelenésével a Microsoft hivatalosan is elavulttá (deprecated) nyilvánította a ListView-t, szóval a CollectionView már nem csupán egy jó választás, hanem az egyetlen ajánlott megoldás.

A .NET 10-ben ráadásul a CollectionView2 handlerek váltak alapértelmezetté, ami komoly teljesítménybeli ugrást jelent. Ez az új handler-réteg natívabb integrációt biztosít az egyes platformokkal (Android, iOS, Windows, macOS), gyorsabb renderelést és jobb memóriakezelést kínálva.

Ha korábban ListView-t használtál, most tényleg itt az ideje átállni. Ez az útmutató pontosan ebben segít.

Végigmegyünk a CollectionView minden fontos funkcióján: az alapvető beállítástól a haladó mintákon át egészen a teljesítményoptimalizálásig és a ListView-ról való migrációig. Minden szakaszhoz gyakorlati kódpéldákat találsz, amelyeket azonnal fel tudsz használni a saját projektjeidben.

A CollectionView alapjai

A CollectionView használatának megkezdéséhez igazából két alapvető tulajdonságot kell ismerni: az ItemsSource-ot (ez határozza meg az adatforrást) és az ItemTemplate-et (ez definiálja az egyes elemek kinézetét). Az ItemsSource bármilyen IEnumerable típust elfogad, de a legtöbb esetben ObservableCollection<T>-t használunk, hogy az adatváltozások automatikusan megjelenjenek a felületen.

Az ItemTemplate egy DataTemplate objektumot vár, amelyben tetszőleges MAUI vezérlőelemeket helyezhetünk el. Fontos különbség a ListView-hoz képest: a CollectionView nem igényel ViewCell burkolóelemet — közvetlenül elhelyezhetjük a tartalom-elrendezést a DataTemplate-ben. Ez egyébként nem csak egyszerűbb, de gyorsabb is.

<!-- Alapvető CollectionView beállítás XAML-ben -->
<CollectionView x:Name="termekLista">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Termek">
            <Grid Padding="10" ColumnDefinitions="60,*">
                <Image Source="{Binding KepUrl}"
                       WidthRequest="50"
                       HeightRequest="50"
                       Aspect="AspectFill" />
                <VerticalStackLayout Grid.Column="1" Spacing="4">
                    <Label Text="{Binding Nev}"
                           FontSize="16"
                           FontAttributes="Bold" />
                    <Label Text="{Binding Leiras}"
                           FontSize="13"
                           TextColor="Gray"
                           LineBreakMode="TailTruncation" />
                    <Label Text="{Binding Ar, StringFormat='{0:N0} Ft'}"
                           FontSize="14"
                           TextColor="Green" />
                </VerticalStackLayout>
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

A mögöttes C# kódban az adatmodellt és az adatforrás beállítását valahogy így végezhetjük el:

// Termék adatmodell
public class Termek
{
    public string Nev { get; set; }
    public string Leiras { get; set; }
    public decimal Ar { get; set; }
    public string KepUrl { get; set; }
}

// Az adatforrás beállítása a kód mögötti fájlban
public partial class TermekekOldal : ContentPage
{
    public TermekekOldal()
    {
        InitializeComponent();

        // Terméklista feltöltése példaadatokkal
        var termekek = new List<Termek>
        {
            new Termek
            {
                Nev = "Vezeték nélküli egér",
                Leiras = "Ergonomikus kialakítás, 2.4GHz",
                Ar = 8990,
                KepUrl = "eger.png"
            },
            new Termek
            {
                Nev = "Mechanikus billentyűzet",
                Leiras = "RGB háttérvilágítás, Cherry MX kapcsolók",
                Ar = 24990,
                KepUrl = "bill.png"
            },
            new Termek
            {
                Nev = "USB-C Hub",
                Leiras = "7 az 1-ben, HDMI, SD kártya, USB 3.0",
                Ar = 12490,
                KepUrl = "hub.png"
            }
        };

        termekLista.ItemsSource = termekek;
    }
}

Ez a minimális beállítás már egy teljesen működő, görgethető listát eredményez. A x:DataType attribútum használata a DataTemplate-ben lehetővé teszi a fordítási idejű kötés-ellenőrzést (compiled bindings), ami nemcsak a hibák korai felismerésében segít, hanem a teljesítményt is javítja. Őszintén szólva, nincs ok kihagyni.

Elrendezési lehetőségek (Layouts)

A CollectionView egyik legnagyobb előnye a ListView-val szemben a rugalmas elrendezési lehetőségek. Alapértelmezetten függőleges listát kapunk, de könnyedén válthatunk vízszintes elrendezésre vagy rácsszerű megjelenítésre is. Nézzük meg ezeket részletesebben.

LinearItemsLayout — Függőleges és vízszintes lista

A LinearItemsLayout egydimenziós listát hoz létre — lehet függőleges (ami az alapértelmezett) vagy vízszintes irányú. Az ItemSpacing tulajdonsággal szabályozhatjuk az elemek közötti térközt.

<!-- Vízszintes elrendezés egyedi térközzel -->
<CollectionView ItemsSource="{Binding Kategoriak}">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout Orientation="Horizontal"
                           ItemSpacing="12" />
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Kategoria">
            <Frame WidthRequest="140"
                   HeightRequest="100"
                   CornerRadius="12"
                   Padding="10"
                   BackgroundColor="{Binding Szin}">
                <VerticalStackLayout VerticalOptions="Center"
                                     HorizontalOptions="Center">
                    <Image Source="{Binding IkonUrl}"
                           WidthRequest="36"
                           HeightRequest="36"
                           HorizontalOptions="Center" />
                    <Label Text="{Binding Nev}"
                           HorizontalTextAlignment="Center"
                           FontSize="13"
                           TextColor="White" />
                </VerticalStackLayout>
            </Frame>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

GridItemsLayout — Rácsos elrendezés

A GridItemsLayout lehetővé teszi az elemek rácsszerű elrendezését meghatározott oszlop- vagy sorszámmal. A Span tulajdonság adja meg az oszlopok számát (függőleges orientáció esetén), a VerticalItemSpacing és HorizontalItemSpacing pedig a térközöket szabályozza.

<!-- Kétoszlopos rácsos elrendezés -->
<CollectionView ItemsSource="{Binding Termekek}">
    <CollectionView.ItemsLayout>
        <GridItemsLayout Orientation="Vertical"
                         Span="2"
                         VerticalItemSpacing="10"
                         HorizontalItemSpacing="10" />
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Termek">
            <Border StrokeShape="RoundRectangle 12"
                    StrokeThickness="0"
                    BackgroundColor="White"
                    Padding="8">
                <Border.Shadow>
                    <Shadow Brush="Gray" Opacity="0.2" Radius="8" />
                </Border.Shadow>
                <VerticalStackLayout Spacing="6">
                    <Image Source="{Binding KepUrl}"
                           HeightRequest="120"
                           Aspect="AspectFill" />
                    <Label Text="{Binding Nev}"
                           FontAttributes="Bold"
                           FontSize="14" />
                    <Label Text="{Binding Ar, StringFormat='{0:N0} Ft'}"
                           TextColor="Green"
                           FontSize="13" />
                </VerticalStackLayout>
            </Border>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Egy hasznos tipp: a Span értéke futásidőben is módosítható, szóval az eszköz tájolásának változásakor simán átválthatunk 2 oszlopról 3-ra. Ez reszponzív felületeknél nagyon jól jön.

Adatkötés és MVVM minta

Modern .NET MAUI alkalmazásoknál az MVVM (Model-View-ViewModel) architektúraminta használata az ajánlott megközelítés — és nem véletlenül. A CollectionView tökéletesen illeszkedik ehhez, mivel minden fontos tulajdonsága támogatja az adatkötést. Az ObservableCollection<T> használata biztosítja, hogy az elemek hozzáadása vagy eltávolítása automatikusan frissítse a felületet.

A CommunityToolkit.Mvvm NuGet csomag — amit személyes tapasztalatom alapján csak ajánlani tudok — jelentősen egyszerűsíti a ViewModel-ek létrehozását forrásgenerátorok segítségével. Az [ObservableProperty] attribútum automatikusan generálja az INotifyPropertyChanged implementációt, míg a [RelayCommand] az ICommand implementációkat hozza létre. Rengeteg boilerplate kódot spórol meg.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

// ViewModel a terméklistához MVVM mintával
public partial class TermekekViewModel : ObservableObject
{
    private readonly ITermekSzolgaltatas _szolgaltatas;

    // Az ObservableProperty attribútum automatikusan generálja
    // a tulajdonságot és az értesítési mechanizmust
    [ObservableProperty]
    private bool _betoltes;

    [ObservableProperty]
    private string _keresesiKifejezes;

    public ObservableCollection<Termek> Termekek { get; } = new();

    public TermekekViewModel(ITermekSzolgaltatas szolgaltatas)
    {
        _szolgaltatas = szolgaltatas;
    }

    // Termékek betöltése a szolgáltatásból
    [RelayCommand]
    private async Task TermekekBetolteseAsync()
    {
        if (Betoltes) return;

        try
        {
            Betoltes = true;
            var termekek = await _szolgaltatas.GetTermekekAsync();

            Termekek.Clear();
            foreach (var termek in termekek)
            {
                Termekek.Add(termek);
            }
        }
        catch (Exception ex)
        {
            // Hibakezelés - értesítés a felhasználónak
            await Shell.Current.DisplayAlert(
                "Hiba",
                $"Nem sikerült betölteni a termékeket: {ex.Message}",
                "Rendben");
        }
        finally
        {
            Betoltes = false;
        }
    }

    // Termék törlése a listából
    [RelayCommand]
    private async Task TermekTorleseAsync(Termek termek)
    {
        bool megerosites = await Shell.Current.DisplayAlert(
            "Törlés megerősítése",
            $"Biztosan törölni szeretnéd a(z) '{termek.Nev}' terméket?",
            "Igen", "Nem");

        if (megerosites)
        {
            await _szolgaltatas.TermekTorleseAsync(termek.Id);
            Termekek.Remove(termek);
        }
    }
}

A hozzá tartozó XAML nézet az adatkötéseket a ViewModel tulajdonságaihoz és parancsaihoz kapcsolja:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MauiApp.ViewModels"
             x:Class="MauiApp.Views.TermekekOldal"
             x:DataType="vm:TermekekViewModel"
             Title="Termékek">

    <Grid RowDefinitions="Auto,*">
        <!-- Keresőmező -->
        <SearchBar Text="{Binding KeresesiKifejezes}"
                   Placeholder="Termék keresése..."
                   Margin="10" />

        <!-- Termék lista -->
        <CollectionView Grid.Row="1"
                        ItemsSource="{Binding Termekek}">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:Termek">
                    <Grid Padding="12,8" ColumnDefinitions="50,*,Auto">
                        <Image Source="{Binding KepUrl}"
                               WidthRequest="45"
                               HeightRequest="45" />
                        <VerticalStackLayout Grid.Column="1"
                                             VerticalOptions="Center">
                            <Label Text="{Binding Nev}"
                                   FontAttributes="Bold" />
                            <Label Text="{Binding Ar, StringFormat='{0:N0} Ft'}"
                                   TextColor="Gray" />
                        </VerticalStackLayout>
                        <Button Grid.Column="2"
                                Text="Törlés"
                                BackgroundColor="Red"
                                TextColor="White"
                                Command="{Binding Source={RelativeSource AncestorType={x:Type vm:TermekekViewModel}}, Path=TermekTorleseCommand}"
                                CommandParameter="{Binding .}" />
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <!-- Betöltés jelző -->
        <ActivityIndicator Grid.RowSpan="2"
                           IsRunning="{Binding Betoltes}"
                           IsVisible="{Binding Betoltes}"
                           HorizontalOptions="Center"
                           VerticalOptions="Center" />
    </Grid>
</ContentPage>

Figyeld meg a DataTemplate-en belüli gombnál a RelativeSource kötés használatát. Mivel a DataTemplate kontextusa az egyes Termek objektum, a ViewModel parancsaihoz felfelé kell navigálnunk a vizuális fában. Ez egy gyakori minta — eleinte kicsit fura lehet, de gyorsan hozzá fogsz szokni.

DataTemplateSelector használata

Mi van, ha a CollectionView-ban különböző típusú elemeket szeretnél eltérő megjelenéssel ellátni? Erre szolgál a DataTemplateSelector, ami futásidőben dönti el, melyik DataTemplate-et kell alkalmazni. Rendkívül hasznos például chat alkalmazásoknál (saját üzenet vs. kapott üzenet), vegyes tartalomlistáknál vagy különböző státuszú elemeknél.

// Egyedi DataTemplateSelector megvalósítása
// Ez a selektor különböző sablonokat választ az üzenet típusa alapján
public class UzenetSablonValaszto : DataTemplateSelector
{
    // Saját üzenet sablonja - jobb oldalon jelenik meg
    public DataTemplate SajatUzenetSablon { get; set; }

    // Kapott üzenet sablonja - bal oldalon jelenik meg
    public DataTemplate KapottUzenetSablon { get; set; }

    // Rendszerüzenet sablonja - középen jelenik meg
    public DataTemplate RendszerUzenetSablon { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        if (item is Uzenet uzenet)
        {
            return uzenet.Tipus switch
            {
                UzenetTipus.Sajat => SajatUzenetSablon,
                UzenetTipus.Kapott => KapottUzenetSablon,
                UzenetTipus.Rendszer => RendszerUzenetSablon,
                _ => KapottUzenetSablon
            };
        }

        return KapottUzenetSablon;
    }
}

// Az üzenet modell és típus felsorolás
public enum UzenetTipus
{
    Sajat,
    Kapott,
    Rendszer
}

public class Uzenet
{
    public string Szoveg { get; set; }
    public DateTime Idopont { get; set; }
    public UzenetTipus Tipus { get; set; }
    public string FeladoNev { get; set; }
}

A XAML-ben a selectort erőforrásként definiáljuk, az egyes sablonokkal együtt:

<ContentPage.Resources>
    <!-- Saját üzenet sablonja -->
    <DataTemplate x:Key="sajatSablon" x:DataType="models:Uzenet">
        <Grid Padding="10,4" ColumnDefinitions="*,Auto">
            <Frame Grid.Column="1"
                   BackgroundColor="#DCF8C6"
                   CornerRadius="10"
                   Padding="10"
                   MaximumWidthRequest="280">
                <VerticalStackLayout>
                    <Label Text="{Binding Szoveg}" />
                    <Label Text="{Binding Idopont, StringFormat='{0:HH:mm}'}"
                           FontSize="10" TextColor="Gray"
                           HorizontalOptions="End" />
                </VerticalStackLayout>
            </Frame>
        </Grid>
    </DataTemplate>

    <!-- Kapott üzenet sablonja -->
    <DataTemplate x:Key="kapottSablon" x:DataType="models:Uzenet">
        <Grid Padding="10,4" ColumnDefinitions="Auto,*">
            <Frame BackgroundColor="White"
                   CornerRadius="10"
                   Padding="10"
                   MaximumWidthRequest="280">
                <VerticalStackLayout>
                    <Label Text="{Binding FeladoNev}"
                           FontAttributes="Bold"
                           FontSize="12"
                           TextColor="DarkBlue" />
                    <Label Text="{Binding Szoveg}" />
                    <Label Text="{Binding Idopont, StringFormat='{0:HH:mm}'}"
                           FontSize="10" TextColor="Gray"
                           HorizontalOptions="End" />
                </VerticalStackLayout>
            </Frame>
        </Grid>
    </DataTemplate>

    <!-- Sablon választó regisztrálása -->
    <selectors:UzenetSablonValaszto x:Key="uzenetValaszto"
        SajatUzenetSablon="{StaticResource sajatSablon}"
        KapottUzenetSablon="{StaticResource kapottSablon}" />
</ContentPage.Resources>

<CollectionView ItemsSource="{Binding Uzenetek}"
                ItemTemplate="{StaticResource uzenetValaszto}" />

Egy fontos dolog a DataTemplateSelector kapcsán: az OnSelectTemplate metódus minden elem megjelenítésekor meghívódik, tehát kerüld a bonyolult logikát ebben a metódusban. Ha lassú, az az egész görgetést fogja akadoztatni.

Csoportosítás (Grouping)

A CollectionView beépítetten támogatja az elemek csoportosított megjelenítését. Ehhez az IsGrouped tulajdonságot kell True-ra állítani, és a GroupHeaderTemplate-ben megadni a fejléc kinézetét. Az adatmodellnek persze hierarchikus struktúrát kell követnie: a csoportok maguk is gyűjtemények.

// Csoportosított adatmodell
// A csoport maga is egy gyűjtemény, amely tartalmazza az elemeket
public class TermekCsoport : List<Termek>
{
    public string KategoriaNev { get; set; }
    public int OsszesTermek => Count;

    public TermekCsoport(string kategoriaNev, List<Termek> termekek)
        : base(termekek)
    {
        KategoriaNev = kategoriaNev;
    }
}

// Csoportok létrehozása a ViewModel-ben
public ObservableCollection<TermekCsoport> CsoportosítottTermekek { get; } = new()
{
    new TermekCsoport("Elektronika", new List<Termek>
    {
        new Termek { Nev = "Laptop", Ar = 299990 },
        new Termek { Nev = "Tablet", Ar = 149990 },
        new Termek { Nev = "Okostelefon", Ar = 199990 }
    }),
    new TermekCsoport("Kiegészítők", new List<Termek>
    {
        new Termek { Nev = "Tok", Ar = 4990 },
        new Termek { Nev = "Töltő", Ar = 6990 },
        new Termek { Nev = "Fülhallgató", Ar = 12990 }
    }),
    new TermekCsoport("Szoftverek", new List<Termek>
    {
        new Termek { Nev = "Vírusvédelem", Ar = 9990 },
        new Termek { Nev = "Office csomag", Ar = 29990 }
    })
};
<!-- Csoportosított CollectionView -->
<CollectionView ItemsSource="{Binding CsoportosítottTermekek}"
                IsGrouped="True">

    <!-- Csoport fejléc sablon -->
    <CollectionView.GroupHeaderTemplate>
        <DataTemplate x:DataType="models:TermekCsoport">
            <Grid Padding="12,8"
                  BackgroundColor="#F0F0F0"
                  ColumnDefinitions="*,Auto">
                <Label Text="{Binding KategoriaNev}"
                       FontAttributes="Bold"
                       FontSize="16"
                       VerticalOptions="Center" />
                <Label Grid.Column="1"
                       Text="{Binding OsszesTermek, StringFormat='{0} termék'}"
                       FontSize="12"
                       TextColor="Gray"
                       VerticalOptions="Center" />
            </Grid>
        </DataTemplate>
    </CollectionView.GroupHeaderTemplate>

    <!-- Elem sablon -->
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Termek">
            <Grid Padding="16,10" ColumnDefinitions="*,Auto">
                <Label Text="{Binding Nev}" FontSize="14" />
                <Label Grid.Column="1"
                       Text="{Binding Ar, StringFormat='{0:N0} Ft'}"
                       TextColor="Green" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

A csoportosítás támogatja a GroupFooterTemplate tulajdonságot is, amivel a csoportok lábléce is testreszabható. Ez jól jöhet részösszegek vagy statisztikák megjelenítéséhez.

Kijelölés kezelése (Selection)

A CollectionView három kijelölési módot kínál a SelectionMode tulajdonságon keresztül: None (nincs kijelölés), Single (egyszeres) és Multiple (többszörös). A kijelölés változásait a SelectionChanged eseménnyel vagy az MVVM-barát adatkötéssel kezelhetjük.

Egyszeres kijelölésnél a SelectedItem tulajdonságot köthetjük a ViewModel-ünk egy tulajdonságához. Többszörös kijelölésnél a SelectedItems egy IList<object> gyűjteményt ad vissza.

<!-- Egyszeres kijelölés adatkötéssel -->
<CollectionView ItemsSource="{Binding Termekek}"
                SelectionMode="Single"
                SelectedItem="{Binding KivalasztottTermek}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Termek">
            <Grid Padding="12,10">
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup Name="CommonStates">
                        <!-- Normál állapot -->
                        <VisualState Name="Normal">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="Transparent" />
                            </VisualState.Setters>
                        </VisualState>
                        <!-- Kijelölt állapot egyedi stílussal -->
                        <VisualState Name="Selected">
                            <VisualState.Setters>
                                <Setter Property="BackgroundColor" Value="#E3F2FD" />
                            </VisualState.Setters>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
                <Label Text="{Binding Nev}" FontSize="16" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

A VisualStateManager segítségével testreszabhatjuk a kijelölt elemek vizuális megjelenését. A fenti példában a kijelölt elem halványkék hátteret kap. Fontos: a VisualStateManager-t a DataTemplate gyökér eleméhez kell hozzáadni, nem magához a DataTemplate-hez.

Az MVVM mintában a kijelölés változásait a ViewModel-ben a tulajdonság setter-jében kezelhetjük:

// Kijelölés kezelése a ViewModel-ben
[ObservableProperty]
private Termek _kivalasztottTermek;

// A CommunityToolkit.Mvvm automatikusan generál egy
// OnKivalasztottTermekChanged partial metódust
partial void OnKivalasztottTermekChanged(Termek value)
{
    if (value is not null)
    {
        // Navigálás a termék részletek oldalra
        Shell.Current.GoToAsync($"termekReszletek",
            new Dictionary<string, object>
            {
                { "Termek", value }
            });
    }
}

Húzd le a frissítéshez (Pull-to-Refresh)

A „húzd le a frissítéshez" funkció a mobilalkalmazások egyik legismertebb interakciós mintája — a felhasználók szinte reflexszerűen használják. A .NET MAUI-ban ezt a RefreshView vezérlővel valósíthatjuk meg, amely körülöleli a CollectionView-t.

A RefreshView két kulcsfontosságú tulajdonsággal rendelkezik: az IsRefreshing jelzi, hogy folyamatban van-e a frissítés, és a Command határozza meg a végrehajtandó műveletet.

<!-- RefreshView a CollectionView körül -->
<RefreshView IsRefreshing="{Binding Frissites}"
             Command="{Binding FrissitesCommand}"
             RefreshColor="DodgerBlue">
    <CollectionView ItemsSource="{Binding Hirek}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="models:Hir">
                <Grid Padding="12" ColumnDefinitions="80,*" ColumnSpacing="10">
                    <Image Source="{Binding BoritokepUrl}"
                           HeightRequest="60"
                           Aspect="AspectFill" />
                    <VerticalStackLayout Grid.Column="1" Spacing="4">
                        <Label Text="{Binding Cim}"
                               FontAttributes="Bold"
                               LineBreakMode="TailTruncation"
                               MaxLines="2" />
                        <Label Text="{Binding KozzetetelDatum, StringFormat='{0:yyyy.MM.dd HH:mm}'}"
                               FontSize="11"
                               TextColor="Gray" />
                    </VerticalStackLayout>
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</RefreshView>
// Frissítés parancs a ViewModel-ben
[ObservableProperty]
private bool _frissites;

[RelayCommand]
private async Task FrissitesAsync()
{
    try
    {
        // Legfrissebb hírek lekérése a szerverről
        var ujHirek = await _hirSzolgaltatas.GetLegfrissebbHirekAsync();

        Hirek.Clear();
        foreach (var hir in ujHirek)
        {
            Hirek.Add(hir);
        }
    }
    catch (Exception ex)
    {
        await Shell.Current.DisplayAlert(
            "Hiba",
            "Nem sikerült frissíteni a híreket.",
            "Rendben");
    }
    finally
    {
        // Fontos: mindig állítsuk vissza false-ra,
        // különben a frissítés animáció nem áll le
        Frissites = false;
    }
}

Ezt nem hangsúlyozhatom eléggé: a Frissites (IsRefreshing) tulajdonságot mindig állítsd vissza false-ra a művelet végén, hiba esetén is. Ezért van a finally blokkban — ha elfelejtjük, a forgó animáció a végtelenségig pörög. Ezt a hibát sajnos egyszer mindenkinek el kell követnie.

Végtelen görgetés (Infinite Scrolling)

A végtelen görgetés (vagy inkrementális betöltés) lehetővé teszi, hogy az adatokat kisebb adagokban töltsük be, ahogy a felhasználó közeledik a lista végéhez. Ez javítja a válaszidőt és csökkenti a memóriahasználatot, mert nem kell egyszerre mindent a memóriában tartani.

A CollectionView két kulcstulajdonságot biztosít ehhez: a RemainingItemsThreshold meghatározza, hány elem maradjon a lista végéig mielőtt az új adagot betöltjük, és a RemainingItemsThresholdReachedCommand a betöltést végző parancsra mutat.

// Végtelen görgetés megvalósítása a ViewModel-ben
public partial class CikkekViewModel : ObservableObject
{
    private readonly ICikkSzolgaltatas _szolgaltatas;
    private int _aktualisOldal = 0;
    private const int OldalMeret = 20;
    private bool _vanMegTobb = true;

    [ObservableProperty]
    private bool _tovabiakBetoltese;

    public ObservableCollection<Cikk> Cikkek { get; } = new();

    public CikkekViewModel(ICikkSzolgaltatas szolgaltatas)
    {
        _szolgaltatas = szolgaltatas;
    }

    // Következő oldal betöltése görgetéskor
    [RelayCommand]
    private async Task TovabbiBetoltesAsync()
    {
        // Elkerüljük a párhuzamos betöltéseket
        if (TovabiakBetoltese || !_vanMegTobb) return;

        try
        {
            TovabiakBetoltese = true;
            _aktualisOldal++;

            var ujCikkek = await _szolgaltatas
                .GetCikkekAsync(_aktualisOldal, OldalMeret);

            // Ha kevesebb elem jött, mint az oldal mérete,
            // akkor nincs több betölthető adat
            if (ujCikkek.Count < OldalMeret)
            {
                _vanMegTobb = false;
            }

            foreach (var cikk in ujCikkek)
            {
                Cikkek.Add(cikk);
            }
        }
        catch (Exception ex)
        {
            // Hiba esetén visszalépünk az oldalszámmal,
            // hogy újra lehessen próbálni
            _aktualisOldal--;
        }
        finally
        {
            TovabiakBetoltese = false;
        }
    }

    // Teljes lista újratöltése (pl. pull-to-refresh esetén)
    [RelayCommand]
    private async Task UjratoltesAsync()
    {
        _aktualisOldal = 0;
        _vanMegTobb = true;
        Cikkek.Clear();
        await TovabbiBetoltesAsync();
    }
}
<!-- Végtelen görgetés XAML beállítása -->
<CollectionView ItemsSource="{Binding Cikkek}"
                RemainingItemsThreshold="5"
                RemainingItemsThresholdReachedCommand="{Binding TovabbiBetoltesCommand}">

    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Cikk">
            <Grid Padding="12,8">
                <Label Text="{Binding Cim}" FontSize="15" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>

    <!-- Betöltés jelző a lista alján -->
    <CollectionView.Footer>
        <ActivityIndicator IsRunning="{Binding TovabiakBetoltese}"
                           IsVisible="{Binding TovabiakBetoltese}"
                           Margin="0,10"
                           HorizontalOptions="Center" />
    </CollectionView.Footer>
</CollectionView>

A RemainingItemsThreshold értékét érdemes az oldal méretéhez igazítani. Ha 20 elemet töltünk be oldalanként, a küszöbértéket 5 körülire érdemes állítani — így lesz elég idő az új adag betöltésére, mielőtt a felhasználó eléri a lista végét. A Footer tulajdonság remek hely egy betöltés indikátor elhelyezésére.

SwipeView integrálása

A SwipeView vezérlő kontextusműveleteket (swipe actions) tesz lehetővé az egyes listaelemeken. Balra vagy jobbra húzva felfedheti a rejtett műveletgombokat — törlés, archiválás, szerkesztés, amit csak akarsz. A SwipeView-t a CollectionView ItemTemplate-jén belül kell elhelyezni.

<!-- SwipeView műveletek a CollectionView elemein -->
<CollectionView ItemsSource="{Binding Emailek}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Email">
            <SwipeView>
                <!-- Balra húzás: Törlés -->
                <SwipeView.RightItems>
                    <SwipeItems Mode="Execute">
                        <SwipeItem Text="Törlés"
                                   BackgroundColor="Red"
                                   IconImageSource="torles_ikon.png"
                                   Command="{Binding Source={RelativeSource AncestorType={x:Type vm:EmailekViewModel}}, Path=EmailTorleseCommand}"
                                   CommandParameter="{Binding .}" />
                    </SwipeItems>
                </SwipeView.RightItems>

                <!-- Jobbra húzás: Archiválás és Megjelölés -->
                <SwipeView.LeftItems>
                    <SwipeItems>
                        <SwipeItem Text="Archiválás"
                                   BackgroundColor="DarkGreen"
                                   IconImageSource="archiv_ikon.png"
                                   Command="{Binding Source={RelativeSource AncestorType={x:Type vm:EmailekViewModel}}, Path=ArchivalasCommand}"
                                   CommandParameter="{Binding .}" />
                        <SwipeItem Text="Megjelölés"
                                   BackgroundColor="Orange"
                                   IconImageSource="csillag_ikon.png"
                                   Command="{Binding Source={RelativeSource AncestorType={x:Type vm:EmailekViewModel}}, Path=MegjelolesCommand}"
                                   CommandParameter="{Binding .}" />
                    </SwipeItems>
                </SwipeView.LeftItems>

                <!-- Az elem tényleges tartalma -->
                <Grid Padding="14,10"
                      ColumnDefinitions="Auto,*,Auto"
                      BackgroundColor="White">
                    <BoxView WidthRequest="8"
                             HeightRequest="8"
                             CornerRadius="4"
                             Color="{Binding OlvasottSzin}"
                             VerticalOptions="Center" />
                    <VerticalStackLayout Grid.Column="1" Spacing="2" Margin="10,0">
                        <Label Text="{Binding Felado}"
                               FontAttributes="Bold" />
                        <Label Text="{Binding Targy}"
                               FontSize="13" />
                        <Label Text="{Binding Elonezet}"
                               FontSize="12"
                               TextColor="Gray"
                               LineBreakMode="TailTruncation" />
                    </VerticalStackLayout>
                    <Label Grid.Column="2"
                           Text="{Binding Datum, StringFormat='{0:MM.dd}'}"
                           FontSize="11"
                           TextColor="Gray"
                           VerticalOptions="Start" />
                </Grid>
            </SwipeView>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

A SwipeItems.Mode tulajdonság két értéket vehet fel: Reveal (alapértelmezett), amely megjeleníti a gombokat, amelyekre a felhasználó kattinthat; és Execute, amely a teljes elhúzásnál automatikusan végrehajtja az első műveletet. Törléshez az Execute mód az ideális, több műveletnél viszont inkább a Reveal-t ajánlom.

Üres nézet (EmptyView)

Amikor a CollectionView adatforrása üres — akár mert nincs adat, akár mert egy szűrés nem hozott eredményt —, érdemes informatív visszajelzést adni a felhasználónak. Erre szolgál az EmptyView tulajdonság. Őszintén szólva, ez egy apróság, de hatalmas különbséget jelent a felhasználói élményben.

Az egyszerűbb esetben elegendő egy szöveg:

<!-- Egyszerű szöveges üres nézet -->
<CollectionView ItemsSource="{Binding SzurtTermekek}"
                EmptyView="Nem található termék a keresési feltételeknek megfelelően." />

Összetettebb megoldáshoz az EmptyViewTemplate-et használhatjuk:

<!-- Testreszabott üres nézet sablonnal -->
<CollectionView ItemsSource="{Binding SzurtTermekek}">
    <CollectionView.EmptyViewTemplate>
        <DataTemplate>
            <VerticalStackLayout HorizontalOptions="Center"
                                 VerticalOptions="Center"
                                 Spacing="12"
                                 Padding="30">
                <Image Source="ures_lista.png"
                       WidthRequest="120"
                       HeightRequest="120"
                       Opacity="0.5"
                       HorizontalOptions="Center" />
                <Label Text="Nincs megjeleníthető elem"
                       FontSize="18"
                       FontAttributes="Bold"
                       HorizontalTextAlignment="Center" />
                <Label Text="Próbáld meg módosítani a keresési feltételeket, vagy húzd le az oldalt a frissítéshez."
                       FontSize="14"
                       TextColor="Gray"
                       HorizontalTextAlignment="Center" />
                <Button Text="Szűrők törlése"
                        Command="{Binding SzurokTorleseCommand}"
                        HorizontalOptions="Center"
                        BackgroundColor="DodgerBlue"
                        TextColor="White" />
            </VerticalStackLayout>
        </DataTemplate>
    </CollectionView.EmptyViewTemplate>
</CollectionView>

Az EmptyView és az EmptyViewTemplate kölcsönösen kizárják egymást — ha mindkettő meg van adva, az EmptyViewTemplate nyer. Az üres nézet automatikusan megjelenik, amikor az ItemsSource null vagy üres, és eltűnik, amint adat kerül a gyűjteménybe.

Teljesítményoptimalizálás

Na, ez az a rész, amit igazán érdemes figyelni. A CollectionView teljesítménye kritikus a felhasználói élmény szempontjából, különösen nagy adathalmazok és összetett elem-sablonok esetén. Az alábbi tanácsokat érdemes betartani.

Kerüld a ScrollView-ba ágyazást

A CollectionView-t soha ne helyezd ScrollView-ba. Ez talán a leggyakoribb hiba, amit kezdő MAUI fejlesztőknél látok. A CollectionView saját görgetési mechanizmussal rendelkezik, és a ScrollView-ba ágyazás megakadályozza a virtualizációt — vagyis minden elem egyszerre renderelődik. Nagy listák esetén ez komoly gondot okoz. Ha a lista felett vagy alatt statikus tartalmat akarsz megjeleníteni, használd a Header és Footer tulajdonságokat.

Használj Grid szülőelemet

A CollectionView szülőelemeként mindig Grid-et használj StackLayout vagy VerticalStackLayout helyett. A StackLayout nem tud végtelen magasságot adni a gyermeknek, és ez gondot okozhat a görgetés és virtualizáció során.

Fordítási idejű kötések (Compiled Bindings)

Mindig add meg az x:DataType attribútumot a DataTemplate-en. Ez lehetővé teszi a fordítási idejű kötések használatát, amelyek jóval gyorsabbak a reflexió-alapú futásidejű kötéseknél. A .NET 10-ben amúgy már figyelmeztetést is kapsz, ha hiányzik.

Egyszerű ItemTemplate

Kerüld a túlzottan összetett elem-sablonokat. Minél kevesebb vezérlőelem és egymásba ágyazás van egy elemen belül, annál gyorsabb a renderelés. Ajánlott maximum 2-3 szintű nézethierarchia. Néhány konkrét tipp:

  • Kerüld a mély egymásba ágyazást — használj lapos Grid elrendezést több szintű StackLayout helyett
  • Korlátozd a kötések számát — csak a valóban szükséges tulajdonságokat kösd
  • Használj gyorsítótárazott képeket — távoli képeknél érdemes explicit megadni a CachingStrategy-t
  • Kerüld a konvertereket, ha lehetséges — inkább számított tulajdonságokat (computed properties) használj a ViewModel-ben
  • Ne használj Opacity animációkat a listaelemeken, mert ez extra renderelési réteget hoz létre

RecycleElement és ItemsUpdatingScrollMode

A CollectionView alapértelmezetten újrahasznosítja az elemek vizuális megjelenítőit (RecycleElement stratégia), ami azt jelenti, hogy a képernyőn kívülre gördülő elemek konténereit újra felhasználja. Ez sokat segít a memóriahasználaton. Az ItemsUpdatingScrollMode tulajdonsággal a görgetési viselkedést szabályozhatjuk új elemek hozzáadásakor:

  • KeepItemsInView — a látható elemek helyükön maradnak, amikor új elemek kerülnek a listába
  • KeepScrollOffset — a görgetési pozíciót tartja meg
  • KeepLastItemInView — az utolsó elemet tartja láthatóan (chat felületeknél nagyon hasznos)

Általános tanácsok

Még néhány fontos dolog, amit érdemes fejben tartani:

  1. Használj CancellationToken-t az aszinkron adatbetöltéseknél, hogy az oldal elhagyásakor a folyamatban lévő kérések megszakíthatók legyenek.
  2. Alkalmazz lapozást a végtelen görgetés mintával — ne tölts be egyszerre több száz elemet.
  3. A képek méretét optimalizáld a megjelenítési mérethez. Nem kell 4K-s képet betölteni egy 50x50-es bélyegképhez (láttam már ilyet, és nem volt szép).
  4. Kerüld az IsVisible kötéseket a DataTemplate-en belül — ezek nem mentesítik az elemet a layout-számításból. Használj inkább DataTemplateSelector-t.

Migráció ListView-ról CollectionView-ra

Mivel a .NET 10-ben a ListView hivatalosan elavulttá vált, a meglévő alkalmazásokban érdemes migrálni. Ne aggódj, nem olyan bonyolult, mint amilyennek tűnik. Nézzük a legfontosabb különbségeket és a lépéseket.

Legfontosabb különbségek

  • ViewCell eltávolítása: A CollectionView nem használ ViewCell burkolóelemet. A DataTemplate-ben közvetlenül helyezhetjük el a tartalmat. Ez egyszerűsíti a kódot és javítja a teljesítményt.
  • ItemSelected helyett SelectionChanged: A ListView ItemSelected és ItemTapped eseményei helyett a CollectionView egységes SelectionChanged eseményt használ.
  • IsGroupingEnabled helyett IsGrouped: Apróság, de fontos — a tulajdonság neve megváltozott.
  • HasUnevenRows szükségtelen: A CollectionView automatikusan kezeli a különböző méretű elemeket. Egy dologgal kevesebb, amire gondolni kell.
  • IsPullToRefreshEnabled eltávolítása: A beépített pull-to-refresh helyett a CollectionView-t RefreshView-ba kell csomagolni.
  • Beépített elválasztók hiánya: A CollectionView-nak nincs SeparatorVisibility tulajdonsága. Ha kellenek elválasztók, a DataTemplate-ben kell manuálisan megoldani (pl. egy vékony BoxView-val).
  • Kontextusmenü változása: A ListView MenuItem-alapú kontextusmenüje helyett SwipeView-t kell használni.

Gyakorlati migráció lépései

Íme egy egyszerű checklist az átalakításhoz:

  1. Cseréld le a <ListView> taget <CollectionView>-ra
  2. Távolítsd el az összes <ViewCell> burkolóelemet a DataTemplate-ből
  3. Nevezd át az IsGroupingEnabled-et IsGrouped-ra
  4. Cseréld le az ItemSelected / ItemTapped eseményeket SelectionChanged-re, vagy használj SelectedItem adatkötést
  5. Töröld a HasUnevenRows tulajdonságot
  6. A IsPullToRefreshEnabled, RefreshCommand és IsRefreshing tulajdonságokat helyezd át egy RefreshView-ra
  7. A ContextActions-öket cseréld le SwipeView-ra
  8. Töröld a GroupShortNameBinding-ot — a CollectionView nem támogatja natívan
  9. Ellenőrizd, hogy az x:DataType attribútum meg van-e adva a DataTemplate-eken

Lássunk egy gyors összehasonlítást a régi és az új szintaxis között:

<!-- RÉGI: ListView megvalósítás (elavult a .NET 10-ben) -->
<ListView ItemsSource="{Binding Elemek}"
          HasUnevenRows="True"
          IsPullToRefreshEnabled="True"
          RefreshCommand="{Binding FrissitesCommand}"
          IsRefreshing="{Binding Frissites}"
          IsGroupingEnabled="True"
          ItemSelected="Lista_ItemSelected">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <Grid Padding="10">
                    <Label Text="{Binding Nev}" />
                </Grid>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

<!-- ÚJ: CollectionView megvalósítás -->
<RefreshView IsRefreshing="{Binding Frissites}"
             Command="{Binding FrissitesCommand}">
    <CollectionView ItemsSource="{Binding Elemek}"
                    IsGrouped="True"
                    SelectionMode="Single"
                    SelectedItem="{Binding KivalasztottElem}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="models:Elem">
                <Grid Padding="10">
                    <Label Text="{Binding Nev}" />
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</RefreshView>

A migráció során érdemes minden oldalt egyenként átalakítani és tesztelni. Ne próbáld meg egyszerre az egész alkalmazást átírni — az sosem végződik jól. A CollectionView viselkedése bizonyos szélsőséges esetekben eltérhet a ListView-tól, különösen a görgetési pozíció megtartása és az animációk terén.

Összefoglalás

A .NET MAUI CollectionView egy rendkívül sokoldalú és hatékony vezérlőelem, ami a .NET 10-ben a ListView teljes utódjaként vette át a szerepet. Ebben az útmutatóban rengeteg funkciót és mintát néztünk meg — íme a legfontosabb tanulságok dióhéjban:

Legjobb gyakorlatok összefoglalása

  1. Mindig használj ObservableCollection-t az ItemsSource-hoz, ha az adatok változhatnak futásidőben. A sima List<T> nem értesíti a felületet a változásokról.
  2. Add meg az x:DataType attribútumot minden DataTemplate-en a compiled bindings aktiválásához.
  3. Használj MVVM mintát a CommunityToolkit.Mvvm csomaggal. Hidd el, megéri a pár perces beállítást.
  4. Soha ne tedd ScrollView-ba a CollectionView-t — ez az egy szabály több teljesítményproblémát előz meg, mint bármi más.
  5. Tartsd egyszerűnek az ItemTemplate-et — lapos Grid-ek a barátaid, nem a mélyen egymásba ágyazott StackLayout-ok.
  6. Implementálj végtelen görgetést nagy adathalmazok esetén.
  7. Használj DataTemplateSelector-t különböző elemtípusoknál, ne IsVisible kötéseket.
  8. Mindig adj EmptyView-t — a felhasználók értékelik, ha tudják, miért üres a lista.
  9. A RefreshView-t a CollectionView köré tedd, és mindig állítsd vissza az IsRefreshing-et a finally blokkban.
  10. Tesztelj minden platformon — a viselkedés eltérhet Android, iOS és Windows között, különösen a görgetés és a SwipeView terén.

A CollectionView a .NET MAUI egyik leggyakrabban használt vezérlője, és a megfelelő alkalmazásával gyors, reszponzív adatlistákat hozhatsz létre minden támogatott platformon. A .NET 10 CollectionView2 handlerei további teljesítménybeli fejlesztéseket hoznak, szóval érdemes mindig a legfrissebb verziót használni.

Ha még nem tetted meg, kezdd el a meglévő ListView-jaid migrálását. A .NET 10 deprecation figyelmeztetései amúgy is emlékeztetni fognak rá, de ezzel az útmutatóval a kezedben a folyamat egyszerű és kiszámítható lesz.

A Szerzőről Editorial Team

Our team of expert writers and editors.