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.
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.
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();
}
}
"Data Source=sklep.db"Łańcuch połączenia z plikiem bazy. Jeśli plik nie istnieje, SQLite go utworzy.CREATE TABLE IF NOT EXISTSTworzy tabelę tylko wtedy, gdy jej nie ma, więc konstruktor można wywoływać przy każdym uruchomieniu bez utraty danych.using var polaczenie = ...Gwarantuje zamknięcie połączenia po użyciu, nawet gdy wystąpi błąd.polaczenie.Open()Bez tego zapytanie zakończy się wyjątkiem o zamkniętym połączeniu.Parameters.AddWithValue("$nazwa", nazwa)Wartość trafia do bazy jako dane, nie jako fragment SQL.ExecuteNonQuery()iExecuteReader()Pierwszy dla poleceń, które nic nie zwracają (INSERT,DELETE). Drugi dlaSELECT, który zwraca wiersze.while (czytnik.Read())Przechodzi po kolejnych wierszach wyniku.GetInt32(0)iGetString(1)czytają kolumny po numerze zSELECT."%" + 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.
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()}";
ExecuteScalar()Zwraca pierwszą komórkę pierwszego wiersza jakoobject. Zamieniasz ją na liczbę przezConvert.ToInt32.- Pusty fragment w
LIKEDla"%" + "" + "%"wzorzec to%%i pasują wszystkie wiersze, więc pusta fraza pokazuje całą listę. - Apostrof we frazie Parametr
$fragmentprzekazuje wartość osobno od zapytania, więcO'Briennie 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.
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());
}
}
}
private readonly BazaDanych _bazaJeden obiekt na okno. Jego konstruktor tworzy tabelę przy pierwszym uruchomieniu.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.decimal.TryParseBezpieczna zamiana ceny. Gdy się nie uda, nie wywołujemy bazy.SelectedItem is Produkt wybranyOdczyt zaznaczonego wiersza i jegoIddo usunięcia.
6 · Wygląd
MainWindow.xaml
Pasek wyszukiwania u góry, tabela w środku i formularz dodawania u dołu.
<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>
DockPanel.Dock="Top"iBottomPasek 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
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).
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()`
using var polaczenie = new SqliteConnection(ConnectionString);
var polecenie = polaczenie.CreateCommand();
polecenie.ExecuteNonQuery();Wyjątek informujący, że połączenie jest zamknięte.
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
_baza.DodajProdukt(PoleNazwa.Text, cena);
// tabela na ekranie bez zmianRekord jest w bazie, ale lista na ekranie go nie pokazuje.
_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
using Microsoft.Data.Sqlite; // podkreślone na czerwonoNie można znaleźć typu lub przestrzeni nazw SqliteConnection.
Menedżer pakietów NuGet → zainstaluj Microsoft.Data.Sqlite w projekcie z kodemTypy z pakietu są dostępne dopiero po jego instalacji.
8 · Sprawdź się
Którą metodę wywołasz?
Cztery sytuacje z bazą danych.
Podsumowanie
Co warto zapamiętać
- Łańcuch: **XAML → code-behind → klasa bazy → plik
.db**. Code-behind nie zna SQL. - Pakiet **
Microsoft.Data.Sqlite** dajeSqliteConnectioniSqliteCommand. ZawszeOpen()przed zapytaniem. CREATE TABLE IF NOT EXISTSbezpiecznie tworzy tabelę przy każdym starcie.ExecuteNonQuery()dlaINSERT,UPDATE,DELETE, aExecuteReader()dlaSELECT.- 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ę →