Aplikacje mobilne · Projekt
Projekt: sklep z produktami i koszykiem (SQLite, dwie powiązane tabele)
Poprzednia lekcja pokazała SQLite na jednej tabeli. W prawdziwych aplikacjach dane są powiązane: produkt trafia do koszyka, zamówienie należy do klienta. Zbudujesz dwie powiązane tabele, połączysz je zapytaniem JOIN i wyliczysz sumę koszyka.
- około 90 minut
- .NET MAUI · SQLite · JOIN · parametry
- INF.04.6.2
Po tej lekcji: zbudować dwie tabele powiązane kluczem obcym, odczytać dane zapytaniem JOIN z wyliczoną kolumną, użyć parametrów zamiast sklejania SQL i odczytać pojedynczą wartość przez ExecuteScalarAsync.
Koszyk, którego suma wynika z danych
Cena produktu i liczba sztuk to dwie różne informacje. Zapiszemy je w powiązanych tabelach, a SQL połączy je i obliczy wartość koszyka.
Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.
Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji
2 · Sprawdź w działaniu
Jak koszyk „łączy się” z produktami
W tabeli Koszyk są tylko numery i ilości. Dodaj produkt do koszyka i zobacz, jak JOIN dopina nazwę, cenę i sumę. Potem zaznacz produkt użyty w koszyku i go usuń: wiersz koszyka zostaje, ale JOIN go pomija.
Najpierw przewidź, potem sprawdź: W modelu sklepu dodaj produkt, wybierz kilka sztuk i obserwuj sumę. Porównaj rekord produktu z pozycją koszyka. Sprawdź, co musi się stać z koszykiem po usunięciu produktu.
Tabela Koszyk trzyma odniesienie (produkt_id), a czytelną listę składa dopiero JOIN. Wyliczona kolumna suma nie istnieje w żadnej tabeli: liczy ją SQL. Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.
1. Przygotuj miejsce pracy
Co dodajesz, a co podmieniasz?
Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.
| Plik | Rola w ćwiczeniu |
|---|---|
| projekt.csproj (fragment) | Fragment w istniejącym pliku |
| Modele/Modele.cs | Logika i obsługa danych |
| Logika/BazaDanych.cs | Logika i obsługa danych |
| MainPage.xaml | Wygląd strony lub kontrolki |
| MainPage.xaml.cs | Logika i obsługa danych |
Zanim uruchomisz: trzy połączenia
x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.
x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.
Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.
2. Uruchom sam wygląd — jeszcze bez logiki
W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MojaAplikacja.MainPage">
<VerticalStackLayout Padding="24" Spacing="14">
<Label Text="Sklep i koszyk" FontSize="24" />
<Entry Placeholder="Nazwa produktu" />
<Entry Placeholder="Cena" Keyboard="Numeric" />
</VerticalStackLayout>
</ContentPage>
- Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.
Zacznijmy od osiedlowego sklepu
Lista produktów i koszyk z podliczaną sumą
Mała aplikacja sklepowa pozwala pracownikowi przeglądać i wyszukiwać produkty, a klientowi dodawać je do koszyka z sumą liczoną na bieżąco. To uproszczona, ale w pełni działająca wersja mechanizmu z prawdziwych sklepów.
W tej lekcji użyjesz drugiego podejścia do SQLite: pakietu Microsoft.Data.Sqlite (znanego z aplikacji desktopowych) zamiast sqlite-net-pcl. Zapytania SQL piszesz samodzielnie, z parametrami.
1 · Kto za co odpowiada
Cztery ogniwa aplikacji
XAMLwygląd
Entry, Button, CollectionView. Niczego nie liczy.
Na przykład MainPage.xaml
Code-behindkliknięcia
Obsługuje Dodaj, Usuń, Szukaj i Dodaj do koszyka. Nie zna SQL.
Na przykład MainPage.xaml.cs
BazaDanych.cscały SQL
Tworzy tabele i wykonuje zapytania z parametrami.
Na przykład PobierzKoszykAsync()
sklep.dbdane
Plik w FileSystem.AppDataDirectory.
Na przykład dwie tabele
3 · Klucz obcy
Dwie tabele i połączenie
| Tabela | Kolumny | Rola |
|---|---|---|
Produkty | id (klucz główny), nazwa, cena | Katalog produktów. |
Koszyk | id (klucz główny), produkt_id (klucz obcy), ilosc | Pozycje koszyka. Nie powtarza nazwy ani ceny. |
4 · Pakiety
Microsoft.Data.Sqlite i silnik SQLite
Zainstaluj dwa pakiety NuGet. Zaznaczaj kroki w trakcie.
3. Wskaż stronę startową ćwiczenia
Po dodaniu docelowych stron podmień App.xaml.cs na poniższy. App.xaml zostaje z szablonu. Od tej chwili startuje MainPage, a nie wcześniejszy tymczasowy MainPage z AppShell. Uzupełnij jej pliki w kolejnych etapach, zanim ponownie uruchomisz projekt.
namespace MojaAplikacja;
public partial class App : Application
{
public App() => InitializeComponent();
protected override Window CreateWindow(IActivationState? activationState)
=> new Window(new NavigationPage(new MainPage()));
}
CreateWindowTworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.new MainPage()Tworzy instancję docelowej strony. Aby testować drugi niezależny przykład, zatrzymaj program, zmień tylko ten typ i uruchom ponownie.
4. Połącz wygląd z działaniem
Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.
Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.
Budowa krok po kroku
Zależności w pliku projektu
To fragment do istniejącego projekt.csproj (fragment), nie cały plik. Zachowaj pozostałe ustawienia szablonu i dodaj podaną sekcję w odpowiednim miejscu.
Wersje kontrolne dla .NET10. Pakiet natywnego silnika jest przypięty jawnie, aby nie pozostać przy historycznej zależności2.x. Nie mieszaj różnych zestawów bundle w jednym projekcie.
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.13" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
</ItemGroup>
Budowa krok po kroku
Modele: Produkt i KoszykWidok
W jednym pliku są dwa zwykłe typy: Produkt i KoszykWidok. Drugi odbiera wynik JOIN z liczbą sztuk oraz wyliczoną sumą.
Produkt odpowiada tabeli. KoszykWidok to wynik JOIN, z wyliczoną sumą.
namespace MojaAplikacja.Modele;
public class Produkt
{
public int Id { get; set; }
public string Nazwa { get; set; } = "";
public double Cena { get; set; }
}
// „Widok” koszyka po JOIN: gotowy do wyświetlenia, z wyliczoną sumą
public class KoszykWidok
{
public int Id { get; set; }
public string Nazwa { get; set; } = "";
public double Cena { get; set; }
public int Ilosc { get; set; }
public double Suma { get; set; }
}
Budowa krok po kroku
BazaDanych.cs
Nie czytaj całego pliku jak jednego algorytmu. Najpierw CREATE TABLE i PobierzProduktyAsync, potem INSERT z parametrami, na końcu koszyk/JOIN i transakcja usuwania. Każda metoda ma inne zadanie.
Każda metoda otwiera połączenie, wykonuje jedno zapytanie z parametrami i je zamyka.
using Microsoft.Data.Sqlite;
using MojaAplikacja.Modele;
namespace MojaAplikacja.Logika;
public class BazaDanych
{
private readonly string _sciezkaBazy;
private string ConnectionString => $"Data Source={_sciezkaBazy}";
public BazaDanych()
{
SQLitePCL.Batteries_V2.Init(); // inicjalizacja silnika SQLite (Android i iOS)
_sciezkaBazy = Path.Combine(FileSystem.AppDataDirectory, "sklep.db");
UtworzTabeleJesliNieIstnieja();
}
private void UtworzTabeleJesliNieIstnieja()
{
using var con = new SqliteConnection(ConnectionString);
con.Open();
using var cmd1 = con.CreateCommand();
cmd1.CommandText = "CREATE TABLE IF NOT EXISTS Produkty (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, nazwa TEXT NOT NULL, cena REAL NOT NULL);";
cmd1.ExecuteNonQuery();
using var cmd2 = con.CreateCommand();
cmd2.CommandText = "CREATE TABLE IF NOT EXISTS Koszyk (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, produkt_id INTEGER NOT NULL, ilosc INTEGER NOT NULL);";
cmd2.ExecuteNonQuery();
}
// -------------------- PRODUKTY --------------------
public async Task<List<Produkt>> PobierzProduktyAsync(string filtr = "")
{
var lista = new List<Produkt>();
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
string sql = "SELECT id, nazwa, cena FROM Produkty";
if (!string.IsNullOrWhiteSpace(filtr)) sql += " WHERE nazwa LIKE @f";
await using var cmd = con.CreateCommand();
cmd.CommandText = sql;
if (!string.IsNullOrWhiteSpace(filtr))
cmd.Parameters.AddWithValue("@f", "%" + filtr + "%"); // parametr, NIE sklejanie napisów
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
lista.Add(new Produkt { Id = r.GetInt32(0), Nazwa = r.GetString(1), Cena = r.GetDouble(2) });
}
return lista;
}
public async Task DodajProduktAsync(string nazwa, double cena)
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "INSERT INTO Produkty(nazwa, cena) VALUES (@n, @c)";
cmd.Parameters.AddWithValue("@n", nazwa);
cmd.Parameters.AddWithValue("@c", cena);
await cmd.ExecuteNonQueryAsync();
}
public async Task UsunProduktAsync(int id)
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
using var transakcja = con.BeginTransaction();
using var pozycje = con.CreateCommand();
pozycje.Transaction = transakcja;
pozycje.CommandText = "DELETE FROM Koszyk WHERE produkt_id=@id";
pozycje.Parameters.AddWithValue("@id", id);
pozycje.ExecuteNonQuery();
using var produkt = con.CreateCommand();
produkt.Transaction = transakcja;
produkt.CommandText = "DELETE FROM Produkty WHERE id=@id";
produkt.Parameters.AddWithValue("@id", id);
produkt.ExecuteNonQuery();
transakcja.Commit();
}
// -------------------- KOSZYK --------------------
public async Task DodajDoKoszykaAsync(int produktId, int ilosc)
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "INSERT INTO Koszyk(produkt_id, ilosc) SELECT @pid, @ilosc WHERE @ilosc > 0 AND EXISTS (SELECT 1 FROM Produkty WHERE id=@pid)";
cmd.Parameters.AddWithValue("@pid", produktId);
cmd.Parameters.AddWithValue("@ilosc", ilosc);
if (await cmd.ExecuteNonQueryAsync() != 1)
throw new InvalidOperationException("Produkt nie istnieje lub ilość jest niepoprawna.");
}
public async Task<List<KoszykWidok>> PobierzKoszykAsync()
{
var lista = new List<KoszykWidok>();
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "SELECT Koszyk.id, Produkty.nazwa, Produkty.cena, Koszyk.ilosc, " +
"(Produkty.cena * Koszyk.ilosc) AS suma " +
"FROM Koszyk JOIN Produkty ON Koszyk.produkt_id = Produkty.id";
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
lista.Add(new KoszykWidok
{
Id = r.GetInt32(0), Nazwa = r.GetString(1), Cena = r.GetDouble(2),
Ilosc = r.GetInt32(3), Suma = r.GetDouble(4)
});
}
return lista;
}
// Zadania rozszerzające: wyczyszczenie, usunięcie pozycji i suma całego koszyka
public async Task WyczyscKoszykAsync()
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "DELETE FROM Koszyk";
await cmd.ExecuteNonQueryAsync();
}
public async Task UsunZKoszykaAsync(int koszykId)
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "DELETE FROM Koszyk WHERE id=@id";
cmd.Parameters.AddWithValue("@id", koszykId);
await cmd.ExecuteNonQueryAsync();
}
public async Task<double> SumaKoszykaAsync()
{
await using var con = new SqliteConnection(ConnectionString);
await con.OpenAsync();
await using var cmd = con.CreateCommand();
cmd.CommandText = "SELECT IFNULL(SUM(Produkty.cena * Koszyk.ilosc), 0) " +
"FROM Koszyk JOIN Produkty ON Koszyk.produkt_id = Produkty.id";
var wynik = await cmd.ExecuteScalarAsync(); // pojedyncza wartość zamiast czytnika
return Convert.ToDouble(wynik);
}
}
SQLitePCL.Batteries_V2.Init()Linia, o której łatwo zapomnieć. Bez niej na Androidzie i iOS zabraknie natywnej biblioteki SQLite.CREATE TABLE IF NOT EXISTSBezpieczne przy każdym starcie: nie kasuje istniejących danych.cmd.Parameters.AddWithValue("@f", "%" + filtr + "%")Fraza z%trafia do wartości parametru, a nie do tekstu zapytania.await using var conZamyka połączenie po użyciu, nawet gdy wystąpi błąd.r.GetInt32(0),r.GetString(1),r.GetDouble(2)Odczyt kolumn po **numerze w zapytaniuSELECT**, a nie po kolejności właściwości w klasie.FROM Koszyk JOIN Produkty ON ...Łączy tabele i wylicza(cena * ilosc) AS suma.ExecuteScalarAsync()Odczytuje pojedynczą wartość (suma koszyka), bez czytnika.BeginTransaction/cmd.Transaction/CommitUsunięcie pozycji koszyka i produktu to jedna operacja logiczna. Obie komendy muszą należeć do tej samej transakcji. Brak Commit przy błędzie oznacza wycofanie przy zwolnieniu transakcji.INSERT ... SELECT ... WHERE EXISTSWstawiamy pozycję tylko dla istniejącego produktu i dodatniej ilości. Brak wstawionego wiersza jest jawnym błędem, nie pozornym sukcesem.- SQLite i async Microsoft.Data.Sqlite nie zapewnia rzeczywistego asynchronicznego I/O SQLite; przykłady są małe. Dla dużych operacji zaprojektuj pracę poza wątkiemUI. Same nazwy Async nie gwarantują braku blokowania.
- Kwoty REAL/double to uproszczenie do ćwiczenia SQL. W systemie rozliczeniowym zaprojektuj kwoty np. jako całkowite grosze, unikając błędów zaokrągleń.
Budowa krok po kroku
MainPage.xaml
Dodaj lub otwórz MainPage.xaml i podmień jego zawartość. Najpierw odszukaj kontener, potem nazwane kontrolki oraz wskazane zdarzenia. Pozostaw zgodne x:Class i nazwę klasy w pliku .xaml.cs.
Pola produktu, wyszukiwanie, lista produktów i lista koszyka z sumą.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MojaAplikacja.Modele"
x:Class="MojaAplikacja.MainPage" Title="Produkty + Koszyk">
<ScrollView>
<VerticalStackLayout Padding="12" Spacing="12">
<Label Text="Dane produktu" FontAttributes="Bold" />
<Entry x:Name="txtNazwa" Placeholder="Nazwa" />
<Entry x:Name="txtCena" Placeholder="Cena" Keyboard="Numeric" />
<Entry x:Name="txtID" Placeholder="ID (edycja/usuwanie)" Keyboard="Numeric" />
<HorizontalStackLayout Spacing="10">
<Button Text="Dodaj" Clicked="btnDodaj_Clicked" />
<Button Text="Usuń" Clicked="btnUsun_Clicked" />
</HorizontalStackLayout>
<Label Text="Wyszukiwanie" FontAttributes="Bold" />
<Entry x:Name="txtSzukaj" Placeholder="Szukaj po nazwie..." />
<HorizontalStackLayout Spacing="10">
<Button Text="Szukaj" Clicked="btnSzukaj_Clicked" />
<Button Text="Odśwież" Clicked="btnOdswiez_Clicked" />
</HorizontalStackLayout>
<Label Text="Produkty (kliknij, żeby wypełnić pola)" FontAttributes="Bold" />
<CollectionView x:Name="listaProdukty" SelectionMode="Single"
SelectionChanged="listaProdukty_SelectionChanged">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Produkt">
<Grid Padding="8" ColumnDefinitions="60,*,90">
<Label Text="{Binding Id}" Grid.Column="0" />
<Label Text="{Binding Nazwa}" Grid.Column="1" />
<Label Text="{Binding Cena}" Grid.Column="2" HorizontalTextAlignment="End" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label Text="Koszyk" FontAttributes="Bold" />
<Entry x:Name="txtIlosc" Placeholder="Ilość (do koszyka)" Keyboard="Numeric" />
<Button Text="Dodaj do koszyka (ID z pola ID)" Clicked="btnDodajDoKoszyka_Clicked" />
<CollectionView x:Name="listaKoszyk">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:KoszykWidok">
<Grid Padding="8" ColumnDefinitions="60,*,70,60,90">
<Label Text="{Binding Id}" Grid.Column="0" />
<Label Text="{Binding Nazwa}" Grid.Column="1" />
<Label Text="{Binding Cena}" Grid.Column="2" HorizontalTextAlignment="End" />
<Label Text="{Binding Ilosc}" Grid.Column="3" HorizontalTextAlignment="End" />
<Label Text="{Binding Suma}" Grid.Column="4" HorizontalTextAlignment="End" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label x:Name="EtykietaSuma" FontAttributes="Bold" HorizontalOptions="End" />
<Button Text="Wyczyść koszyk" Clicked="btnWyczyscKoszyk_Clicked" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
SelectionChanged="listaProdukty_SelectionChanged"Kliknięcie produktu wypełnia pola formularza, więc przyciski wiedzą, o który rekord chodzi.Grid ColumnDefinitions="60,*,90"Skrócony zapis kolumn wiersza listy.x:DataTypeSkompilowane wiązania dlaProduktiKoszykWidok.
Budowa krok po kroku
MainPage.xaml.cs
Uruchom katalog: odświeżanie, dodanie i wyszukiwanie. Potem wybierz produkt, wpisz ilość i uruchom koszyk. Id produktu i Id pozycji koszyka są różnymi identyfikatorami.
Strona nie zna SQL. Waliduje dane, woła metody bazy i odświeża listy.
using MojaAplikacja.Logika;
using MojaAplikacja.Modele;
namespace MojaAplikacja;
public partial class MainPage : ContentPage
{
private readonly BazaDanych _baza = new BazaDanych();
public MainPage()
{
InitializeComponent();
}
private async Task OdswiezProduktyAsync(string filtr = "")
=> listaProdukty.ItemsSource = await _baza.PobierzProduktyAsync(filtr);
private async Task OdswiezKoszykAsync()
{
listaKoszyk.ItemsSource = await _baza.PobierzKoszykAsync();
EtykietaSuma.Text = $"Razem: {await _baza.SumaKoszykaAsync():F2} zł";
}
private async void btnDodaj_Clicked(object sender, EventArgs e)
{
try
{
if (string.IsNullOrWhiteSpace(txtNazwa.Text)) { await DisplayAlertAsync("Błąd", "Podaj nazwę!", "OK"); return; }
if (!double.TryParse(txtCena.Text, out double cena) || !double.IsFinite(cena) || cena < 0) { await DisplayAlertAsync("Błąd", "Cena musi być nieujemną, skończoną liczbą.", "OK"); return; }
await _baza.DodajProduktAsync(txtNazwa.Text.Trim(), cena);
await OdswiezProduktyAsync();
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
private async void btnUsun_Clicked(object sender, EventArgs e)
{
try
{
if (!int.TryParse(txtID.Text, out int id)) { await DisplayAlertAsync("Błąd", "Wybierz produkt z listy.", "OK"); return; }
await _baza.UsunProduktAsync(id);
await OdswiezProduktyAsync();
await OdswiezKoszykAsync();
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
private async void btnSzukaj_Clicked(object sender, EventArgs e)
{
try
{
await OdswiezProduktyAsync(txtSzukaj.Text ?? "");
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
private async void btnOdswiez_Clicked(object sender, EventArgs e)
{
try
{
txtSzukaj.Text = "";
await OdswiezProduktyAsync();
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
private void listaProdukty_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection?.FirstOrDefault() is Produkt p)
{
txtID.Text = p.Id.ToString();
txtNazwa.Text = p.Nazwa;
txtCena.Text = p.Cena.ToString();
}
}
private async void btnDodajDoKoszyka_Clicked(object sender, EventArgs e)
{
try
{
if (!int.TryParse(txtID.Text, out int produktId)) { await DisplayAlertAsync("Błąd", "Najpierw wybierz produkt.", "OK"); return; }
if (!int.TryParse(txtIlosc.Text, out int ilosc) || ilosc <= 0) { await DisplayAlertAsync("Błąd", "Podaj poprawną ilość (>0).", "OK"); return; }
await _baza.DodajDoKoszykaAsync(produktId, ilosc);
await OdswiezKoszykAsync();
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
private async void btnWyczyscKoszyk_Clicked(object sender, EventArgs e)
{
try
{
await _baza.WyczyscKoszykAsync();
await OdswiezKoszykAsync();
}
catch (Exception)
{
await DisplayAlertAsync("Baza", "Operacja się nie powiodła. Sprawdź dane i spróbuj ponownie.", "OK");
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
try { await OdswiezProduktyAsync(); await OdswiezKoszykAsync(); }
catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się wczytać sklepu.", "OK"); }
}
}
_ = OdswiezProduktyAsync();Konstruktor nie może byćasync. Podkreślnik celowo ignoruje zwróconyTask, a metoda i tak wykona się w tle.e.CurrentSelection?.FirstOrDefault() is Produkt pOdczyt zaznaczonego elementu z argumentu zdarzenia.int.TryParse(txtIlosc.Text, out int ilosc) || ilosc <= 0Walidacja: liczba dodatnia.await OdswiezKoszykAsync()po każdej zmianie Zmiana w bazie nie odświeża listy samoczynnie.- Bez ignorowania Task w konstruktorze Dane odczytujemy w OnAppearing z await i obsługą wyjątku, zamiast uruchamiać zapytania i ignorować ich rezultat.
5. Odbiór: czy naprawdę działa?
Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.
10 · Czego unikać
Cztery błędy w projekcie z bazą
Sklejanie SQL z danymi użytkownika
cmd.CommandText = "... WHERE nazwa = '" + txtSzukaj.Text + "'";Luka bezpieczeństwa (SQL injection) i błąd przy apostrofie w nazwie.
cmd.CommandText = "... WHERE nazwa LIKE @f";
cmd.Parameters.AddWithValue("@f", "%" + filtr + "%");Parametr jest zawsze daną, a nie częścią zapytania.
Brak `SQLitePCL.Batteries_V2.Init()`
public BazaDanych() { UtworzTabele(); }Na Windows może działać, a na Androidzie i iOS zabraknie natywnej biblioteki SQLite.
SQLitePCL.Batteries_V2.Init();Microsoft.Data.Sqlite wymaga jawnej inicjalizacji silnika.
Zły numer kolumny w `GetString`
// SELECT id, nazwa, cena ...
Nazwa = r.GetString(2)Wyjątek lub zamienione wartości, bo indeks to numer w SELECT.
Nazwa = r.GetString(1)Indeks odpowiada kolejności kolumn w zapytaniu.
Brak `using` przy połączeniu
var con = new SqliteConnection(ConnectionString);Połączenia nie są zamykane, co przy wielu operacjach wyczerpuje zasoby.
await using var con = new SqliteConnection(ConnectionString);using gwarantuje zwolnienie zasobu.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Dodaj przycisk usuwający tylko wybraną pozycję koszyka.
Dodaj przycisk usuwający tylko wybraną pozycję koszyka. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Odczytaj Id obiektu KoszykWidok, nie Id produktu.
Krok 2 — pokaż podpowiedź
Wywołaj istniejące UsunZKoszykaAsync.
Krok 3 — pokaż podpowiedź
Odśwież listę koszyka i sumę, pozostawiając produkt w katalogu.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
11 · Sprawdź się
Zapytanie, metoda czy odświeżenie?
Cztery sytuacje.
Podsumowanie
Co warto zapamiętać
- Dwie tabele łączy klucz obcy (
Koszyk.produkt_idwskazujeProdukty.id), a czytelną listę składaJOIN. Microsoft.Data.Sqlitewymaga pakietu silnika iSQLitePCL.Batteries_V2.Init().- Dane od użytkownika zawsze parametrami (
@f,@n), nigdy sklejane. - Kolumny w
GetInt32(i)liczysz wedługSELECT. Połączenia zamykaj przezawait using. - Po każdej zmianie w bazie odśwież listy. Pojedynczą wartość odczytasz
ExecuteScalarAsync.
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ę →