Aplikacje desktopowe · Projekt końcowy

Projekt końcowy: Wypożyczalnia sprzętu sportowego

Czas połączyć wszystko w jeden projekt, w takiej formie, w jakiej zadania pojawiają się na egzaminie INF.04. Zaprojektujesz i zbudujesz kompletną aplikację okienkową z bazą danych, walidacją i podziałem na warstwy, najpierw na kartce, potem w kodzie.

  • około 120 minut
  • WPF · SQLite · model · walidator
  • INF.04.5

Po tej lekcji: zaplanować aplikację przed pisaniem kodu, podzielić ją na Modele, Walidacja, Baza i widok oraz zbudować pełny cykl: lista, dodawanie z walidacją, wypożyczanie, zwrot, wyszukiwanie i trwały zapis.

Zacznijmy od zlecenia

Mała firma potrzebuje prostego systemu

Wypożyczalnia sprzętu sportowego chce zarządzać asortymentem i wypożyczeniami, bez kupowania drogiego, gotowego oprogramowania. Pracownik ma widzieć listę sprzętu, dodawać nowy, wypożyczać go i przyjmować zwroty.

To dokładnie ten typ zadania, jaki pojawia się na egzaminie. Podejdź do niego jak do egzaminu: najpierw plan na kartce, potem model danych, dopiero na końcu kod.

1 · Co ma robić program

Sześć wymagań funkcjonalnych

WymaganieCo to znaczy w praktyce
Lista sprzętuDataGrid z nazwą, kategorią, ceną za dzień i statusem (dostępny lub wypożyczony).
Dodawanie z walidacjąFormularz. Nazwa i kategoria niepuste, cena dodatnia. Błędy pokazywane wszystkie naraz.
WypożyczanieZaznaczenie sprzętu i „Wypożycz”: status się zmienia po potwierdzeniu w MessageBox.
ZwrotAnalogicznie, status wraca na „Dostępny”.
Trwałe daneBaza SQLite (albo JSON, jeśli wolisz prościej). Dane przetrwają zamknięcie programu.
WyszukiwaniePo nazwie lub kategorii, najlepiej na bieżąco podczas pisania.
Przed pisaniem kodu zapisz te sześć punktów w notatniku. Na koniec odhaczysz je po kolei.

2 · Podział na warstwy

Cztery części, cztery odpowiedzialności

To zasada oddzielenia logiki od interfejsu z lekcji o MVVM, zastosowana w całym projekcie.

Modele/Sprzet

Klasa z danymi jednego egzemplarza sprzętu. Bez wiedzy o oknie.

Na przykład Nazwa, Kategoria, CenaZaDzien, Status

Walidacja/WalidatorSprzetu

Zwraca listę błędów. Niczego nie wyświetla.

Na przykład Sprawdz(sprzet)

Baza/BazaDanych

Cały SQL w jednym miejscu, z parametrami.

Na przykład Dodaj, Szukaj, ZmienStatus

OknoXAML + code-behind

Cienki klej: odczytuje pola, woła walidator i bazę, odświeża tabelę.

Na przykład MainWindow

3 · Plan na kartce

Układ okna zaplanowany przed kodem

Tak może wyglądać plan okna: wyszukiwanie u góry, tabela w środku, formularz po prawej, akcje pod tabelą. Całość to jeden DockPanel. Zmień szerokość formularza albo przenieś go na lewą stronę i zobacz efekt.

Ta interakcja wymaga włączonego JavaScriptu.

Układ okna wybierasz zanim napiszesz pierwszą metodę. Dzięki temu wiesz, które kontrolki potrzebują x:Name, a które zdarzeń.

4 · Kolejność kroków

Jak podejść do projektu na egzaminie

Idź w tej kolejności. Każdy krok można sprawdzić, zanim zaczniesz następny.

5 · Dopiero po własnej próbie

Rozwiązanie do porównania

Poniżej pełny, sprawdzony kompilatorem projekt. Otwórz go dopiero po własnej próbie: najpierw spróbuj każdą część napisać sam, a potem porównaj. Zwróć uwagę nie na dosłowny kod, lecz na podział odpowiedzialności i kolejność w metodach.

Modele/

Sprzet.cs

Pięć właściwości. Id nadaje baza, a Status startuje jako „Dostępny”.

Modele/Sprzet.cscsharp
namespace Wypozyczalnia.Modele;

public class Sprzet
{
    public int Id { get; set; }
    public string Nazwa { get; set; } = "";
    public string Kategoria { get; set; } = "";
    public decimal CenaZaDzien { get; set; }
    public string Status { get; set; } = "Dostępny";
}

Walidacja/

WalidatorSprzetu.cs

Trzy reguły, każda dodaje osobny komunikat. Walidator zwraca listę i niczego nie wyświetla.

Walidacja/WalidatorSprzetu.cscsharp
using System.Collections.Generic;
using Wypozyczalnia.Modele;

namespace Wypozyczalnia.Walidacja;

public class WalidatorSprzetu
{
    public List<string> Sprawdz(Sprzet sprzet)
    {
        var bledy = new List<string>();

        if (string.IsNullOrWhiteSpace(sprzet.Nazwa))
        {
            bledy.Add("Nazwa nie może być pusta.");
        }
        if (string.IsNullOrWhiteSpace(sprzet.Kategoria))
        {
            bledy.Add("Podaj kategorię sprzętu.");
        }
        if (sprzet.CenaZaDzien <= 0)
        {
            bledy.Add("Cena za dzień musi być liczbą większą od zera.");
        }

        return bledy;
    }
}

Baza/

BazaDanych.cs

Cały SQL w jednym miejscu, wszystkie wartości przez parametry.

Baza/BazaDanych.cscsharp
using System.Collections.Generic;
using Microsoft.Data.Sqlite;
using Wypozyczalnia.Modele;

namespace Wypozyczalnia.Baza;

public class BazaDanych
{
    private const string ConnectionString = "Data Source=wypozyczalnia.db";

    public BazaDanych()
    {
        using var polaczenie = new SqliteConnection(ConnectionString);
        polaczenie.Open();

        var polecenie = polaczenie.CreateCommand();
        polecenie.CommandText = @"
            CREATE TABLE IF NOT EXISTS Sprzet (
                Id INTEGER PRIMARY KEY AUTOINCREMENT,
                Nazwa TEXT NOT NULL,
                Kategoria TEXT NOT NULL,
                CenaZaDzien REAL NOT NULL,
                Status TEXT NOT NULL DEFAULT 'Dostępny'
            )";
        polecenie.ExecuteNonQuery();
    }

    public void Dodaj(Sprzet sprzet)
    {
        using var polaczenie = new SqliteConnection(ConnectionString);
        polaczenie.Open();

        var polecenie = polaczenie.CreateCommand();
        polecenie.CommandText = "INSERT INTO Sprzet (Nazwa, Kategoria, CenaZaDzien, Status) VALUES ($nazwa, $kategoria, $cena, $status)";
        polecenie.Parameters.AddWithValue("$nazwa", sprzet.Nazwa);
        polecenie.Parameters.AddWithValue("$kategoria", sprzet.Kategoria);
        polecenie.Parameters.AddWithValue("$cena", sprzet.CenaZaDzien);
        polecenie.Parameters.AddWithValue("$status", sprzet.Status);
        polecenie.ExecuteNonQuery();
    }

    // Pusta fraza zwraca wszystko, a niepusta filtruje po nazwie lub kategorii
    public List<Sprzet> Szukaj(string fraza)
    {
        var wynik = new List<Sprzet>();
        using var polaczenie = new SqliteConnection(ConnectionString);
        polaczenie.Open();

        var polecenie = polaczenie.CreateCommand();
        polecenie.CommandText = @"
            SELECT Id, Nazwa, Kategoria, CenaZaDzien, Status
            FROM Sprzet
            WHERE Nazwa LIKE $fraza OR Kategoria LIKE $fraza
            ORDER BY Nazwa";
        polecenie.Parameters.AddWithValue("$fraza", "%" + fraza + "%");

        using var czytnik = polecenie.ExecuteReader();
        while (czytnik.Read())
        {
            wynik.Add(new Sprzet
            {
                Id = czytnik.GetInt32(0),
                Nazwa = czytnik.GetString(1),
                Kategoria = czytnik.GetString(2),
                CenaZaDzien = (decimal)czytnik.GetDouble(3),
                Status = czytnik.GetString(4)
            });
        }
        return wynik;
    }

    public void ZmienStatus(int id, string status)
    {
        using var polaczenie = new SqliteConnection(ConnectionString);
        polaczenie.Open();

        var polecenie = polaczenie.CreateCommand();
        polecenie.CommandText = "UPDATE Sprzet SET Status = $status WHERE Id = $id";
        polecenie.Parameters.AddWithValue("$status", status);
        polecenie.Parameters.AddWithValue("$id", id);
        polecenie.ExecuteNonQuery();
    }
}
  1. WHERE Nazwa LIKE $fraza OR Kategoria LIKE $fraza Jedno zapytanie szuka po nazwie lub kategorii. Pusta fraza (%%) zwraca wszystko, więc osobna metoda „pokaż wszystko” nie jest potrzebna.
  2. UPDATE Sprzet SET Status = $status WHERE Id = $id Zmiana statusu bez usuwania rekordu.
  3. ORDER BY Nazwa Stała kolejność listy, żeby po odświeżeniu pozycje nie „skakały”.

Widok i klej

MainWindow.xaml.cs

Cały kod okna to kilka krótkich metod. Każda robi jedną rzecz i deleguje resztę do walidatora lub bazy.

MainWindow.xaml.cscsharp
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using Wypozyczalnia.Baza;
using Wypozyczalnia.Modele;
using Wypozyczalnia.Walidacja;

namespace Wypozyczalnia;

public partial class MainWindow : Window
{
    private readonly BazaDanych _baza = new BazaDanych();
    private readonly WalidatorSprzetu _walidator = new WalidatorSprzetu();
    public ObservableCollection<Sprzet> Lista { get; } = new();

    public MainWindow()
    {
        InitializeComponent();
        TabelaSprzetu.ItemsSource = Lista;
        Odswiez();
    }

    // Jedno miejsce, które pobiera dane z bazy (z uwzględnieniem frazy) i odświeża tabelę
    private void Odswiez()
    {
        Lista.Clear();
        foreach (var s in _baza.Szukaj(PoleSzukaj.Text))
        {
            Lista.Add(s);
        }
    }

    private void PoleSzukaj_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (TabelaSprzetu != null)
        {
            Odswiez();
        }
    }

    private void PrzyciskDodaj_Click(object sender, RoutedEventArgs e)
    {
        var sprzet = new Sprzet
        {
            Nazwa = PoleNazwa.Text.Trim(),
            Kategoria = PoleKategoria.Text.Trim(),
            CenaZaDzien = decimal.TryParse(PoleCena.Text, out var cena) ? cena : 0
        };

        List<string> bledy = _walidator.Sprawdz(sprzet);
        if (bledy.Count > 0)
        {
            EtykietaBledy.Text = string.Join("\n", bledy);
            return;
        }

        _baza.Dodaj(sprzet);
        EtykietaBledy.Text = "";
        PoleNazwa.Clear();
        PoleKategoria.Clear();
        PoleCena.Clear();
        Odswiez();
    }

    private void PrzyciskWypozycz_Click(object sender, RoutedEventArgs e)
    {
        ZmienStatusZaznaczonego("Dostępny", "Wypożyczony", "wypożyczyć");
    }

    private void PrzyciskZwroc_Click(object sender, RoutedEventArgs e)
    {
        ZmienStatusZaznaczonego("Wypożyczony", "Dostępny", "przyjąć zwrot");
    }

    private void ZmienStatusZaznaczonego(string wymaganyStatus, string nowyStatus, string czynnosc)
    {
        if (TabelaSprzetu.SelectedItem is not Sprzet wybrany)
        {
            MessageBox.Show("Zaznacz najpierw sprzęt na liście.", "Brak zaznaczenia", MessageBoxButton.OK, MessageBoxImage.Information);
            return;
        }

        if (wybrany.Status != wymaganyStatus)
        {
            MessageBox.Show($"Sprzęt „{wybrany.Nazwa}” ma status „{wybrany.Status}”, więc nie można go teraz {czynnosc}.", "Niedozwolona operacja", MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }

        var odpowiedz = MessageBox.Show($"Czy chcesz {czynnosc}: {wybrany.Nazwa}?", "Potwierdzenie", MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (odpowiedz == MessageBoxResult.Yes)
        {
            _baza.ZmienStatus(wybrany.Id, nowyStatus);
            Odswiez();
        }
    }
}
  1. Odswiez() Jedno miejsce, które czyta z bazy z uwzględnieniem frazy wyszukiwania. Wołane po każdej zmianie.
  2. PoleSzukaj_TextChanged Wyszukiwanie na bieżąco. Sprawdzenie TabelaSprzetu != null chroni przed zdarzeniem w trakcie budowania okna.
  3. _walidator.Sprawdz(sprzet) Przy błędach pokazujemy je wszystkie naraz i kończymy metodę przez return.
  4. ZmienStatusZaznaczonego(...) Wspólna metoda dla „Wypożycz” i „Zwróć”: sprawdza zaznaczenie, dozwolony status i pyta o potwierdzenie.
  5. is not Sprzet wybrany Odwrócony wzorzec is: jeśli nic nie zaznaczono, przerywamy z komunikatem.

6 · Czego unikać

Trzy błędy w dużym zadaniu

Wszystko w code-behind
Tak nie
private void PrzyciskDodaj_Click(...)
{
    using var polaczenie = new SqliteConnection(...);
    polaczenie.Open();
    // SQL, walidacja i odświeżanie w jednej metodzie
}

Kilkaset linii w jednym pliku, trudnych do poprawienia. Każda zmiana ryzykuje zepsucie czegoś innego.

Tak
var bledy = _walidator.Sprawdz(sprzet);
if (bledy.Count > 0) { /* pokaż */ return; }
_baza.Dodaj(sprzet);
Odswiez();

Okno odczytuje, woła walidator i bazę oraz odświeża widok. Reszta jest w osobnych klasach.

Pominięta walidacja
Tak nie
„Wiem, co wpiszę podczas testów”

Prawdziwy użytkownik (i egzaminator) wpisze puste pole, ujemną cenę lub litery zamiast liczby, a program się zawiesi.

Tak
Test: puste pola, cena 0, cena „abc”, bardzo długa nazwa

Walidacja i TryParse kosztują kilka linii, a chronią przed awarią.

Brak sprawdzenia statusu przy wypożyczaniu
Tak nie
_baza.ZmienStatus(wybrany.Id, "Wypożyczony");   // bez sprawdzenia

Można „wypożyczyć” sprzęt, który już jest wypożyczony.

Tak
if (wybrany.Status != "Dostępny") { /* komunikat */ return; }

Reguły biznesowe (kto może co) sprawdzaj przed zmianą danych.

7 · Sprawdź się

Do której części projektu to należy?

Cztery elementy projektu. Wskaż, gdzie je napiszesz.

Ta interakcja wymaga włączonego JavaScriptu.

8 · Przed oddaniem

Sprawdź swój projekt jak egzaminator

Wykonaj ten scenariusz na gotowym programie. Każdy punkt to sytuacja, która potrafi zaskoczyć.

Podsumowanie

Co warto zapamiętać

  1. Zadanie egzaminacyjne zaczynasz od planu: układ okna, pola modelu, metody bazy.
  2. Struktura: Modele, Walidacja, Baza i cienkie okno.
  3. Okno odczytuje dane, woła walidator i bazę oraz odświeża tabelę jedną metodą Odswiez().
  4. Wszystkie wartości do SQL przekazuj parametrami, a liczby z pól zamieniaj przez TryParse.
  5. Przed oddaniem przejdź cały scenariusz testowy, także dla pustej bazy i błędnych danych.

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ę →
Źródła i dalsza nauka