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ábaKeepScrollOffset— a görgetési pozíciót tartja megKeepLastItemInView— 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:
- 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. - Alkalmazz lapozást a végtelen görgetés mintával — ne tölts be egyszerre több száz elemet.
- 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).
- Kerüld az
IsVisiblekö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
ViewCellburkoló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ésItemTappedeseményei helyett a CollectionView egységesSelectionChangedesemé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
SeparatorVisibilitytulajdonsága. Ha kellenek elválasztók, a DataTemplate-ben kell manuálisan megoldani (pl. egy vékonyBoxView-val). - Kontextusmenü változása: A ListView
MenuItem-alapú kontextusmenüje helyettSwipeView-t kell használni.
Gyakorlati migráció lépései
Íme egy egyszerű checklist az átalakításhoz:
- Cseréld le a
<ListView>taget<CollectionView>-ra - Távolítsd el az összes
<ViewCell>burkolóelemet a DataTemplate-ből - Nevezd át az
IsGroupingEnabled-etIsGrouped-ra - Cseréld le az
ItemSelected/ItemTappedeseményeketSelectionChanged-re, vagy használjSelectedItemadatkötést - Töröld a
HasUnevenRowstulajdonságot - A
IsPullToRefreshEnabled,RefreshCommandésIsRefreshingtulajdonságokat helyezd át egyRefreshView-ra - A
ContextActions-öket cseréld leSwipeView-ra - Töröld a
GroupShortNameBinding-ot — a CollectionView nem támogatja natívan - Ellenőrizd, hogy az
x:DataTypeattribú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
- 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. - Add meg az x:DataType attribútumot minden DataTemplate-en a compiled bindings aktiválásához.
- Használj MVVM mintát a CommunityToolkit.Mvvm csomaggal. Hidd el, megéri a pár perces beállítást.
- 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.
- Tartsd egyszerűnek az ItemTemplate-et — lapos Grid-ek a barátaid, nem a mélyen egymásba ágyazott StackLayout-ok.
- Implementálj végtelen görgetést nagy adathalmazok esetén.
- Használj DataTemplateSelector-t különböző elemtípusoknál, ne IsVisible kötéseket.
- Mindig adj EmptyView-t — a felhasználók értékelik, ha tudják, miért üres a lista.
- A RefreshView-t a CollectionView köré tedd, és mindig állítsd vissza az IsRefreshing-et a finally blokkban.
- 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.