Dataczwartek, 13 sierpnia 2026 Czas17:42:33
← Aplikacje desktopowe (WPF)

Lekcja 16. Tworzenie aplikacji WPF z bazą danych SQLite

Trudny / egzaminacyjny

Po 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

  1. Dodaj metodę PolicztProdukty() zwracającą liczbę produktów w bazie (zapytanie SELECT COUNT(*) FROM Produkty) i wyświetl ją w etykiecie na dole okna.

  2. Dodaj metodę ZnajdzPoNazwie(string fragment) wyszukującą produkty, których nazwa zawiera podany fragment tekstu (SQL WHERE Nazwa LIKE $fragment, gdzie parametr to "%" + fragment + "%"), i podłącz ją pod pole wyszukiwania z przyciskiem "Szukaj".

  3. 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 przez MessageBox (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.