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
| Wymaganie | Co to znaczy w praktyce |
|---|---|
| Lista sprzętu | DataGrid 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życzanie | Zaznaczenie sprzętu i „Wypożycz”: status się zmienia po potwierdzeniu w MessageBox. |
| Zwrot | Analogicznie, status wraca na „Dostępny”. |
| Trwałe dane | Baza SQLite (albo JSON, jeśli wolisz prościej). Dane przetrwają zamknięcie programu. |
| Wyszukiwanie | Po nazwie lub kategorii, najlepiej na bieżąco podczas pisania. |
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.
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”.
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.
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.
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();
}
}
WHERE Nazwa LIKE $fraza OR Kategoria LIKE $frazaJedno zapytanie szuka po nazwie lub kategorii. Pusta fraza (%%) zwraca wszystko, więc osobna metoda „pokaż wszystko” nie jest potrzebna.UPDATE Sprzet SET Status = $status WHERE Id = $idZmiana statusu bez usuwania rekordu.ORDER BY NazwaStał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.
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();
}
}
}
Odswiez()Jedno miejsce, które czyta z bazy z uwzględnieniem frazy wyszukiwania. Wołane po każdej zmianie.PoleSzukaj_TextChangedWyszukiwanie na bieżąco. SprawdzenieTabelaSprzetu != nullchroni przed zdarzeniem w trakcie budowania okna._walidator.Sprawdz(sprzet)Przy błędach pokazujemy je wszystkie naraz i kończymy metodę przezreturn.ZmienStatusZaznaczonego(...)Wspólna metoda dla „Wypożycz” i „Zwróć”: sprawdza zaznaczenie, dozwolony status i pyta o potwierdzenie.is not Sprzet wybranyOdwrócony wzorzecis: jeśli nic nie zaznaczono, przerywamy z komunikatem.
6 · Czego unikać
Trzy błędy w dużym zadaniu
Wszystko w code-behind
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.
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
„Wiem, co wpiszę podczas testów”Prawdziwy użytkownik (i egzaminator) wpisze puste pole, ujemną cenę lub litery zamiast liczby, a program się zawiesi.
Test: puste pola, cena 0, cena „abc”, bardzo długa nazwaWalidacja i TryParse kosztują kilka linii, a chronią przed awarią.
Brak sprawdzenia statusu przy wypożyczaniu
_baza.ZmienStatus(wybrany.Id, "Wypożyczony"); // bez sprawdzeniaMożna „wypożyczyć” sprzęt, który już jest wypożyczony.
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.
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ć
- Zadanie egzaminacyjne zaczynasz od planu: układ okna, pola modelu, metody bazy.
- Struktura: Modele, Walidacja, Baza i cienkie okno.
- Okno odczytuje dane, woła walidator i bazę oraz odświeża tabelę jedną metodą
Odswiez(). - Wszystkie wartości do SQL przekazuj parametrami, a liczby z pól zamieniaj przez
TryParse. - 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ę →