Aplikacje mobilne · Listy
Wyświetlanie list danych: CollectionView, wiązanie i ItemTemplate
Wiesz już, że CollectionView służy do list. Teraz zobaczysz, jak używa się jej naprawdę: skąd bierze dane, jak wygląda jeden wiersz i jak sprawić, żeby lista odświeżała się sama, także gdy zmieni się pojedyncze pole obiektu i gdy użytkownik filtruje listę wyszukiwarką.
- około 55 minut
- .NET MAUI · CollectionView · ObservableCollection · INotifyPropertyChanged
- INF.04.6.2
Po tej lekcji: połączyć CollectionView z kolekcją, opisać wiersz w ItemTemplate, zadbać o odświeżanie przez ObservableCollection i INotifyPropertyChanged oraz poprawnie filtrować listę.
Biblioteka z wyszukiwaniem i zmianą statusu
Lista nie jest fotografią danych. Użytkownik filtruje książki i je wypożycza, a interfejs musi reagować na zmianę obiektu, nie tylko na dodanie nowego wiersza.
Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.
Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji
1 · Sprawdź w działaniu
Kiedy lista się odświeża, a kiedy nie
Kliknij „Wypożycz” przy trzech konfiguracjach modelu: bez powiadamiania, z powiadamianiem tylko własnej właściwości i z powiadamianiem także właściwości wyliczanych. Potem wyszukaj „o”, skasuj znak i porównaj dwa tryby filtrowania.
Najpierw przewidź, potem sprawdź: W modelu listy zmień status książki, potem wpisz filtr i go usuń. Zobacz, czy zmieniony obiekt nadal ma ten sam status po ponownym pokazaniu.
Model musi zgłosić każdą właściwość, od której zależy widok, także wyliczaną. Filtruj zawsze zapamiętaną listę oryginalną. Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.
1. Przygotuj miejsce pracy
Co dodajesz, a co podmieniasz?
Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.
| Plik | Rola w ćwiczeniu |
|---|---|
| Modele/Ksiazka.cs | Logika i obsługa danych |
| Logika/BibliotekaViewModel.cs | Logika i obsługa danych |
| MainPage.xaml | Wygląd strony lub kontrolki |
| MainPage.xaml.cs | Logika i obsługa danych |
| WierszUstawienia.xaml | Wygląd strony lub kontrolki |
| WierszUstawienia.xaml.cs | Logika i obsługa danych |
| UstawieniaPage.xaml | Wygląd strony lub kontrolki |
Zanim uruchomisz: trzy połączenia
x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.
x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.
Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.
2. Uruchom sam wygląd — jeszcze bez logiki
W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MojaAplikacja.MainPage">
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="Moja biblioteka" FontSize="24" />
<SearchBar Placeholder="Szukaj książki" />
<Label Text="Tutaj za chwilę pojawią się dane z modelu." />
</VerticalStackLayout>
</ContentPage>
- Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.
Zacznijmy od biblioteki
Program ma sam wypełnić wiersze
Chcesz pokazać listę książek, produktów albo zadań. Nie tworzysz osobnej kontrolki Label dla każdej pozycji. Opisujesz jeden wiersz, a program sam powtarza go dla każdego elementu kolekcji.
Trzy rzeczy muszą współgrać: kolekcja (ObservableCollection<T>), model z powiadamianiem o zmianach (INotifyPropertyChanged) i szablon wiersza (ItemTemplate).
2 · Dwa rodzaje powiadomień
ObservableCollection i INotifyPropertyChanged
| Mechanizm | O czym powiadamia | Efekt braku |
|---|---|---|
ObservableCollection<T> | Dodanie i usunięcie elementu kolekcji. | Z List<T> nowa pozycja nie pojawia się na ekranie. |
INotifyPropertyChanged | Zmianę jednej właściwości istniejącego obiektu. | Zmiana statusu nie odświeża wiersza, lista wygląda na „zamrożoną”. |
Dla chętnych: własna kontrolka z BindableProperty
Po poznaniu Binding i INotifyPropertyChanged wróć do ContentView z lekcji5. Poniższe trzy pliki to wariant zamienny, nie druga klasa o tej samej nazwie do dodania obok. BindableProperty pozwala ustawiać Tytul/Opis również przez {Binding ...}. Wiersz jest zwykłym widokiem, nie komórką CollectionView.
3. Wskaż stronę startową ćwiczenia
Po dodaniu docelowych stron podmień App.xaml.cs na poniższy. App.xaml zostaje z szablonu. Od tej chwili startuje MainPage, a nie wcześniejszy tymczasowy MainPage z AppShell. Uzupełnij jej pliki w kolejnych etapach, zanim ponownie uruchomisz projekt.
namespace MojaAplikacja;
public partial class App : Application
{
public App() => InitializeComponent();
protected override Window CreateWindow(IActivationState? activationState)
=> new Window(new NavigationPage(new MainPage()));
}
CreateWindowTworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.new MainPage()Tworzy instancję docelowej strony. Aby testować drugi niezależny przykład, zatrzymaj program, zmień tylko ten typ i uruchom ponownie.
4. Połącz wygląd z działaniem
Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.
Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.
Budowa krok po kroku
Ksiazka.cs
Ksiazka implementuje INotifyPropertyChanged. Samo ustawienie pola nie powiadomi widoku; OnPropertyChanged informuje go, co należy odczytać ponownie.
Każda właściwość zgłasza zmianę w setterze. Dodatkowo Wypozyczona zgłasza dwie właściwości wyliczane, które od niej zależą.
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MojaAplikacja.Modele;
public class Ksiazka : INotifyPropertyChanged
{
private string _tytul = "";
private string _autor = "";
private bool _wypozyczona;
public string Tytul { get => _tytul; set { _tytul = value; OnPropertyChanged(); } }
public string Autor { get => _autor; set { _autor = value; OnPropertyChanged(); } }
public bool Wypozyczona
{
get => _wypozyczona;
set
{
_wypozyczona = value;
OnPropertyChanged();
OnPropertyChanged(nameof(StatusOpis)); // właściwości wyliczane też trzeba zgłosić
OnPropertyChanged(nameof(AkcjaTekst));
}
}
public string StatusOpis => Wypozyczona ? "Wypożyczona" : "Dostępna";
public string AkcjaTekst => Wypozyczona ? "Zwróć" : "Wypożycz";
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? nazwa = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
}
OnPropertyChanged()w setterze Wywołanie bez argumentu podstawia nazwę właściwości dzięki[CallerMemberName].OnPropertyChanged(nameof(StatusOpis))Etykieta ze statusem zależy odWypozyczona, więc też trzeba ją zgłosić. Inaczej się nie odświeży.public string StatusOpis => Wypozyczona ? ... : ...Właściwość wyliczana, bez własnego pola.event PropertyChangedEventHandler? PropertyChangedZdarzenie, które wiązania MAUI nasłuchują.
Budowa krok po kroku
BibliotekaViewModel.cs
ObservableCollection informuje o dodaniu lub usunięciu elementu, a sama Ksiazka o zmianie statusu wewnątrz wiersza. To dwa poziomy powiadomień.
Prosta klasa trzymająca kolekcję trzech książek.
using System.Collections.ObjectModel;
using MojaAplikacja.Modele;
namespace MojaAplikacja.Logika;
public class BibliotekaViewModel
{
public ObservableCollection<Ksiazka> Ksiazki { get; }
public BibliotekaViewModel()
{
Ksiazki = new ObservableCollection<Ksiazka>
{
new Ksiazka { Tytul = "Hobbit", Autor = "J.R.R. Tolkien" },
new Ksiazka { Tytul = "Dune", Autor = "Frank Herbert", Wypozyczona = true },
new Ksiazka { Tytul = "1984", Autor = "George Orwell" }
};
}
}
Budowa krok po kroku
MainPage.xaml
Dodaj lub otwórz MainPage.xaml i podmień jego zawartość. Najpierw odszukaj kontener, potem nazwane kontrolki oraz wskazane zdarzenia. Pozostaw zgodne x:Class i nazwę klasy w pliku .xaml.cs.
DataTemplate opisuje jeden wiersz. MAUI powtórzy go dla każdej książki.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MojaAplikacja.Modele"
x:Class="MojaAplikacja.MainPage"
Title="Lista książek">
<VerticalStackLayout Padding="20">
<SearchBar Placeholder="Szukaj książki..." TextChanged="Szukaj_TextChanged" />
<CollectionView x:Name="cvKsiazki">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Ksiazka">
<Grid ColumnDefinitions="*,Auto" Padding="12" Margin="0,4">
<VerticalStackLayout>
<Label Text="{Binding Tytul}" FontAttributes="Bold" />
<Label Text="{Binding Autor}" />
<Label Text="{Binding StatusOpis}" />
</VerticalStackLayout>
<Button Grid.Column="1"
Text="{Binding AkcjaTekst}"
Clicked="ZmienStatus_Click" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
DataTemplate x:DataType="models:Ksiazka"Mówi kompilatorowi, jakiego typu jest element, więc literówki w{Binding}zostaną wykryte. Bez tego wiązania też działają, ale przez refleksję i wolniej.Grid ColumnDefinitions="*,Auto"Tekst po lewej, przycisk po prawej.{Binding StatusOpis},{Binding AkcjaTekst}Etykieta i napis przycisku pochodzą z modelu.
Budowa krok po kroku
MainPage.xaml.cs
_oryginalnaLista przechowuje wszystkie obiekty. Filtr tworzy widok wyników; nie niszczy źródła. BindingContext klikniętego przycisku wskazuje książkę z tego wiersza.
Przycisk w wierszu zmienia status książki z tego wiersza, a wyszukiwarka filtruje oryginalną listę.
using System.Collections.ObjectModel;
using MojaAplikacja.Logika;
using MojaAplikacja.Modele;
namespace MojaAplikacja;
public partial class MainPage : ContentPage
{
private readonly BibliotekaViewModel _vm;
private readonly ObservableCollection<Ksiazka> _oryginalnaLista;
public MainPage()
{
InitializeComponent();
_vm = new BibliotekaViewModel();
_oryginalnaLista = _vm.Ksiazki;
cvKsiazki.ItemsSource = _vm.Ksiazki;
}
private void ZmienStatus_Click(object sender, EventArgs e)
{
// BindingContext przycisku to książka z tego wiersza listy
var ks = (sender as BindableObject)?.BindingContext as Ksiazka;
if (ks != null)
ks.Wypozyczona = !ks.Wypozyczona;
}
private void Szukaj_TextChanged(object sender, TextChangedEventArgs e)
{
string filtr = e.NewTextValue?.ToLower() ?? "";
if (string.IsNullOrWhiteSpace(filtr))
{
cvKsiazki.ItemsSource = _oryginalnaLista;
return;
}
// Zawsze filtrujemy ZAPAMIĘTANĄ listę oryginalną, a nie już zawężone wyniki
var wyniki = new ObservableCollection<Ksiazka>(
_oryginalnaLista.Where(k =>
k.Tytul.ToLower().Contains(filtr) || k.Autor.ToLower().Contains(filtr)));
cvKsiazki.ItemsSource = wyniki;
}
}
(sender as BindableObject)?.BindingContext as KsiazkaPrzycisk w szablonie ma jakoBindingContextksiążkę z danego wiersza.ks.Wypozyczona = !ks.WypozyczonaZmiana jednej właściwości. DziękiINotifyPropertyChangedwidok sam się odświeży.e.NewTextValue?.ToLower() ?? ""Bezpieczny odczyt wpisanej frazy małymi literami._oryginalnaLista.Where(...)LINQ filtruje zapamiętaną listę, a nie wyniki poprzedniego filtrowania.new ObservableCollection<Ksiazka>(...)Wynik filtrowania jako nowa kolekcja przypisana doItemsSource.
Zatrzymaj się przed kolejnym wariantem
Nie zastępuj jeszcze działającej wersji. Wykonaj tę próbę i dopiero potem przejdź do dodatku.
Dopiero teraz: drugi wariant lub rozszerzenie
To następny etap tej lekcji, nie warunek pierwszego uruchomienia. Porównaj jego rolę z działającą bazą. Jeśli brakuje czasu, wróć do niego po sprawdzeniu podstawowej wersji.
Dodatek do działającej wersji
Dodatek po wiązaniu danych: WierszUstawienia: własny kontener
Dodatek: alternatywny ContentView po opanowaniu listy. Nie kopiuj jednocześnie prostej i wiązalnej wersji WierszUstawienia do tego samego projektu.
ContentView to wydzielony fragment interfejsu, który możesz użyć w wielu miejscach, jak osobny komponent. Tutaj wiersz ustawienia: tytuł i opis po lewej, przełącznik po prawej.
<?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"
x:Class="MojaAplikacja.WierszUstawienia"
x:Name="Wiersz">
<Grid ColumnDefinitions="*,Auto" Padding="12">
<VerticalStackLayout Grid.Column="0">
<Label Text="{Binding Tytul, Source={x:Reference Wiersz}}" FontAttributes="Bold" />
<Label Text="{Binding Opis, Source={x:Reference Wiersz}}" TextColor="Gray" FontSize="12" />
</VerticalStackLayout>
<Switch Grid.Column="1" VerticalOptions="Center" />
</Grid>
</ContentView>
<ContentView ... x:Name="Wiersz">Nazwa pozwala odwołać się do własności tego kontenera w wiązaniach.Grid ColumnDefinitions="*,Auto"Skrócony zapis kolumn: lewa elastyczna, prawa dopasowana do przełącznika.{Binding Tytul, Source={x:Reference Wiersz}}Wiąże etykietę z własnościąTytultego kontenera.
Dodatek do działającej wersji
Dodatek po wiązaniu danych: WierszUstawienia.xaml.cs
BindableProperty udostępnia właściwość systemowi wiązań MAUI. Publiczna właściwość C# przekazuje GetValue/SetValue do tego samego klucza.
Własności Tytul i Opis są BindableProperty, więc strona, która używa kontenera, może je ustawiać atrybutami XAML i wiązać.
namespace MojaAplikacja;
public partial class WierszUstawienia : ContentView
{
// Własności, które ustawia strona używająca tego kontenera
public static readonly BindableProperty TytulProperty =
BindableProperty.Create(nameof(Tytul), typeof(string), typeof(WierszUstawienia), "");
public static readonly BindableProperty OpisProperty =
BindableProperty.Create(nameof(Opis), typeof(string), typeof(WierszUstawienia), "");
public string Tytul
{
get => (string)GetValue(TytulProperty);
set => SetValue(TytulProperty, value);
}
public string Opis
{
get => (string)GetValue(OpisProperty);
set => SetValue(OpisProperty, value);
}
public WierszUstawienia()
{
InitializeComponent();
}
}
BindableProperty.Create(...)Rejestruje własność w systemie wiązania MAUI.GetValue/SetValueOdczyt i zapis wartości własności.
Dodatek do działającej wersji
Dodatek po wiązaniu danych: UstawieniaPage.xaml
UstawieniaPage używa nowego kontenera. To dodatkowy ekran ćwiczeniowy; nie zastępuje listy książek.
Ten sam kontener użyty cztery razy z różnymi danymi, wewnątrz ScrollView. Każda zmiana wyglądu wiersza dotyczy wszystkich czterech naraz.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MojaAplikacja"
x:Class="MojaAplikacja.UstawieniaPage">
<ScrollView>
<VerticalStackLayout>
<local:WierszUstawienia Tytul="Powiadomienia" Opis="Pokazuj powiadomienia na ekranie blokady" />
<local:WierszUstawienia Tytul="Tryb ciemny" Opis="Ciemny motyw aplikacji" />
<local:WierszUstawienia Tytul="Lokalizacja" Opis="Zezwól na dostęp do położenia" />
<local:WierszUstawienia Tytul="Synchronizacja" Opis="Wysyłaj dane w tle" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
xmlns:local="clr-namespace:MojaAplikacja"Przestrzeń nazw, dzięki której XAML zna Twój kontener.<local:WierszUstawienia Tytul="…" />Użycie własnego kontenera jak zwykłej kontrolki.
Plik .xaml.cs też musi istnieć
Szablon strony lub ContentView generuje poniższe klasy. Nie dodawaj ich drugi raz, jeśli Visual Studio już je utworzyło. W każdej parze musi pozostać InitializeComponent().
UstawieniaPage — minimalny kod za widokiem
namespace MojaAplikacja;
public partial class UstawieniaPage : ContentPage
{
public UstawieniaPage() => InitializeComponent();
}
partialDruga część klasy powstaje z XAML. Nie twórz ręcznie pól kontrolek ani drugiej metody InitializeComponent.
5. Odbiór: czy naprawdę działa?
Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.
7 · Czego unikać
Cztery błędy z listami
Model bez `INotifyPropertyChanged`
public bool Wypozyczona { get; set; }Zmiana statusu nie odświeża widoku, lista wygląda na „zamrożoną”.
set { _wypozyczona = value; OnPropertyChanged(); }Wiązania odświeżają się tylko po powiadomieniu.
Brak powiadomienia o właściwości wyliczanej
set { _wypozyczona = value; OnPropertyChanged(); } // bez StatusOpisEtykieta „Dostępna / Wypożyczona” nie zmienia się, choć dane się zmieniły.
OnPropertyChanged(nameof(StatusOpis));Każda właściwość, od której zależy widok, musi być zgłoszona.
Filtrowanie już zawężonego `ItemsSource`
var wyniki = cvKsiazki.ItemsSource.Cast<Ksiazka>().Where(...);Po skasowaniu znaku wyniki nie wracają, bo każde filtrowanie zawęża poprzednie.
var wyniki = _oryginalnaLista.Where(...);Zawsze filtruj zapamiętaną listę oryginalną.
Brak `x:DataType` w szablonie
<DataTemplate>
<Label Text="{Binding Tytl}" />Literówka w nazwie właściwości nie zostanie wykryta w czasie kompilacji.
<DataTemplate x:DataType="models:Ksiazka">Skompilowane wiązania wychwytują błędy wcześniej i działają szybciej.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Dodaj czwartą książkę i sprawdź wyszukiwanie po autorze.
Dodaj czwartą książkę i sprawdź wyszukiwanie po autorze. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Dopisz Ksiazka do kolekcji tworzonej w BibliotekaViewModel.
Krok 2 — pokaż podpowiedź
Nie wpisuj dodatkowego wiersza na sztywno w XAML — wiersze tworzy DataTemplate.
Krok 3 — pokaż podpowiedź
Sprawdź autora z różną wielkością liter oraz powrót po usunięciu filtra.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
8 · Sprawdź się
Co odświeży listę?
Cztery sytuacje.
Podsumowanie
Co warto zapamiętać
CollectionViewpokazuje kolekcję przezItemsSourcei wiersz opisany wItemTemplate.ObservableCollection<T>zgłasza dodanie i usunięcie elementów, aINotifyPropertyChangedzmianę właściwości obiektu.- Zgłaszaj także właściwości wyliczane (
OnPropertyChanged(nameof(...))). - Filtruj zawsze oryginalną listę, a wynik przypisuj do
ItemsSource. x:DataTypewDataTemplatedaje sprawdzanie wiązań przy kompilacji.
Dalej na swoim koncie
Rozgrzewki, quiz i zadania są w biurku ucznia.
Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.
Zaloguj się →