Lekcja 16. Tworzenie aplikacji WPF z bazą danych SQLite
Trudny / egzaminacyjnyPo co się tego uczymy?
Zapis do pliku JSON z poprzedniej lekcji działa dobrze dla prostych list, ale prawdziwe aplikacje biznesowe (magazyn, system rezerwacji, ewidencja klientów) potrzebują prawdziwej bazy danych — z możliwością wyszukiwania, filtrowania i pracy na dużych ilościach danych. SQLite to najwygodniejszy wybór na egzamin INF.04: działa bez internetu, nie wymaga instalowania serwera, a cała baza to jeden plik .db.
Teoria
Warto zrozumieć cały "łańcuch" połączeń, zanim napiszesz kod: [UI – XAML] → [Logika okna – XAML.cs] → [Logika bazy – BazaDanych.cs] → [plik .db]. Każda część ma jasno określoną rolę:
- XAML — tylko wygląd: pola tekstowe, przyciski, tabela. Nic nie liczy, tylko pokazuje;
- XAML.cs (code-behind) — logika zachowania przycisków: klik "Dodaj" wysyła dane do bazy, klik "Usuń" usuwa rekord, po każdej operacji odświeża widok. Code-behind NIE zna SQL-a — woła tylko metody z osobnej klasy bazy danych (nawiązanie do zasady oddzielenia logiki od UI!);
- BazaDanych.cs — tu jest prawdziwa praca: połączenie z plikiem .db, tworzenie tabeli (jeśli jeszcze nie istnieje), wykonywanie zapytań SQL (SELECT/INSERT/UPDATE/DELETE);
- plik .db — zwykły plik na dysku przechowujący wszystkie dane.
Podstawowe pojęcia (znane już z działu Bazy danych i SQL w INF.03, tutaj w kontekście C#): baza danych to plik z zapisanymi informacjami, tabela to podział danych na grupy (np. Produkty, Klienci), rekord to jeden wiersz tabeli (np. jeden produkt), pole/kolumna to pojedyncza cecha rekordu (nazwa, cena, id), a PRIMARY KEY to unikalny identyfikator każdego rekordu.
Do połączenia C# z SQLite potrzebny jest pakiet NuGet Microsoft.Data.Sqlite (Narzędzia → Menedżer pakietów NuGet → wyszukaj i zainstaluj). Komunikacja z bazą odbywa się przez obiekt SqliteConnection (połączenie) i SqliteCommand (pojedyncze zapytanie SQL).
Schemat
Architektura aplikacji WPF + SQLite:
[MainWindow.xaml] [MainWindow.xaml.cs] [BazaDanych.cs]
UI (wyglad) --> obsluga przyciskow --> SQL (SELECT/
przyciski, (klej UI <-> baza), INSERT/UPDATE/
tabela, pola NIE zna SQL DELETE)
│
▼
[sklep.db]
(plik na dysku)
Przykład z życia
System kasowy w małym sklepie: lista produktów pokazywana w DataGrid jest ładowana z pliku sklep.db przy starcie programu, dodanie nowego produktu przez formularz wykonuje zapytanie INSERT do tej samej bazy, a zamknięcie i ponowne otwarcie programu (nawet następnego dnia) pokaże dokładnie te same dane — bo są trwale zapisane na dysku, nie tylko w pamięci programu.
Kod (C#)
// BazaDanych.cs - cala logika SQL w jednym miejscu
using Microsoft.Data.Sqlite;
using System.Collections.Generic;
namespace SklepApp
{
public class Produkt
{
public int Id { get; set; }
public string Nazwa { get; set; } = "";
public decimal Cena { get; set; }
}
public class BazaDanych
{
private const string ConnectionString = "Data Source=sklep.db";
public BazaDanych()
{
using var polaczenie = new SqliteConnection(ConnectionString);
polaczenie.Open();
var utworzTabele = polaczenie.CreateCommand();
utworzTabele.CommandText = @"
CREATE TABLE IF NOT EXISTS Produkty (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Nazwa TEXT NOT NULL,
Cena REAL NOT NULL
)";
utworzTabele.ExecuteNonQuery();
}
public void DodajProdukt(string nazwa, decimal cena)
{
using var polaczenie = new SqliteConnection(ConnectionString);
polaczenie.Open();
var polecenie = polaczenie.CreateCommand();
polecenie.CommandText = "INSERT INTO Produkty (Nazwa, Cena) VALUES ($nazwa, $cena)";
polecenie.Parameters.AddWithValue("$nazwa", nazwa);
polecenie.Parameters.AddWithValue("$cena", cena);
polecenie.ExecuteNonQuery();
}
public List<Produkt> PobierzWszystkie()
{
var wynik = new List<Produkt>();
using var polaczenie = new SqliteConnection(ConnectionString);
polaczenie.Open();
var polecenie = polaczenie.CreateCommand();
polecenie.CommandText = "SELECT Id, Nazwa, Cena FROM Produkty";
using var czytnik = polecenie.ExecuteReader();
while (czytnik.Read())
{
wynik.Add(new Produkt
{
Id = czytnik.GetInt32(),
Nazwa = czytnik.GetString(1),
Cena = (decimal)czytnik.GetDouble(2)
});
}
return wynik;
}
public void UsunProdukt(int id)
{
using var polaczenie = new SqliteConnection(ConnectionString);
polaczenie.Open();
var polecenie = polaczenie.CreateCommand();
polecenie.CommandText = "DELETE FROM Produkty WHERE Id = $id";
polecenie.Parameters.AddWithValue("$id", id);
polecenie.ExecuteNonQuery();
}
}
}
Komentarz i wyjaśnienie kodu
Konstruktor BazaDanych() wykonuje się raz przy tworzeniu obiektu i uruchamia CREATE TABLE IF NOT EXISTS — to zapytanie tworzy tabelę Produkty TYLKO jeśli jeszcze nie istnieje, więc bezpiecznie można je wywoływać przy każdym uruchomieniu programu, nie kasując istniejących danych.
DodajProdukt, PobierzWszystkie i UsunProdukt to trzy osobne metody, każda otwierająca własne połączenie (using var polaczenie = ... — słowo kluczowe using gwarantuje zamknięcie połączenia zaraz po użyciu, nawet jeśli wystąpi błąd). Zwróć uwagę na parametry zapytania ($nazwa, $cena, $id zamiast wklejania wartości bezpośrednio do tekstu SQL) — to zabezpieczenie przed atakiem SQL injection, dokładnie tak samo ważne w C#, jak było w dziale PHP i SQL z INF.03.
PobierzWszystkie używa ExecuteReader() zamiast ExecuteNonQuery(), bo SELECT zwraca dane do odczytania — pętla while (czytnik.Read()) przechodzi przez każdy wiersz wyniku, a czytnik.GetInt32(0)/GetString(1) odczytują kolejne kolumny po ich numerze (0 = pierwsza kolumna w zapytaniu SELECT).
Ćwiczenie samodzielne
Zainstaluj pakiet NuGet Microsoft.Data.Sqlite w nowym projekcie WPF, wklej klasę BazaDanych z przykładu, i w code-behind okna wywołaj PobierzWszystkie() przy starcie, wypełniając nią ObservableCollection połączoną z DataGrid (dokładnie jak w lekcji o wyświetlaniu list danych).
Zadania do pracy własnej
Dodaj metodę
PolicztProdukty()zwracającą liczbę produktów w bazie (zapytanieSELECT COUNT(*) FROM Produkty) i wyświetl ją w etykiecie na dole okna.Dodaj metodę
ZnajdzPoNazwie(string fragment)wyszukującą produkty, których nazwa zawiera podany fragment tekstu (SQLWHERE Nazwa LIKE $fragment, gdzie parametr to"%" + fragment + "%"), i podłącz ją pod pole wyszukiwania z przyciskiem "Szukaj".Zbuduj kompletną aplikację "Magazyn" z tabelą Produkty (Nazwa, Cena, IloscNaStanie) i pełnym CRUD-em: dodawanie, edycja istniejącego rekordu (formularz wypełniony danymi zaznaczonego produktu po kliknięciu "Edytuj", zapis przez
UPDATE), usuwanie z potwierdzeniem przezMessageBox(nawiązanie do lekcji o dialogach), i wyszukiwanie po nazwie.
Typowe błędy
Wklejanie wartości bezpośrednio do tekstu zapytania SQL (np. "INSERT INTO Produkty VALUES ('" + nazwa + "')") zamiast użycia parametrów — to poważna luka bezpieczeństwa (SQL injection) i częsta przyczyna błędów, gdy nazwa zawiera apostrof.
Nieotwieranie połączenia przed wykonaniem zapytania (brak polaczenie.Open()) — spowoduje wyjątek informujący, że połączenie jest zamknięte.
Zapominanie o odświeżeniu widoku listy po operacji na bazie — dodanie rekordu do bazy SQL nie odświeży automatycznie DataGrid na ekranie; trzeba ręcznie ponownie pobrać dane (PobierzWszystkie()) i zaktualizować kolekcję połączoną z widokiem.
Nawiązanie do egzaminu zawodowego
To połączenie INF.04.5 (aplikacje desktopowe) z INF.04.3.2 (struktury danych) i wiedzą SQL z działu Bazy danych INF.03 — w praktyce niemal każde duże zadanie egzaminacyjne z aplikacją desktopową wymaga właśnie takiego połączenia WPF z bazą danych.