Bazy Danych w .Net Maui. Produkty + Koszyk (SQLite, offline)

1) Po co baza danych w aplikacji mobilnej?

W mobilce jest dokładnie to samo co w desktopie:

  • lista produktów/klientów/zamówień,
  • historia działań,
  • dane użytkownika, ustawienia.

Gdyby wszystko było tylko w RAM, po zamknięciu aplikacji wszystko znika.
SQLite pasuje idealnie, bo:

  • działa offline,
  • nie potrzebuje serwera,
  • baza to jeden plik,
  • działa na Android/iOS/Windows.

2) Jak to działa w MAUI (łańcuch)

[UI – XAML] → [Code-behind strony – MainPage.xaml.cs] → [Logika bazy – BazaDanych.cs] → [plik sklep.db w AppDataDirectory]

  • XAML – wygląd: Entry, Button, listy.
  • MainPage.xaml.cs – obsługa kliknięć (Dodaj/Usuń/Zapisz/Szukaj/Dodaj do koszyka).
  • BazaDanych.cs – tworzenie tabel i SQL (SELECT/INSERT/UPDATE/DELETE/JOIN).
  • sklep.db – plik w telefonie w: FileSystem.AppDataDirectory.

3) Pakiety NuGet (MAUI)

W projekcie MAUI doinstaluj:

  • Microsoft.Data.Sqlite
  • SQLitePCLRaw.bundle_e_sqlite3

Dzięki temu masz SQLite działające też na Android/iOS.


4) Struktura plików (czytelnie)

  • Modele
    • Produkt.cs
    • KoszykWidok.cs
  • Logika
    • BazaDanych.cs
  • MainPage.xaml
  • MainPage.xaml.cs

5) MODELE

Modele/Produkt.cs

namespace BazaDanychMaui.Modele
{
    public class Produkt
    {
        public int Id { get; set; }
        public string Nazwa { get; set; } = "";
        public double Cena { get; set; }
    }
}

Modele/KoszykWidok.cs

namespace BazaDanychMaui.Modele
{
    // To jest “widok” koszyka po JOIN (czyli gotowe do wyświetlenia)
    public class KoszykWidok
    {
        public int Id { get; set; }          // id wpisu w koszyku
        public string Nazwa { get; set; } = "";
        public double Cena { get; set; }
        public int Ilosc { get; set; }
        public double Suma { get; set; }
    }
}

6) LOGIKA BAZY (SQLite + SQL)

Logika/BazaDanych.cs

using Microsoft.Data.Sqlite;
using BazaDanychMaui.Modele;

namespace BazaDanychMaui.Logika
{
    public class BazaDanych
    {
        private readonly string _sciezkaBazy;

        public BazaDanych()
        {
            // WAŻNE: inicjalizacja silnika SQLite (Android/iOS)
            SQLitePCL.Batteries_V2.Init();

            _sciezkaBazy = Path.Combine(FileSystem.AppDataDirectory, "sklep.db");
            UtworzTabeleJesliNieIstnieja();
        }

        private string ConnectionString => $"Data Source={_sciezkaBazy}";

        private void UtworzTabeleJesliNieIstnieja()
        {
            using var con = new SqliteConnection(ConnectionString);
            con.Open();

            string sqlProdukty =
                "CREATE TABLE IF NOT EXISTS Produkty (" +
                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "nazwa TEXT NOT NULL, " +
                "cena REAL NOT NULL);";

            string sqlKoszyk =
                "CREATE TABLE IF NOT EXISTS Koszyk (" +
                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "produkt_id INTEGER NOT NULL, " +
                "ilosc INTEGER NOT NULL);";

            using var cmd1 = con.CreateCommand();
            cmd1.CommandText = sqlProdukty;
            cmd1.ExecuteNonQuery();

            using var cmd2 = con.CreateCommand();
            cmd2.CommandText = sqlKoszyk;
            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 + "%");

            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();

            await using var cmd = con.CreateCommand();
            cmd.CommandText = "DELETE FROM Produkty WHERE id=@id";
            cmd.Parameters.AddWithValue("@id", id);

            await cmd.ExecuteNonQueryAsync();
        }

        public async Task EdytujProduktAsync(int id, string nazwa, double cena)
        {
            await using var con = new SqliteConnection(ConnectionString);
            await con.OpenAsync();

            await using var cmd = con.CreateCommand();
            cmd.CommandText = "UPDATE Produkty SET nazwa=@n, cena=@c WHERE id=@id";
            cmd.Parameters.AddWithValue("@n", nazwa);
            cmd.Parameters.AddWithValue("@c", cena);
            cmd.Parameters.AddWithValue("@id", id);

            await cmd.ExecuteNonQueryAsync();
        }

        // -------------------- 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) VALUES (@pid, @ilosc)";
            cmd.Parameters.AddWithValue("@pid", produktId);
            cmd.Parameters.AddWithValue("@ilosc", ilosc);

            await cmd.ExecuteNonQueryAsync();
        }

        public async Task<List<KoszykWidok>> PobierzKoszykAsync()
        {
            var lista = new List<KoszykWidok>();

            await using var con = new SqliteConnection(ConnectionString);
            await con.OpenAsync();

            string sql =
                "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 cmd = con.CreateCommand();
            cmd.CommandText = sql;

            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;
        }
    }
}

7) UI (MAUI XAML) – zamiast DataGrid używamy CollectionView

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="BazaDanychMaui.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Title="Produkty + Koszyk">

    <ScrollView>
        <VerticalStackLayout Padding="12" Spacing="12">

            <!-- FORMULARZ -->
            <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="Zapisz" Clicked="btnZapisz_Clicked"/>
                <Button Text="Usuń" Clicked="btnUsun_Clicked"/>
            </HorizontalStackLayout>

            <!-- SZUKAJ -->
            <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>

            <!-- LISTA PRODUKTÓW -->
            <Label Text="Produkty (kliknij, żeby wypełnić pola)" FontAttributes="Bold"/>

            <CollectionView x:Name="listaProdukty" SelectionMode="Single" SelectionChanged="listaProdukty_SelectionChanged">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <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>

            <!-- KOSZYK -->
            <Label Text="Koszyk" FontAttributes="Bold"/>

            <Entry x:Name="txtIlosc" Placeholder="Ilość (do koszyka)" Keyboard="Numeric"/>
            <Button Text="Dodaj do koszyka (używa ID z pola ID)" Clicked="btnDodajDoKoszyka_Clicked"/>

            <CollectionView x:Name="listaKoszyk">
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <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>

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

8) Logika strony (MainPage.xaml.cs)

MainPage.xaml.cs

using BazaDanychMaui.Logika;
using BazaDanychMaui.Modele;

namespace BazaDanychMaui;

public partial class MainPage : ContentPage
{
    private readonly BazaDanych _baza = new BazaDanych();

    public MainPage()
    {
        InitializeComponent();
        _ = OdswiezProduktyAsync();
        _ = OdswiezKoszykAsync();
    }

    private async Task OdswiezProduktyAsync(string filtr = "")
    {
        listaProdukty.ItemsSource = await _baza.PobierzProduktyAsync(filtr);
    }

    private async Task OdswiezKoszykAsync()
    {
        listaKoszyk.ItemsSource = await _baza.PobierzKoszykAsync();
    }

    private async void btnDodaj_Clicked(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtNazwa.Text))
        {
            await DisplayAlert("Błąd", "Podaj nazwę!", "OK");
            return;
        }

        if (!double.TryParse(txtCena.Text, out double cena))
        {
            await DisplayAlert("Błąd", "Cena musi być liczbą.", "OK");
            return;
        }

        await _baza.DodajProduktAsync(txtNazwa.Text.Trim(), cena);
        await OdswiezProduktyAsync();
    }

    private async void btnUsun_Clicked(object sender, EventArgs e)
    {
        if (!int.TryParse(txtID.Text, out int id))
        {
            await DisplayAlert("Błąd", "Podaj poprawne ID!", "OK");
            return;
        }

        await _baza.UsunProduktAsync(id);
        await OdswiezProduktyAsync();
    }

    private async void btnZapisz_Clicked(object sender, EventArgs e)
    {
        if (!int.TryParse(txtID.Text, out int id))
        {
            await DisplayAlert("Błąd", "Podaj ID!", "OK");
            return;
        }

        if (string.IsNullOrWhiteSpace(txtNazwa.Text))
        {
            await DisplayAlert("Błąd", "Podaj nazwę!", "OK");
            return;
        }

        if (!double.TryParse(txtCena.Text, out double cena))
        {
            await DisplayAlert("Błąd", "Cena musi być liczbą.", "OK");
            return;
        }

        await _baza.EdytujProduktAsync(id, txtNazwa.Text.Trim(), cena);
        await OdswiezProduktyAsync();
    }

    private async void btnSzukaj_Clicked(object sender, EventArgs e)
    {
        await OdswiezProduktyAsync(txtSzukaj.Text ?? "");
    }

    private async void btnOdswiez_Clicked(object sender, EventArgs e)
    {
        txtSzukaj.Text = "";
        await OdswiezProduktyAsync();
    }

    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)
    {
        if (!int.TryParse(txtID.Text, out int produktId))
        {
            await DisplayAlert("Błąd", "Najpierw wybierz produkt (albo wpisz ID).", "OK");
            return;
        }

        if (!int.TryParse(txtIlosc.Text, out int ilosc) || ilosc <= 0)
        {
            await DisplayAlert("Błąd", "Podaj poprawną ilość (>0).", "OK");
            return;
        }

        await _baza.DodajDoKoszykaAsync(produktId, ilosc);
        await OdswiezKoszykAsync();
    }
}