Klasa w osobnym pliku i okno

W kilku ostatnich lekcjach dane trzymałeś w tablicach równoległych: nazwy w jednej, ceny w drugiej, pliki w trzeciej. Działa, ale łatwo o pomyłkę — wystarczy dodać element do jednej tablicy i zapomnieć o drugiej. Z kursu C# znasz lepsze rozwiązanie: klasę. W tej lekcji opiszesz dane klasą w osobnym pliku, będziesz tworzyć obiekty z formularza, trzymać je w List<T> i pokazywać w oknie.

C# XAML klasa List<T> ToString as 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz do projektu WPF nową klasę w osobnym pliku.
  • Utworzysz obiekt z danych wpisanych w formularz.
  • Przechowasz obiekty w List<T> i przejdziesz po nich pętlą.
  • Pokażesz obiekty w ListBox dzięki ToString() i odczytasz zaznaczony obiekt przez as.
  • Rozdzielisz dane (model) od tego, co widać w oknie (widok), i odświeżysz widok po zmianie danych.
TEORIA

Tablice równoległe a klasa

było — trzy tablice
string[] tytuly = { "Lalka", "Solaris" };
string[] autorzy = { "Bolesław Prus", "Stanisław Lem" };
int[] lata = { 1890, 1961 };
// tytuly[1], autorzy[1] i lata[1] opisują tę samą książkę — pilnujesz tego sam
jest — jedna lista obiektów
List<Ksiazka> ksiazki = new List<Ksiazka>();
ksiazki.Add(new Ksiazka("Lalka", "Bolesław Prus", 1890));
ksiazki.Add(new Ksiazka("Solaris", "Stanisław Lem", 1961));
// ksiazki[1] to cała książka: Tytul, Autor, Rok razem

Obiekt trzyma wszystkie dane jednej rzeczy razem. Lista może rosnąć i maleć — tablica ma stały rozmiar. A gdy książka dostanie nową cechę (np. „kto wypożyczył”), dopisujesz jedną właściwość w klasie zamiast nowej tablicy.

TEORIA

Klasa w osobnym pliku

W Eksploratorze rozwiązań kliknij projekt prawym przyciskiem → Dodaj → Klasa…, wpisz nazwę Ksiazka.cs. Visual Studio utworzy plik z pustą klasą w przestrzeni nazw projektu — tej samej co MainWindow, więc okno „widzi” klasę bez dodatkowego using. Dopisz public przed class.

Lekcja23/
├── App.xaml
├── Ksiazka.csmodel: dane jednej książki
├── MainWindow.xamlwidok: jak wygląda okno
├── MainWindow.xaml.csobsługa zdarzeń, lista książek
└── Lekcja23.csproj
Ksiazka.cs — pełny plik
namespace Lekcja23;

// Model danych: jedna książka z biblioteczki klasowej.
// Klasa nic nie wie o oknie — można jej użyć w dowolnym programie.
public class Ksiazka
{
    public string Tytul { get; set; }
    public string Autor { get; set; }
    public int Rok { get; set; }
    public string Wypozyczajacy { get; set; } = "";   // pusty napis = książka na półce

    public Ksiazka(string tytul, string autor, int rok)
    {
        Tytul = tytul;
        Autor = autor;
        Rok = rok;
    }

    // właściwość tylko do odczytu, wyliczana z innej
    public bool Dostepna => Wypozyczajacy == "";

    // tak obiekt wyświetli się na liście
    public override string ToString()
    {
        string stan = Dostepna ? "" : "  [wypożyczona]";
        return $"{Tytul} — {Autor} ({Rok}){stan}";
    }
}

Wszystko w tej klasie znasz z kursu C#: właściwości z get; set;, konstruktor z parametrami, nadpisanie ToString(). Nowy może być tylko zapis public bool Dostepna => Wypozyczajacy == ""; — to właściwość tylko do odczytu, której wartość jest wyliczana przy każdym odczycie. Dzięki niej w kodzie piszesz k.Dostepna zamiast powtarzać warunek.

TEORIA

List<T>

ZapisCo robi
new List<Ksiazka>()pusta lista obiektów typu Ksiazka
lista.Add(obiekt)dodaje na koniec
lista.Remove(obiekt)usuwa wskazany obiekt
lista.RemoveAt(i)usuwa element o numerze i
lista.Countliczba elementów (w tablicy było Length)
lista[i]element o numerze i
lista.Contains(obiekt)czy obiekt jest na liście
foreach (Ksiazka k in lista)przejście po wszystkich elementach

Lista jest polem klasy okna (private readonly List<Ksiazka> ksiazki), więc żyje tak długo jak okno. readonly oznacza, że zmienna zawsze wskazuje tę samą listę — elementy możesz dodawać i usuwać.

TEORIA

Obiekty w ListBox

Do Items można dodać dowolny obiekt, nie tylko napis. Lista wyświetla wtedy wynik jego metody ToString(). Bez nadpisania zobaczysz nazwę klasy: „Lekcja23.Ksiazka”.

MainWindow.xaml.cs
foreach (Ksiazka k in ksiazki)
{
    Lista.Items.Add(k);                        // na liście: k.ToString()
}

// zaznaczona pozycja to CAŁY obiekt, nie napis
Ksiazka? zaznaczona = Lista.SelectedItem as Ksiazka;
if (zaznaczona != null)
{
    Info.Text = $"{zaznaczona.Tytul}, wydana w {zaznaczona.Rok}";
}

Operator as działa jak rzutowanie, ale zamiast wyjątku zwraca null, gdy obiekt nie jest danego typu albo gdy nic nie zaznaczono. Dlatego po as zawsze sprawdzasz != null.

TEORIA

Model i widok

Lista ksiazki to model — prawdziwe dane. ListBox to widok — to, co aktualnie widać. Mogą się różnić: filtr „tylko dostępne” chowa część książek, ale nie usuwa ich z modelu. Zasada pracy:

  1. każda zmiana dotyczy modelu (ksiazki.Add, zmiana właściwości obiektu, ksiazki.Remove);
  2. po zmianie wywołujesz jedną metodę, która buduje widok od nowa z modelu (PokazListe).

Zmiana właściwości obiektu nie odświeża sama tekstu na liście — ListBox nie wie, że ToString() zwróciłby teraz coś innego. Przebudowanie widoku rozwiązuje problem. W bloku o danych (lekcje 41–43) poznasz mechanizm, który odświeża widok automatycznie.

PRZYKŁAD

Biblioteczka klasowa

Klasa prowadzi biblioteczkę. Po lewej formularz nowej książki, w środku lista wszystkich książek, po prawej szczegóły zaznaczonej z możliwością wypożyczenia, zwrotu i usunięcia. Na dole filtr „tylko dostępne” i statystyka. Dane książki opisuje klasa Ksiazka z pliku powyżej.

Biblioteczka klasy 3TI–□✕
Nowa książka
Tytuł:
 
Autor:
 
Rok wydania:
 
Dodaj
Lalka — Bolesław Prus (1890)
Solaris — Stanisław Lem (1961) [wypożyczona]
Wiedźmin. Ostatnie życzenie — Andrzej Sapkowski (1993)
C# 12. Programowanie — Mark J. Price (2023)
tylko dostępne
Książek: 4, wypożyczonych: 1
Solaris
Stanisław Lem, 1961
Wypożyczona: Ola Nowak
Kto wypożycza:
 
Wypożycz
Zwróć
Usuń z biblioteczki

zaznaczona wypożyczona książka — aktywny jest tylko „Zwróć” i „Usuń”

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Biblioteczka klasy 3TI" Height="440" Width="820"
        MinWidth="700" MinHeight="380" WindowStartupLocation="CenterScreen">

    <DockPanel Margin="12">
        <!-- lewa kolumna: nowa książka -->
        <Border DockPanel.Dock="Left" Width="200" Padding="10" Margin="0,0,10,0"
                BorderBrush="LightGray" BorderThickness="1" CornerRadius="6">
            <StackPanel>
                <TextBlock Text="Nowa książka" FontWeight="Bold" Margin="0,0,0,6"/>
                <Label Content="_Tytuł:" Padding="0,2" Target="{Binding ElementName=Tytul}"/>
                <TextBox x:Name="Tytul" Margin="0,0,0,6"/>
                <Label Content="_Autor:" Padding="0,2" Target="{Binding ElementName=Autor}"/>
                <TextBox x:Name="Autor" Margin="0,0,0,6"/>
                <Label Content="_Rok wydania:" Padding="0,2" Target="{Binding ElementName=Rok}"/>
                <TextBox x:Name="Rok" Width="60" HorizontalAlignment="Left" MaxLength="4"/>
                <Button Content="Dodaj" Margin="0,10,0,0" Height="28" Click="Dodaj_Click"/>
            </StackPanel>
        </Border>

        <!-- prawa kolumna: szczegóły zaznaczonej -->
        <Border DockPanel.Dock="Right" Width="230" Padding="10" Margin="10,0,0,0"
                BorderBrush="LightGray" BorderThickness="1" CornerRadius="6">
            <StackPanel x:Name="Szczegoly" IsEnabled="False">
                <TextBlock x:Name="SzTytul" FontWeight="Bold" FontSize="15" TextWrapping="Wrap"/>
                <TextBlock x:Name="SzAutor" Margin="0,2,0,0"/>
                <TextBlock x:Name="SzStatus" Margin="0,8,0,8" TextWrapping="Wrap"/>
                <Label Content="_Kto wypożycza:" Padding="0,2" Target="{Binding ElementName=Kto}"/>
                <TextBox x:Name="Kto" Margin="0,0,0,6"/>
                <Button x:Name="Wypozycz" Content="Wypożycz" Height="28" Margin="0,0,0,6"
                        Click="Wypozycz_Click"/>
                <Button x:Name="Zwroc" Content="Zwróć" Height="28" Margin="0,0,0,6"
                        Click="Zwroc_Click"/>
                <Button Content="Usuń z biblioteczki" Height="28" Click="Usun_Click"/>
            </StackPanel>
        </Border>

        <!-- dół: filtr i statystyka -->
        <DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
            <CheckBox x:Name="TylkoDostepne" DockPanel.Dock="Left" Content="tylko dostępne"
                      Checked="Filtr_Zmiana" Unchecked="Filtr_Zmiana"/>
            <TextBlock x:Name="Statystyka" TextAlignment="Right"/>
        </DockPanel>

        <!-- środek: lista obiektów -->
        <ListBox x:Name="Lista" SelectionChanged="Lista_SelectionChanged"/>
    </DockPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Lekcja23;

public partial class MainWindow : Window
{
    // wszystkie książki — niezależnie od tego, co akurat widać na liście
    private readonly List<Ksiazka> ksiazki = new List<Ksiazka>();

    public MainWindow()
    {
        InitializeComponent();

        ksiazki.Add(new Ksiazka("Lalka", "Bolesław Prus", 1890));
        ksiazki.Add(new Ksiazka("Solaris", "Stanisław Lem", 1961));
        ksiazki.Add(new Ksiazka("Wiedźmin. Ostatnie życzenie", "Andrzej Sapkowski", 1993));
        ksiazki.Add(new Ksiazka("C# 12. Programowanie", "Mark J. Price", 2023));
        ksiazki[1].Wypozyczajacy = "Ola Nowak";       // Solaris jest wypożyczone

        PokazListe();
    }

    private void Dodaj_Click(object sender, RoutedEventArgs e)
    {
        string tytul = Tytul.Text.Trim();
        string autor = Autor.Text.Trim();

        if (tytul == "" || autor == "" ||
            !int.TryParse(Rok.Text, out int rok) || rok < 1450 || rok > DateTime.Today.Year)
        {
            MessageBox.Show("Podaj tytuł, autora i poprawny rok wydania.", "Nowa książka",
                            MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }

        Ksiazka nowa = new Ksiazka(tytul, autor, rok);   // obiekt z danych formularza
        ksiazki.Add(nowa);

        Tytul.Clear();
        Autor.Clear();
        Rok.Clear();
        Tytul.Focus();

        PokazListe();
        Lista.SelectedItem = nowa;                         // zaznacz dodaną książkę
    }

    private void Wypozycz_Click(object sender, RoutedEventArgs e)
    {
        Ksiazka? k = Lista.SelectedItem as Ksiazka;
        if (k == null || Kto.Text.Trim() == "")
        {
            Kto.Focus();
            return;
        }

        k.Wypozyczajacy = Kto.Text.Trim();
        Kto.Clear();
        PokazListe();
    }

    private void Zwroc_Click(object sender, RoutedEventArgs e)
    {
        Ksiazka? k = Lista.SelectedItem as Ksiazka;
        if (k != null)
        {
            k.Wypozyczajacy = "";
            PokazListe();
        }
    }

    private void Usun_Click(object sender, RoutedEventArgs e)
    {
        Ksiazka? k = Lista.SelectedItem as Ksiazka;
        if (k == null)
        {
            return;
        }

        MessageBoxResult odp = MessageBox.Show($"Usunąć „{k.Tytul}”?", "Usuwanie",
                                               MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (odp == MessageBoxResult.Yes)
        {
            ksiazki.Remove(k);
            PokazListe();
        }
    }

    private void Filtr_Zmiana(object sender, RoutedEventArgs e)
    {
        PokazListe();
    }

    private void Lista_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        PokazSzczegoly();
    }

    // Lista w oknie to tylko WIDOK listy ksiazki — budujemy ją od nowa
    private void PokazListe()
    {
        object? zaznaczona = Lista.SelectedItem;
        Lista.Items.Clear();

        int wypozyczone = 0;
        foreach (Ksiazka k in ksiazki)
        {
            if (!k.Dostepna)
            {
                wypozyczone++;
            }

            if (TylkoDostepne.IsChecked == true && !k.Dostepna)
            {
                continue;       // filtr: pomijamy wypożyczone
            }
            Lista.Items.Add(k); // na liście widać wynik ToString()
        }

        if (zaznaczona != null && Lista.Items.Contains(zaznaczona))
        {
            Lista.SelectedItem = zaznaczona;
        }

        Statystyka.Text = $"Książek: {ksiazki.Count}, wypożyczonych: {wypozyczone}";
        PokazSzczegoly();
    }

    private void PokazSzczegoly()
    {
        Ksiazka? k = Lista.SelectedItem as Ksiazka;

        if (k == null)
        {
            Szczegoly.IsEnabled = false;
            SzTytul.Text = "Wybierz książkę z listy";
            SzAutor.Text = "";
            SzStatus.Text = "";
            return;
        }

        Szczegoly.IsEnabled = true;
        SzTytul.Text = k.Tytul;
        SzAutor.Text = $"{k.Autor}, {k.Rok}";
        SzStatus.Text = k.Dostepna ? "Na półce" : $"Wypożyczona: {k.Wypozyczajacy}";
        SzStatus.Foreground = k.Dostepna ? Brushes.Green : Brushes.Firebrick;
        Wypozycz.IsEnabled = k.Dostepna;
        Kto.IsEnabled = k.Dostepna;
        Zwroc.IsEnabled = !k.Dostepna;
    }
}

Co dzieje się po kolei

  • Okno to DockPanel: formularz po lewej, szczegóły po prawej, filtr na dole, lista jako ostatnie dziecko wypełnia środek.
  • Konstruktor tworzy cztery obiekty Ksiazka i dodaje je do listy ksiazki. ksiazki[1].Wypozyczajacy = … zmienia właściwość drugiej książki.
  • Dodaj_Click sprawdza pola, tworzy obiekt new Ksiazka(tytul, autor, rok) z danych formularza i dodaje go do modelu. Potem przebudowuje widok i zaznacza nową książkę — SelectedItem przyjmuje obiekt.
  • Lista.SelectedItem as Ksiazka daje zaznaczoną książkę albo null. Wypożyczenie i zwrot zmieniają tylko właściwość Wypozyczajacy obiektu.
  • PokazListe zapamiętuje zaznaczenie, czyści ListBox, dodaje obiekty z modelu (pomijając wypożyczone przy włączonym filtrze), przywraca zaznaczenie i liczy statystykę. Statystyka liczy całą bibliotekę, także ukryte pozycje.
  • continue w pętli przechodzi od razu do następnej książki — tak działa filtr.
  • PokazSzczegoly wypełnia prawy panel i włącza tylko pasujące przyciski. Bez zaznaczenia cały panel jest nieaktywny.
  • Klasa Ksiazka nie ma ani jednej linii związanej z oknem — mógłbyś jej użyć w programie konsolowym.

Sprawdź trzy rzeczy

  1. Usuń z klasy Ksiazka metodę ToString(). Lista pokaże cztery razy „Lekcja23.Ksiazka”.
  2. Usuń z Wypozycz_Click wywołanie PokazListe(). Po wypożyczeniu tekst na liście się nie zmieni, choć obiekt już się zmienił — zmienią się dopiero po kliknięciu filtra.
  3. Zaznacz filtr „tylko dostępne” i wypożycz zaznaczoną książkę. Zniknie z listy, ale statystyka nadal ją liczy.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
List<T>kolekcjaAdd, Remove, RemoveAt, Count, [i], Contains.
public override string ToString()metodaTekst obiektu — także na liście w oknie.
public bool X => warunek;właściwośćWyliczana przy każdym odczycie, tylko do odczytu.
obiekt as TypoperatorRzutowanie, które daje null zamiast wyjątku.
Items.Add(obiekt)metodaDodaje obiekt do listy w oknie.
SelectedItem = obiektprzypisanieZaznacza wskazany obiekt.
continueinstrukcjaPrzejście do następnego obiegu pętli.
readonlymodyfikatorPole przypisane raz — zawsze ta sama lista.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
class Ksiazka bez public, użyta w publicznym poluBłąd CS0052 o niespójnej dostępności. Dodaj public.
inna przestrzeń nazw w pliku klasyCS0246 — okno nie widzi klasy. Ustaw namespace Lekcja23; albo dodaj using.
na liście widać „Lekcja23.Ksiazka”Brak override ToString().
(Ksiazka)Lista.SelectedItem bez zaznaczeniaNullReferenceException przy pierwszym użyciu. Użyj as i sprawdź null.
usuwanie z ListBox, a nie z listy ksiazkiPo przebudowie widoku „usunięta” książka wraca.
zmiana właściwości bez odświeżenia widokuNa liście widać stary tekst.
ksiazki.LengthCS1061 — lista ma Count.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Dane opisz klasą w osobnym pliku, przechowuj w List<T>, a widok buduj z modelu jedną metodą. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Lista uczniów★☆☆

Klasa Uczen z właściwościami Imie, Nazwisko, Klasa i konstruktorem. Na liście uczeń wyświetla się jako „Nowak Jan (3TI)”. Formularz dodaje ucznia (wszystkie pola wymagane), przycisk Usuń usuwa zaznaczonego. Pod listą: „Uczniów: 12”. Kliknięcie ucznia pokazuje pod listą jego inicjały dużą czcionką („J.N.”) — z metody Inicjaly() w klasie.

pokaż wskazówkę
  • Pierwsza litera napisu to Imie[0].
ZAD 2Magazyn sklepiku★☆☆

Klasa Produkt: nazwa, cena (decimal), ilość w magazynie i metoda Wartosc() zwracająca cenę × ilość. Na liście: „Drożdżówka — 3,50 zł × 24 szt.”. Formularz dodaje produkt, przyciski +1 i −1 zmieniają ilość zaznaczonego (nie poniżej 0). Pod listą łączna wartość magazynu, a produkty z ilością 0 mają na liście dopisek „BRAK”.

pokaż wskazówkę
  • Po zmianie ilości przebuduj widok i przywróć zaznaczenie — inaczej po każdym „+1” trzeba klikać produkt od nowa.
ZAD 3Dziennik ocen★★☆

Klasa Ocena: przedmiot, wartość (1–6), waga (1–3), opis (np. „sprawdzian z WPF”), data dodania. Formularz: przedmiot (lista rozwijana), ocena (przyciski opcji 1–6), waga (lista), opis. Nad listą ocen lista rozwijana filtra przedmiotu z pozycją „wszystkie”. Pod listą średnia ważona widocznych ocen z dwoma miejscami po przecinku i przewidywana ocena końcowa (od 4,75 — celujący, od 4,5 — bardzo dobry itd., progi ustal sam).

pokaż wskazówkę
  • Średnia ważona = suma (ocena × waga) / suma wag — licz w tej samej pętli, która buduje widok.
  • Gdy po filtrze nie ma ocen, nie dziel przez zero.
ZAD 4Plan wycieczki — wersja z klasą★★☆

Przepisz zadanie „Plan wycieczki do Gdańska” z lekcji 17 tak, żeby zamiast tablic równoległych używało klasy Atrakcja (nazwa, koszt, czas w godzinach) i dwóch list: List<Atrakcja> dostepne i List<Atrakcja> plan. Na obu listach w oknie atrakcja wyświetla się jako „Rejs statkiem (45 zł, 2 h)”. Działanie programu ma się nie zmienić. W komentarzu na początku pliku okna wypisz trzy miejsca, w których kod stał się prostszy.

pokaż wskazówkę
  • Przeniesienie atrakcji: dostepne.Remove(a); plan.Add(a); i przebudowa obu widoków. Array.IndexOf nie jest już potrzebne.
ZAD 5Książka adresowa★★☆

Klasa Kontakt: imię, nazwisko, telefon, e-mail, Ulubiony (bool). Ulubione kontakty na liście mają gwiazdkę „★ ” przed nazwiskiem i są wyświetlane na początku. Kliknięcie kontaktu wypełnia formularz jego danymi; Zapisz zmiany zmienia zaznaczony obiekt, Dodaj jako nowy tworzy nowy. Pole wyszukiwania filtruje po imieniu, nazwisku i telefonie.

pokaż wskazówkę
  • Ulubione na początku: dwie pętle w metodzie budującej widok — najpierw ulubione, potem pozostałe.
  • „Zapisz zmiany” nie tworzy nowego obiektu — ustawia właściwości tego, który jest zaznaczony.
ZAD 6Wypożyczalnia sprzętu sportowego★★★

Dwie klasy: Sprzet (numer, nazwa, rodzaj — piłka, rakieta, rower…) i Wypozyczenie (wypożyczony Sprzet, imię i nazwisko ucznia, data wypożyczenia, termin zwrotu, data zwrotu albo brak). Okno: lista sprzętu (dostępny / wypożyczony), formularz wypożyczenia zaznaczonego sprzętu na 1–14 dni, lista aktywnych wypożyczeń z wyróżnieniem przeterminowanych („PO TERMINIE: 3 dni”) i przycisk Zwrot. Pod spodem historia zwrotów. Sprzęt wypożyczony nie może zostać wypożyczony drugi raz.

pokaż wskazówkę
  • Właściwość Sprzet w klasie Wypozyczenie to odwołanie do obiektu z listy sprzętu — nie kopia. Dostępność sprzętu możesz wyliczać, sprawdzając, czy istnieje aktywne wypożyczenie z tym obiektem.
  • Dni po terminie: (DateTime.Today - TerminZwrotu).Days. Data zwrotu „brak” to typ DateTime? z wartością null.
PODSUMOWANIE

Co trzeba zapamiętać

  • Dane opisuj klasą w osobnym pliku (Dodaj → Klasa…) — zamiast tablic równoległych.
  • Obiekty trzymaj w List<T> jako polu okna; tworzysz je z danych formularza przez new.
  • ListBox może przechowywać obiekty i wyświetla ich ToString().
  • Zaznaczony obiekt: Lista.SelectedItem as Typ + sprawdzenie null.
  • Model (lista obiektów) zmieniasz, widok (ListBox) budujesz od nowa jedną metodą.

Dokumentacja: Microsoft Learn — List<T>.