Aplikacje desktopowe · Baza danych

Aplikacja WPF z bazą SQLite

Zapis do JSON działa dla prostych list, ale prawdziwe aplikacje biznesowe (magazyn, rezerwacje, ewidencja) potrzebują bazy danych. SQLite nie wymaga serwera, a cała baza to jeden plik .db, więc jest idealna na egzamin. Zbudujesz aplikację z pełnym zestawem operacji: dodawanie, odczyt, szukanie i usuwanie.

  • około 60 minut
  • WPF · SQLite · Microsoft.Data.Sqlite
  • INF.04.5.3

Po tej lekcji: połączyć się z bazą SQLite z C#, utworzyć tabelę, wykonać INSERT, SELECT i DELETE z parametrami oraz odświeżyć widok po operacji na bazie.

Zacznijmy od kasy sklepowej

Dane, które przeżyją następny dzień

System kasowy w małym sklepie ładuje listę produktów z pliku sklep.db przy starcie. Dodanie produktu wykonuje INSERT do tej samej bazy, a po zamknięciu i ponownym uruchomieniu (nawet następnego dnia) dane są takie same, bo leżą na dysku, a nie tylko w pamięci programu.

To połączenie wiedzy z aplikacji desktopowych (WPF), struktur danych (kolekcje) i SQL z kwalifikacji INF.03. W praktyce niemal każde większe zadanie egzaminacyjne z aplikacją desktopową tego wymaga.

1 · Kto za co odpowiada

Cztery ogniwa aplikacji z bazą

Zanim napiszesz kod, poznaj drogę danych: **XAML → code-behind → BazaDanych.cs → plik .db**.

XAMLwygląd

Pola, przyciski i tabela. Niczego nie liczy, tylko pokazuje.

Na przykład DataGrid, TextBox

Code-behindzachowanie

Klik „Dodaj” wysyła dane do bazy i odświeża widok. Nie zna SQL, woła tylko metody klasy bazy.

Na przykład _baza.DodajProdukt(...)

BazaDanych.cscały SQL

Połączenie, tworzenie tabeli i zapytania SELECT, INSERT, UPDATE, DELETE w jednym miejscu.

Na przykład CREATE TABLE IF NOT EXISTS

Plik .dbdane

Zwykły plik na dysku z wszystkimi danymi. Nie wymaga serwera.

Na przykład sklep.db

2 · Przygotowanie projektu

Zainstaluj Microsoft.Data.Sqlite

Do rozmowy z SQLite potrzebujesz pakietu NuGet. Zaznaczaj kroki w trakcie.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

3 · Sprawdź w działaniu

Zobacz, jakie SQL wysyła Twój program

Dodaj produkt, usuń inny i wyszukaj fragment nazwy. Pod tabelą widzisz zapytanie SQL, które poszło do bazy. Potem przełącz tryb na „sklejanie tekstu” i spróbuj nazwy O'Brien oraz frazy wyszukiwania ' OR '1'='1.

Ta interakcja wymaga włączonego JavaScriptu.

Wartości od użytkownika zawsze przekazuj parametrami ($nazwa). Sklejanie tekstu zepsuje zapytanie przy apostrofie i otwiera drogę do ataku SQL injection.

4 · Cały SQL w jednym miejscu

BazaDanych.cs

Każda metoda otwiera własne połączenie, wykonuje jedno zapytanie i je zamyka. Wszystkie wartości wchodzą przez parametry.

BazaDanych.cscsharp
using System.Collections.Generic;
using Microsoft.Data.Sqlite;

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(0),
                Nazwa = czytnik.GetString(1),
                Cena = (decimal)czytnik.GetDouble(2)
            });
        }
        return wynik;
    }

    public List<Produkt> ZnajdzPoNazwie(string fragment)
    {
        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 WHERE Nazwa LIKE $fragment";
        polecenie.Parameters.AddWithValue("$fragment", "%" + fragment + "%");

        using var czytnik = polecenie.ExecuteReader();
        while (czytnik.Read())
        {
            wynik.Add(new Produkt
            {
                Id = czytnik.GetInt32(0),
                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();
    }
}
  1. "Data Source=sklep.db" Łańcuch połączenia z plikiem bazy. Jeśli plik nie istnieje, SQLite go utworzy.
  2. CREATE TABLE IF NOT EXISTS Tworzy tabelę tylko wtedy, gdy jej nie ma, więc konstruktor można wywoływać przy każdym uruchomieniu bez utraty danych.
  3. using var polaczenie = ... Gwarantuje zamknięcie połączenia po użyciu, nawet gdy wystąpi błąd.
  4. polaczenie.Open() Bez tego zapytanie zakończy się wyjątkiem o zamkniętym połączeniu.
  5. Parameters.AddWithValue("$nazwa", nazwa) Wartość trafia do bazy jako dane, nie jako fragment SQL.
  6. ExecuteNonQuery() i ExecuteReader() Pierwszy dla poleceń, które nic nie zwracają (INSERT, DELETE). Drugi dla SELECT, który zwraca wiersze.
  7. while (czytnik.Read()) Przechodzi po kolejnych wierszach wyniku. GetInt32(0) i GetString(1) czytają kolumny po numerze z SELECT.
  8. "%" + fragment + "%" Znaki % dopisujemy do wartości parametru, a nie do tekstu zapytania.

Do zadań

Zapytanie, które zwraca jedną liczbę

Dotąd czytałeś wiersze przez ExecuteReader. Gdy zapytanie zwraca jedną wartość (jak COUNT(*)), użyj ExecuteScalar(). Dopisz metodę do klasy BazaDanych.

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

    var polecenie = polaczenie.CreateCommand();
    polecenie.CommandText = "SELECT COUNT(*) FROM Produkty";

    return Convert.ToInt32(polecenie.ExecuteScalar());   // pierwsza komórka wyniku
}

// w oknie, po każdej zmianie (dodaj, usuń, edytuj):
EtykietaLicznik.Text = $"Produktów w bazie: {baza.PoliczProdukty()}";
  1. ExecuteScalar() Zwraca pierwszą komórkę pierwszego wiersza jako object. Zamieniasz ją na liczbę przez Convert.ToInt32.
  2. Pusty fragment w LIKE Dla "%" + "" + "%" wzorzec to %% i pasują wszystkie wiersze, więc pusta fraza pokazuje całą listę.
  3. Apostrof we frazie Parametr $fragment przekazuje wartość osobno od zapytania, więc O'Brien nie psuje SQL.

5 · Klej

MainWindow.xaml.cs

Okno nie zna SQL. Woła metody klasy BazaDanych i po każdej zmianie pobiera dane na nowo.

MainWindow.xaml.cscsharp
using System.Collections.ObjectModel;
using System.Windows;

namespace SklepApp;

public partial class MainWindow : Window
{
    private readonly BazaDanych _baza = new BazaDanych();
    public ObservableCollection<Produkt> Produkty { get; } = new();

    public MainWindow()
    {
        InitializeComponent();
        TabelaProdukty.ItemsSource = Produkty;
        Odswiez(_baza.PobierzWszystkie());
    }

    // Po każdej operacji na bazie pobieramy dane ponownie i wypełniamy kolekcję
    private void Odswiez(System.Collections.Generic.IEnumerable<Produkt> dane)
    {
        Produkty.Clear();
        foreach (var p in dane)
        {
            Produkty.Add(p);
        }
    }

    private void PrzyciskDodaj_Click(object sender, RoutedEventArgs e)
    {
        if (decimal.TryParse(PoleCena.Text, out var cena))
        {
            _baza.DodajProdukt(PoleNazwa.Text, cena);
            Odswiez(_baza.PobierzWszystkie());
        }
    }

    private void PrzyciskSzukaj_Click(object sender, RoutedEventArgs e)
    {
        Odswiez(_baza.ZnajdzPoNazwie(PoleSzukaj.Text));
    }

    private void PrzyciskUsun_Click(object sender, RoutedEventArgs e)
    {
        if (TabelaProdukty.SelectedItem is Produkt wybrany)
        {
            _baza.UsunProdukt(wybrany.Id);
            Odswiez(_baza.PobierzWszystkie());
        }
    }
}
  1. private readonly BazaDanych _baza Jeden obiekt na okno. Jego konstruktor tworzy tabelę przy pierwszym uruchomieniu.
  2. Odswiez(...) Czyści kolekcję i wypełnia ją nowymi danymi. Dodanie rekordu do bazy nie odświeża ekranu samo, bo kolekcja o tym nie wie.
  3. decimal.TryParse Bezpieczna zamiana ceny. Gdy się nie uda, nie wywołujemy bazy.
  4. SelectedItem is Produkt wybrany Odczyt zaznaczonego wiersza i jego Id do usunięcia.

6 · Wygląd

MainWindow.xaml

Pasek wyszukiwania u góry, tabela w środku i formularz dodawania u dołu.

MainWindow.xamlxaml
<Window x:Class="SklepApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Sklep" Height="400" Width="480">
    <DockPanel Margin="15">
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,10">
            <TextBox x:Name="PoleSzukaj" Width="160" Margin="0,0,8,0" />
            <Button Content="Szukaj" Click="PrzyciskSzukaj_Click" />
        </StackPanel>
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,10,0,0">
            <TextBox x:Name="PoleNazwa" Width="140" Margin="0,0,8,0" />
            <TextBox x:Name="PoleCena" Width="70" Margin="0,0,8,0" />
            <Button Content="Dodaj" Click="PrzyciskDodaj_Click" Margin="0,0,8,0" />
            <Button Content="Usuń" Click="PrzyciskUsun_Click" />
        </StackPanel>
        <DataGrid x:Name="TabelaProdukty" AutoGenerateColumns="True" IsReadOnly="True" />
    </DockPanel>
</Window>
  1. DockPanel.Dock="Top" i Bottom Pasek wyszukiwania i formularz przypięte do krawędzi, tabela wypełnia resztę.

7 · Czego unikać

Cztery błędy z bazą danych

Wartości sklejone z tekstem SQL
Tak nie
polecenie.CommandText =
    "INSERT INTO Produkty (Nazwa, Cena) VALUES ('" + nazwa + "', " + cena + ")";

Nazwa z apostrofem (O'Brien) psuje zapytanie. Złośliwy tekst może zmienić jego znaczenie (SQL injection).

Tak
polecenie.CommandText = "INSERT INTO Produkty (Nazwa, Cena) VALUES ($nazwa, $cena)";
polecenie.Parameters.AddWithValue("$nazwa", nazwa);
polecenie.Parameters.AddWithValue("$cena", cena);

Parametry są zawsze danymi, a nie częścią SQL.

Brak `polaczenie.Open()`
Tak nie
using var polaczenie = new SqliteConnection(ConnectionString);
var polecenie = polaczenie.CreateCommand();
polecenie.ExecuteNonQuery();

Wyjątek informujący, że połączenie jest zamknięte.

Tak
using var polaczenie = new SqliteConnection(ConnectionString);
polaczenie.Open();
var polecenie = polaczenie.CreateCommand();
polecenie.ExecuteNonQuery();

Open() otwiera połączenie z plikiem bazy.

Brak odświeżenia po operacji na bazie
Tak nie
_baza.DodajProdukt(PoleNazwa.Text, cena);
// tabela na ekranie bez zmian

Rekord jest w bazie, ale lista na ekranie go nie pokazuje.

Tak
_baza.DodajProdukt(PoleNazwa.Text, cena);
Odswiez(_baza.PobierzWszystkie());

Kolekcja w pamięci nie wie, że zmieniła się baza. Pobierz dane ponownie.

Pakiet NuGet nie zainstalowany w projekcie
Tak nie
using Microsoft.Data.Sqlite;   // podkreślone na czerwono

Nie można znaleźć typu lub przestrzeni nazw SqliteConnection.

Tak
Menedżer pakietów NuGet → zainstaluj Microsoft.Data.Sqlite w projekcie z kodem

Typy z pakietu są dostępne dopiero po jego instalacji.

8 · Sprawdź się

Którą metodę wywołasz?

Cztery sytuacje z bazą danych.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Łańcuch: **XAML → code-behind → klasa bazy → plik .db**. Code-behind nie zna SQL.
  2. Pakiet **Microsoft.Data.Sqlite** daje SqliteConnection i SqliteCommand. Zawsze Open() przed zapytaniem.
  3. CREATE TABLE IF NOT EXISTS bezpiecznie tworzy tabelę przy każdym starcie.
  4. ExecuteNonQuery() dla INSERT, UPDATE, DELETE, a ExecuteReader() dla SELECT.
  5. Wartości zawsze parametrami ($nazwa), nigdy sklejane z tekstem. Po zmianie bazy odśwież widok.

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