Wyświetlanie list danych w .NET MAUI i WPF (INF.04)

Wprowadzenie

Wyobraź sobie, że tworzysz aplikację, w której chcesz wyświetlić listę rzeczy: książek, produktów, uczniów albo zadań do wykonania.
Nie chcesz ich tworzyć po jednej kontrolce dla każdego wiersza – chcesz, żeby program sam wypełnił listę z danych.
Do tego właśnie służą listy danych i kontrolki takie jak CollectionView (w .NET MAUI) oraz ListView (w WPF).

To, co ważne:

  • dane trzymamy w specjalnej kolekcji (np. ObservableCollection),
  • każda pozycja ma swój model danych,
  • wygląd pojedynczego elementu opisuje DataTemplate,
  • całość łączy mechanizm Bindingu (czyli automatycznego powiązania danych z interfejsem).

Zacznijmy od podstaw.


Co to jest lista i kolekcja

Lista to po prostu zbiór elementów, np. książek.
W języku C# często używamy:

List<string> owoce = new List<string> { "Jabłko", "Gruszka", "Pomarańcza" };

Ale zwykła List<T> nie odświeża się automatycznie w interfejsie, gdy coś się zmieni.
Dlatego w aplikacjach graficznych używamy ObservableCollection, która informuje interfejs o każdej zmianie (dodaniu, usunięciu, edycji).


Zasada działania

  1. Model – klasa z danymi (np. Ksiazka z tytułem, autorem, statusem).
  2. Kolekcja – lista obiektów ObservableCollection<Ksiazka>.
  3. Binding – połączenie danych z interfejsem (ItemsSource + DataTemplate).
  4. Zdarzenia – reagowanie na kliknięcia, zmiany, filtrowanie.

Foldery projektu (ten sam układ w obu technologiach)

BibliotekaApp/
│
├── Modele/
│   └── Ksiazka.cs
│
├── Logika/
│   └── BibliotekaViewModel.cs
│
├── Walidacja/
│   └── WalidatorKsiazki.cs
│
├── MainPage.xaml / MainPage.xaml.cs   (.NET MAUI)
└── MainWindow.xaml / MainWindow.xaml.cs (WPF)

SEKCJA A – .NET MAUI

1. Model danych

Modele/Ksiazka.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace BibliotekaApp.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));
                OnPropertyChanged(nameof(AkcjaTekst));
            }
        }

        public string StatusOpis => Wypozyczona ? "Wypożyczona" : "Dostępna";
        public string AkcjaTekst => Wypozyczona ? "Zwróć" : "Wypożycz";

        public event PropertyChangedEventHandler? PropertyChanged;
        void OnPropertyChanged([CallerMemberName] string? nazwa = null) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
    }
}

2. Logika (ViewModel)

Logika/BibliotekaViewModel.cs

using System.Collections.ObjectModel;
using BibliotekaApp.Modele;

namespace BibliotekaApp.Logika
{
    public class BibliotekaViewModel
    {
        public ObservableCollection<Ksiazka> Ksiazki { get; set; }

        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" }
            };
        }
    }
}

3. Widok – lista książek

MainPage.xaml

<ContentPage
    x:Class="BibliotekaApp.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:models="clr-namespace:BibliotekaApp.Modele"
    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>

MainPage.xaml.cs

using BibliotekaApp.Logika;
using BibliotekaApp.Modele;
using System.Collections.ObjectModel;

namespace BibliotekaApp
{
    public partial class MainPage : ContentPage
    {
        private BibliotekaViewModel vm;
        private ObservableCollection<Ksiazka> oryginalnaLista;

        public MainPage()
        {
            InitializeComponent();
            vm = new BibliotekaViewModel();
            oryginalnaLista = vm.Ksiazki;
            cvKsiazki.ItemsSource = vm.Ksiazki;
        }

        private void ZmienStatus_Click(object sender, EventArgs e)
        {
            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;
            }

            var wyniki = new ObservableCollection<Ksiazka>(
                oryginalnaLista.Where(k =>
                    k.Tytul.ToLower().Contains(filtr) ||
                    k.Autor.ToLower().Contains(filtr)));
            cvKsiazki.ItemsSource = wyniki;
        }
    }
}

SEKCJA B – WPF

Zasada jest identyczna, tylko kontrolka i zdarzenia mają inne nazwy.
W WPF używamy ListView, DataContext i Click.


MainWindow.xaml

<Window x:Class="BibliotekaApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:models="clr-namespace:BibliotekaApp.Modele"
        Title="Lista książek" Height="400" Width="600">
    <Grid Margin="10" RowDefinitions="Auto,*" RowSpacing="6">
        <TextBox x:Name="txtSzukaj" Height="28" TextChanged="txtSzukaj_TextChanged"
                 PlaceholderText="Szukaj książki..." />
        <ListView x:Name="lvKsiazki" Grid.Row="1" SelectionMode="Single">
            <ListView.ItemTemplate>
                <DataTemplate DataType="{x:Type models:Ksiazka}">
                    <Grid ColumnDefinitions="*,Auto" Margin="4">
                        <StackPanel>
                            <TextBlock Text="{Binding Tytul}" FontWeight="Bold" />
                            <TextBlock Text="{Binding Autor}" />
                            <TextBlock Text="{Binding StatusOpis}" />
                        </StackPanel>
                        <Button Grid.Column="1"
                                Content="{Binding AkcjaTekst}"
                                Click="btnZmienStatus_Click" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

MainWindow.xaml.cs

using BibliotekaApp.Logika;
using BibliotekaApp.Modele;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace BibliotekaApp
{
    public partial class MainWindow : Window
    {
        private BibliotekaViewModel vm;
        private ObservableCollection<Ksiazka> oryginalnaLista;

        public MainWindow()
        {
            InitializeComponent();
            vm = new BibliotekaViewModel();
            oryginalnaLista = vm.Ksiazki;
            lvKsiazki.ItemsSource = vm.Ksiazki;
        }

        private void btnZmienStatus_Click(object sender, RoutedEventArgs e)
        {
            var ks = (sender as FrameworkElement)?.DataContext as Ksiazka;
            if (ks != null) ks.Wypozyczona = !ks.Wypozyczona;
        }

        private void txtSzukaj_TextChanged(object sender, TextChangedEventArgs e)
        {
            string filtr = txtSzukaj.Text.ToLower();
            if (string.IsNullOrWhiteSpace(filtr))
            {
                lvKsiazki.ItemsSource = oryginalnaLista;
                return;
            }

            var wyniki = new ObservableCollection<Ksiazka>(
                oryginalnaLista.Where(k =>
                    k.Tytul.ToLower().Contains(filtr) ||
                    k.Autor.ToLower().Contains(filtr)));
            lvKsiazki.ItemsSource = wyniki;
        }
    }
}

Co jest takie samo, a co inne?

Mechanizm.NET MAUIWPF
Lista danychCollectionViewListView
Kontekst danychBindingContextDataContext
Zdarzenie przyciskuClickedClick
FiltrowanieSearchBar.TextChangedTextBox.TextChanged
Kolekcja danychObservableCollectionObservableCollection
Model z INotifyPropertyChangedwspólnywspólny

Zadania dla ucznia (pozostają takie same)

  1. Dodaj do modelu pole RokWydania i wyświetl je w liście.
  2. Dodaj filtr „Pokaż tylko wypożyczone książki”.
  3. Dodaj przycisk „Dodaj książkę”, który otwiera nowe okno / stronę z formularzem dodawania książki.

Podsumowanie

  • Mechanizm listy danych jest taki sam w .NET MAUI i WPF – różni się tylko nazwami kontrolek.
  • Najważniejsze jest zrozumienie trzech warstw: Modele, Logika, Widok.
  • ObservableCollection odświeża interfejs automatycznie.
  • INotifyPropertyChanged informuje o zmianach danych.
  • DataTemplate pozwala zdefiniować wygląd każdego elementu listy.

Gdy to zrozumiesz, umiesz już stworzyć każdą listę danych — w aplikacji desktopowej, webowej czy mobilnej.


###################################################################

Jeśli dotarłeś do tej części bez przewijania i ślepego wklejania mojego kodu, to zauważyłeś, że nie ma tu walidacji !!! OMG. jak to możliwe, że ten artykuł to nie 100% SIGMA 🙂

Uzupełnienie: Walidacja danych i formularz „Dodaj książkę” (MAUI + WPF)

Po co walidacja?

Walidacja to sprawdzanie, czy dane wpisane przez użytkownika mają sens, zanim trafią do listy. Dzięki temu:

  • unikamy głupich błędów (puste tytuły, literówki zamiast roku),
  • uczeń widzi jasny komunikat co poprawić,
  • aplikacja jest stabilniejsza.

Zasada: sprawdź → jeśli OK, zbuduj model → dodaj do kolekcji.


Wspólny rdzeń (działa i w MAUI, i w WPF)

Modele/Ksiazka.cs

(jeśli już masz, tylko uzupełnij o RokWydania jako opcjonalne; zostaw niewymagane, bo część klas już robi zad. 1 „dodaj rok”)

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace BibliotekaApp.Modele
{
    public class Ksiazka : INotifyPropertyChanged
    {
        private string _tytul = "";
        private string _autor = "";
        private int? _rokWydania;       // opcjonalny (zad. 1 go wykorzysta)
        private bool _wypozyczona;

        public string Tytul
        {
            get => _tytul;
            set { if (_tytul != value) { _tytul = value; OnPropertyChanged(); } }
        }

        public string Autor
        {
            get => _autor;
            set { if (_autor != value) { _autor = value; OnPropertyChanged(); } }
        }

        public int? RokWydania
        {
            get => _rokWydania;
            set { if (_rokWydania != value) { _rokWydania = value; OnPropertyChanged(); } }
        }

        public bool Wypozyczona
        {
            get => _wypozyczona;
            set
            {
                if (_wypozyczona != value)
                {
                    _wypozyczona = value;
                    OnPropertyChanged();
                    OnPropertyChanged(nameof(StatusOpis));
                    OnPropertyChanged(nameof(AkcjaTekst));
                }
            }
        }

        public string StatusOpis => Wypozyczona ? "Wypożyczona" : "Dostępna";
        public string AkcjaTekst => Wypozyczona ? "Zwróć" : "Wypożycz";

        public event PropertyChangedEventHandler? PropertyChanged;
        void OnPropertyChanged([CallerMemberName] string? nazwa = null) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
    }
}

Logika/BibliotekaViewModel.cs

(bez zmian — lista książek w ObservableCollection)

using System.Collections.ObjectModel;
using BibliotekaApp.Modele;

namespace BibliotekaApp.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, RokWydania = 1965 },
                new Ksiazka { Tytul = "1984", Autor = "George Orwell", RokWydania = 1949 }
            };
        }
    }
}

Walidacja/Walidacje.cs

(małe, wielorazowe helpery)

namespace BibliotekaApp.Walidacja
{
    public static class Walidacje
    {
        public static bool NiePuste(string? txt) =>
            !string.IsNullOrWhiteSpace(txt);

        public static int? SprobujInt(string? txt)
        {
            if (string.IsNullOrWhiteSpace(txt)) return null;
            return int.TryParse(txt.Trim(), out int val) ? val : null;
        }

        public static bool WZakresie(int? val, int min, int max) =>
            val is not null && val >= min && val <= max;
    }
}

Walidacja/WalidatorKsiazki.cs

(jedna metoda: przyjmuje teksty z formularza, mówi co źle, albo zwraca gotowy model)

using System.Collections.Generic;
using BibliotekaApp.Modele;

namespace BibliotekaApp.Walidacja
{
    public static class WalidatorKsiazki
    {
        // rokWydaniaTxt może być pusty (opcjonalny) – jeśli klasa robi już Zadanie 1,
        // to to pole będzie wymagane wg ustaleń nauczyciela.
        public static (bool ok, List<string> bledy, Ksiazka? model)
            SprawdzIZbuduj(string? tytulTxt, string? autorTxt, string? rokWydaniaTxt, bool wypozyczona)
        {
            var bledy = new List<string>();

            // 1) Tytuł
            if (!Walidacje.NiePuste(tytulTxt))
                bledy.Add("Tytuł nie może być pusty.");
            else if (tytulTxt!.Trim().Length < 2)
                bledy.Add("Tytuł musi mieć co najmniej 2 znaki.");

            // 2) Autor
            if (!Walidacje.NiePuste(autorTxt))
                bledy.Add("Autor nie może być pusty.");
            else if (autorTxt!.Trim().Length < 2)
                bledy.Add("Autor musi mieć co najmniej 2 znaki.");

            // 3) Rok wydania (opcjonalny; jeśli uczeń robi Zad. 1 – zrób „wymagany”)
            int? rok = Walidacje.SprobujInt(rokWydaniaTxt);
            if (!string.IsNullOrWhiteSpace(rokWydaniaTxt))
            {
                if (rok is null) bledy.Add("Rok wydania musi być liczbą całkowitą.");
                else if (!Walidacje.WZakresie(rok, 1450, 2100))
                    bledy.Add("Rok wydania musi być w zakresie 1450–2100.");
            }

            if (bledy.Count > 0)
                return (false, bledy, null);

            var model = new Ksiazka
            {
                Tytul = tytulTxt!.Trim(),
                Autor = autorTxt!.Trim(),
                RokWydania = rok,
                Wypozyczona = wypozyczona
            };

            return (true, bledy, model);
        }
    }
}

SEKCJA A – .NET MAUI: Formularz „Dodaj książkę” z walidacją

Dodaj przycisk „Dodaj książkę” na liście i prostą stronę formularza.

1) Dopisz przycisk w istniejącej liście

MainPage.xaml – nad CollectionView:

<Button Text="Dodaj książkę" Clicked="DodajKsiazke_Click" />

MainPage.xaml.cs – obsługa:

private async void DodajKsiazke_Click(object sender, EventArgs e)
{
    await Navigation.PushAsync(new DodajKsiazkePage(vm)); // przekaż ViewModel
}

Upewnij się, że w App.xaml.cs masz MainPage = new NavigationPage(new MainPage()); żeby działała nawigacja.

2) Nowa strona formularza

DodajKsiazkePage.xaml

<ContentPage
    x:Class="BibliotekaApp.DodajKsiazkePage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Title="Dodaj książkę">

    <ScrollView>
        <VerticalStackLayout Padding="20" Spacing="10">
            <Entry x:Name="entTytul" Placeholder="Tytuł" />
            <Entry x:Name="entAutor" Placeholder="Autor" />
            <Entry x:Name="entRok" Placeholder="Rok wydania (opcjonalnie)" Keyboard="Numeric" />
            <HorizontalStackLayout>
                <Label Text="Wypożyczona:" VerticalOptions="Center" />
                <Switch x:Name="swWypozyczona" />
            </HorizontalStackLayout>

            <Button Text="Zapisz" Clicked="Zapisz_Click" />
            <Label x:Name="lblBledy" TextColor="Red" />
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

DodajKsiazkePage.xaml.cs

using BibliotekaApp.Logika;
using BibliotekaApp.Walidacja;

namespace BibliotekaApp
{
    public partial class DodajKsiazkePage : ContentPage
    {
        private readonly BibliotekaViewModel _vm;

        public DodajKsiazkePage(BibliotekaViewModel vm)
        {
            InitializeComponent();
            _vm = vm;
        }

        private async void Zapisz_Click(object sender, EventArgs e)
        {
            var (ok, bledy, model) = WalidatorKsiazki.SprawdzIZbuduj(
                entTytul.Text, entAutor.Text, entRok.Text, swWypozyczona.IsToggled);

            if (!ok)
            {
                lblBledy.Text = "Popraw dane:\n- " + string.Join("\n- ", bledy);
                return;
            }

            _vm.Ksiazki.Add(model!);
            await DisplayAlert("Sukces", "Książka dodana.", "OK");
            await Navigation.PopAsync();
        }
    }
}

Efekt: jeśli pola są błędne – czerwone komunikaty; jeśli OK – książka trafia do listy i wracasz do ekranu głównego.


SEKCJA B – WPF: Formularz „Dodaj książkę” z walidacją

Analogicznie: przycisk „Dodaj książkę” w MainWindow + nowe okno dialogowe.

1) Przycisk „Dodaj książkę”

MainWindow.xaml – nad ListView:

<Button Content="Dodaj książkę" Click="DodajKsiazke_Click" Height="28" Width="120" Margin="0,0,0,6" />

MainWindow.xaml.cs – obsługa:

private void DodajKsiazke_Click(object sender, RoutedEventArgs e)
{
    var okno = new DodajKsiazkeWindow();
    if (okno.ShowDialog() == true && okno.Model != null)
    {
        vm.Ksiazki.Add(okno.Model);
    }
}

2) Nowe okno dialogowe

DodajKsiazkeWindow.xaml

<Window x:Class="BibliotekaApp.DodajKsiazkeWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Dodaj książkę" Height="260" Width="360" WindowStartupLocation="CenterOwner">
    <Grid Margin="16" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" RowSpacing="8">
        <TextBlock Text="Tytuł:" />
        <TextBox x:Name="txtTytul" Grid.Row="1" />

        <TextBlock Text="Autor:" Grid.Row="2" />
        <TextBox x:Name="txtAutor" Grid.Row="3" />

        <TextBlock Text="Rok wydania (opcjonalnie):" Grid.Row="4" />
        <StackPanel Grid.Row="5" Orientation="Horizontal" Spacing="8">
            <TextBox x:Name="txtRok" Width="120" />
            <CheckBox x:Name="chkWypozyczona" Content="Wypożyczona" VerticalAlignment="Center" />
            <Button Content="Zapisz" Width="80" Click="Zapisz_Click" />
        </StackPanel>

        <TextBlock x:Name="txtBledy" Grid.Row="6" Foreground="Red" TextWrapping="Wrap" />
    </Grid>
</Window>

DodajKsiazkeWindow.xaml.cs

using System.Windows;
using BibliotekaApp.Modele;
using BibliotekaApp.Walidacja;

namespace BibliotekaApp
{
    public partial class DodajKsiazkeWindow : Window
    {
        public Ksiazka? Model { get; private set; }

        public DodajKsiazkeWindow()
        {
            InitializeComponent();
        }

        private void Zapisz_Click(object sender, RoutedEventArgs e)
        {
            var (ok, bledy, model) = WalidatorKsiazki.SprawdzIZbuduj(
                txtTytul.Text, txtAutor.Text, txtRok.Text, chkWypozyczona.IsChecked == true);

            if (!ok)
            {
                txtBledy.Text = "Popraw dane:\n- " + string.Join("\n- ", bledy);
                return;
            }

            Model = model;
            DialogResult = true;
            Close();
        }
    }
}

Efekt: w razie błędu – komunikaty w TextBlock; jeśli OK – okno zwraca DialogResult=true, a MainWindow dodaje książkę do kolekcji.

###########################

Schemat działania: Model – Binding – ListView w WPF

       ┌──────────────────────────────────────────────────────────┐
(1)    │ MODELE/Ksiazka.cs                                       │
       │ Klasa z właściwościami: Tytul, Autor, Wypozyczona, ...  │
       │ Implementuje INotifyPropertyChanged                     │
       └──────────────┬──────────────────────────────────────────┘
                      │   (tworzy obiekty książek)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(2)    │ LOGIKA/BibliotekaViewModel.cs                            │
       │ Zawiera ObservableCollection<Ksiazka>                    │
       │ = dynamiczna lista obiektów (Ksiazki)                    │
       │ Dodaje, usuwa, przechowuje książki                       │
       └──────────────┬──────────────────────────────────────────┘
                      │   (przekazanie kolekcji do UI)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(3)    │ MAINWINDOW.XAML.CS                                       │
       │ Tworzy obiekt BibliotekaViewModel                        │
       │ DataContext = vm  → powiązanie całego okna z danymi      │
       └──────────────┬──────────────────────────────────────────┘
                      │   (dane powiązane z UI)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(4)    │ MAINWINDOW.XAML                                          │
       │ ListView ItemsSource="{Binding Ksiazki}"                 │
       │ ItemTemplate → wzór jednego wiersza                      │
       │ Każdy wiersz ma DataContext = jedna książka              │
       └──────────────┬──────────────────────────────────────────┘
                      │   (binding do właściwości książki)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(5)    │ BINDING ENGINE                                           │
       │ Łączy pola tekstowe, przyciski, etykiety z właściwościami│
       │ np. Text="{Binding Tytul}", Content="{Binding AkcjaTekst}"│
       │ Działa w obie strony (TwoWay)                            │
       └──────────────┬──────────────────────────────────────────┘
                      │   (zmiana w modelu odświeża UI)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(6)    │ UŻYTKOWNIK                                              │
       │ Klika przycisk w wierszu ListView                        │
       │ sender.DataContext → konkretny obiekt Ksiazka            │
       │ Kod: k.Wypozyczona = !k.Wypozyczona;                     │
       └──────────────┬──────────────────────────────────────────┘
                      │   (zgłoszenie zmiany)
                      ▼
       ┌──────────────────────────────────────────────────────────┐
(7)    │ INotifyPropertyChanged                                   │
       │ Wywołuje OnPropertyChanged("Wypozyczona")                │
       │ Binding Engine aktualizuje tylko ten wiersz              │
       │ Lista odświeża się automatycznie                         │
       └──────────────────────────────────────────────────────────┘

Opis poszczególnych kroków

(1) Model – Ksiazka.cs

Zawiera pola opisujące książkę i zgłasza zmiany.
Gdy właściwość się zmienia, wywołuje OnPropertyChanged, co informuje WPF, że trzeba odświeżyć UI.

(2) Logika – BibliotekaViewModel.cs

Tworzy kolekcję ObservableCollection<Ksiazka>.
Ta kolekcja potrafi sama powiadomić interfejs o dodaniu, usunięciu lub zmianie elementu.

(3) Połączenie danych – MainWindow.xaml.cs

W konstruktorze okna ustawiasz:

DataContext = new BibliotekaViewModel();

To jakby powiedzieć:

„Całe to okno ma patrzeć na dane z tej klasy”.

(4) Widok – MainWindow.xaml

ListView ma:

<ListView ItemsSource="{Binding Ksiazki}">

czyli automatycznie pobiera listę książek z ViewModelu (DataContext.Ksiazki),
a ItemTemplate mówi, jak wygląda każdy wiersz.
WPF tworzy po jednym „kafelku” (DataTemplate) dla każdej książki.

(5) Silnik bindingu

Łączy dane (Tytul, Autor, StatusOpis) z elementami interfejsu (TextBlock, Button).
Jeśli Binding jest dwukierunkowy (Mode=TwoWay), zmiana w formularzu zmienia dane w obiekcie i odwrotnie.

(6) Reakcja użytkownika

Użytkownik klika przycisk „Wypożycz / Zwróć”.
Kod:

if ((sender as FrameworkElement)?.DataContext is Ksiazka k)
    k.Wypozyczona = !k.Wypozyczona;

Każdy przycisk ma własny DataContext, więc dokładnie wie, której książki dotyczy.

(7) Aktualizacja interfejsu

Model zgłasza PropertyChanged.
Silnik bindingu odbiera to i automatycznie odświeża tylko ten element listy,
bez ręcznego odmalowywania UI.


W skrócie

EtapCo się dziejeMechanizm
1Tworzymy klasę KsiazkaINotifyPropertyChanged
2Tworzymy listę książekObservableCollection
3Podpinamy dane do oknaDataContext
4Ustawiamy ItemsSource listyBinding
5WPF łączy dane z kontrolkamiBinding Engine
6Klik w przycisk → zmiana danychDataContext (wiersza)
7Model wysyła powiadomienie → UI się odświeżaPropertyChanged

###########################

***

Model z polem okładki

Modele/Ksiazka.cs
(dodaj jedną właściwość; ścieżka względna do pliku w projekcie albo pełna ścieżka na dysku)

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace BibliotekaApp.Modele
{
    public class Ksiazka : INotifyPropertyChanged
    {
        private string _tytul = "";
        private string _autor = "";
        private bool _wypozyczona;
        private string? _okladka; // np. "Assets/Okladki/hobbit.jpg" albo pełna ścieżka

        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)); OnPropertyChanged(nameof(AkcjaTekst)); }
        }

        public string? Okladka { get => _okladka; set { _okladka = value; OnPropertyChanged(); } }

        public string StatusOpis => Wypozyczona ? "Wypożyczona" : "Dostępna";
        public string AkcjaTekst => Wypozyczona ? "Zwróć" : "Wypożycz";

        public event PropertyChangedEventHandler? PropertyChanged;
        void OnPropertyChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
    }
}

2) Gdzie trzymać pliki graficzne

Najprościej: folder w projekcie np. Assets/Okladki.

  • wrzuć tam pliki JPG/PNG,
  • zaznacz pliki → Właściwości:
    • Build Action: Resource
    • (opcjonalnie) Copy to Output Directory: Do not copy – nie trzeba, bo Resource pakuje do EXE.

Wtedy możesz używać krótkich ścieżek (bez pack://...), WPF sam je znajdzie.

3) Przykładowe dane z okładką

Logika/BibliotekaViewModel.cs (fragment)

Ksiazki = new ObservableCollection<Ksiazka>
{
    new Ksiazka { Tytul="Hobbit", Autor="J.R.R. Tolkien", Okladka="Assets/Okladki/hobbit.jpg" },
    new Ksiazka { Tytul="Dune", Autor="Frank Herbert", Wypozyczona=true, Okladka="Assets/Okladki/dune.jpg" },
    new Ksiazka { Tytul="1984", Autor="George Orwell", Okladka="Assets/Okladki/1984.jpg" }
};

4) Szablon wiersza listy: okładka po lewej, tekst po prawej

MainWindow.xamlListView.ItemTemplate

<ListView x:Name="lvKsiazki"
          ScrollViewer.CanContentScroll="True"
          VirtualizingPanel.IsVirtualizing="True">
  <ListView.ItemTemplate>
    <DataTemplate DataType="{x:Type models:Ksiazka}">
      <Grid Margin="0,0,0,8" ColumnDefinitions="Auto,* ,Auto">
        <!-- OKŁADKA PO LEWEJ -->
        <Border Width="64" Height="96" Margin="0,0,12,0" Background="#EEE" CornerRadius="4">
          <Image Source="{Binding Okladka, TargetNullValue={StaticResource PlaceholderImg}}"
                 Stretch="UniformToFill"
                 Width="64" Height="96"/>
        </Border>

        <!-- TEKSTY -->
        <StackPanel Grid.Column="1" VerticalAlignment="Center">
          <TextBlock Text="{Binding Tytul}" FontWeight="Bold" FontSize="14"/>
          <TextBlock Text="{Binding Autor}" Foreground="#666"/>
          <TextBlock Text="{Binding StatusOpis}"/>
        </StackPanel>

        <!-- PRZYCISK AKCJI PO PRAWEJ -->
        <Button Grid.Column="2"
                Content="{Binding AkcjaTekst}"
                Padding="10,4" Margin="12,0,0,0"
                Click="btnAkcja_Click"/>
      </Grid>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

oraz u góry pliku (w zasobach okna) możesz dodać placeholder na brak okładki:

<Window.Resources>
  <BitmapImage x:Key="PlaceholderImg" UriSource="Assets/Okladki/placeholder.png"/>
</Window.Resources>

jeśli nie chcesz placeholdera, usuń TargetNullValue=.... Gdy Okladka jest pusta, Image będzie po prostu „szare tło” z Border.

dlaczego to działa z listą?

tak jak przy przycisku: każdy wiersz ma własny DataContext → konkretny obiekt Ksiazka.
Image.Source="{Binding Okladka}" w wierszu 1 binduje do Ksiazka#1.Okladka, w wierszu 2 do Ksiazka#2.Okladka, itd.
WPF sam tworzy wizualny element dla każdej pozycji i wiąże go z odpowiednim obiektem z ItemsSource.