Aplikacje mobilne · Baza danych
Baza danych SQLite w aplikacji mobilnej: wprowadzenie
Pliki TXT i JSON dobrze służą prostym danym, ale gdy aplikacja ma przechowywać setki rekordów, sortować je i filtrować, zapis całej listy w jednym pliku przestaje wystarczać. Poznasz SQLite w MAUI: tabelę opisaną przez klasę modelu i pełny zestaw operacji dodawania, odczytu i usuwania.
- około 60 minut
- .NET MAUI · SQLite · sqlite-net-pcl
- INF.04.6.2
Po tej lekcji: zainstalować pakiet sqlite-net-pcl, opisać tabelę atrybutami modelu, napisać klasę bazy z CreateTableAsync, InsertAsync, DeleteAsync i wczytać dane na ekran.
Własny notes z dodawaniem, edycją i usuwaniem
Kiedy notatek przybywa, potrzebujesz rekordów z identyfikatorem, a nie jednego długiego napisu. Najpierw uruchomimy listę z SQLite, potem sprawdzimy każdą operację CRUD.
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
1 · Sprawdź w działaniu
Z atrybutów modelu powstaje tabela
Włączaj i wyłączaj atrybuty i obserwuj CREATE TABLE. Dodaj notatkę, zaznacz ją i usuń. Potem wyłącz CreateTableAsync i spróbuj dodać notatkę, a na końcu wyłącz [PrimaryKey], aby zobaczyć, co tracisz.
Najpierw przewidź, potem sprawdź: W modelu ORM porównaj właściwość Id z kolumną tabeli. Sprawdź PrimaryKey i AutoIncrement. Zastanów się, dlaczego edycja ma zachować dotychczasowe Id.
Klasa modelu opisuje tabelę, a CreateTableAsync<T>() ją tworzy. Bez klucza głównego nie zidentyfikujesz pojedynczego wiersza. 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 |
| Models/Notatka.cs | Logika i obsługa danych |
| Data/NotatkaDatabase.cs | Logika i obsługa danych |
| App.xaml.cs | Logika i obsługa danych |
| NotatkiPage.xaml | Wygląd strony lub kontrolki |
| NotatkiPage.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="Moje notatki" FontSize="24" />
<Entry Placeholder="Treść notatki" />
<Label Text="Pusty ekran to jeszcze nie pusta baza." />
</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 jednego pliku
Cała baza to jeden plik
W aplikacji mobilnej baza SQLite to jeden plik (.db3 lub .db) w prywatnym katalogu aplikacji. Nie instalujesz żadnego serwera i nie konfigurujesz niczego dodatkowego, a działa tak samo na Androidzie, iOS i Windows.
To połączenie z wiedzą z kwalifikacji INF.03 (tabela, klucz główny, SELECT, INSERT, DELETE), tylko w nowym kontekście: baza jest wbudowana w aplikację.
2 · Przygotowanie projektu
Pakiet sqlite-net-pcl
SQLite w MAUI obsługuje się przez pakiet NuGet. Zaznaczaj kroki w trakcie.
4 · Model = tabela
Atrybuty biblioteki SQLite
| Atrybut | Zastosowanie |
|---|---|
[PrimaryKey, AutoIncrement] | Pole Id jest kluczem głównym, a baza sama nadaje kolejne numery (jak AUTO_INCREMENT w SQL). |
[MaxLength(100)] | Opisuje długość deklarowaną w schemacie. SQLite nie egzekwuje samego VARCHAR(n), więc długość sprawdzaj też w walidacji aplikacji. |
[NotNull] | Kolumna nie może być pusta (jak NOT NULL). |
[Ignore] | Właściwość klasy, która nie ma być zapisywana jako kolumna. |
6 · Odpowiedniki SQL
Metody `SQLiteAsyncConnection`
| Metoda | Odpowiednik SQL |
|---|---|
CreateTableAsync<T>() | CREATE TABLE IF NOT EXISTS: tworzy tabelę, jeśli jej jeszcze nie ma (bezpieczne przy każdym starcie). |
Table<T>().ToListAsync() | SELECT * FROM tabela. |
InsertAsync(obiekt) | INSERT INTO .... |
UpdateAsync(obiekt) | UPDATE ... (po kluczu głównym). |
DeleteAsync(obiekt) | DELETE FROM ... WHERE Id = .... |
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.
Wersja użyta w kontroli tego przykładu: sqlite-net-pcl1.11.285. Dostarcza aktualne zależności SQLitePCLRaw3; nie dokładaj starego bundle_green2.x. Po aktualizacji przywróć pakiety i sprawdź komunikaty NuGet.
<ItemGroup>
<PackageReference Include="sqlite-net-pcl" Version="1.11.285" />
</ItemGroup>
Budowa krok po kroku
Notatka.cs
Atrybuty opisują mapowanie klasy na tabelę. Id identyfikuje rekord, a nie jego pozycję na ekranie. AutoIncrement nada wartość przy dodawaniu.
Każda właściwość publiczna staje się kolumną tabeli.
using SQLite;
namespace MojaAplikacja.Models;
public class Notatka
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(100)]
public string Tytul { get; set; } = "";
public string Tresc { get; set; } = "";
}
Budowa krok po kroku
NotatkaDatabase.cs
Czytaj w kolejności: konstruktor i _inicjalizacja, InitAsync, pobranie, dodanie/aktualizacja, usunięcie. Wspólne Task inicjalizacji jest oczekiwane przez każdą operację.
Konstruktor uruchamia tworzenie tabeli i zachowuje Task, bez blokowania wątku przez .Wait(). Każda operacja najpierw await InitAsync(), więc czeka na to samo ukończenie inicjalizacji. Błąd inicjalizacji trafia do wywołującej metody; ekran powinien go przechwycić i pokazać komunikat.
using SQLite;
using MojaAplikacja.Models;
namespace MojaAplikacja.Data;
public class NotatkaDatabase
{
private readonly SQLiteAsyncConnection _database;
private readonly Task _inicjalizacja;
public NotatkaDatabase(string dbPath)
{
_database = new SQLiteAsyncConnection(dbPath);
_inicjalizacja = _database.CreateTableAsync<Notatka>();
}
private Task InitAsync() => _inicjalizacja;
public async Task<List<Notatka>> GetNotatkiAsync()
{
await InitAsync();
return await _database.Table<Notatka>().ToListAsync();
}
public async Task<int> ZapiszNotatkeAsync(Notatka notatka)
{
await InitAsync();
return await _database.InsertAsync(notatka);
}
public async Task<int> AktualizujNotatkeAsync(Notatka notatka)
{
await InitAsync();
return await _database.UpdateAsync(notatka);
}
public async Task<int> UsunNotatkeAsync(Notatka notatka)
{
await InitAsync();
return await _database.DeleteAsync(notatka);
}
}
private readonly Task _inicjalizacjaZapamiętana jedna operacja tworzenia tabeli. Nie uruchamiamy jej ponownie przy każdym zapytaniu.await InitAsync()Oczekiwanie asynchroniczne, przed odczytem i każdą zmianą. Nie używaj .Result ani .Wait() do blokowania UI.return await ...Po inicjalizacji wykonujemy właściwą operację i zwracamy jej wynik.
Budowa krok po kroku
App.xaml.cs
App tworzy jeden obiekt dostępu do bazy i wystawia App.Database. Plik danych trafia do AppDataDirectory. To nie to samo co otwieranie połączenia przy każdym kliknięciu.
Bazę tworzysz raz przy starcie i udostępniasz statycznie. Strony korzystają z App.Database.
using MojaAplikacja.Data;
namespace MojaAplikacja;
public partial class App : Application
{
// Jedna, współdzielona instancja bazy dla całej aplikacji
public static NotatkaDatabase Database { get; private set; } = null!;
public App()
{
InitializeComponent();
string dbPath = Path.Combine(FileSystem.AppDataDirectory, "notatki.db3");
Database = new NotatkaDatabase(dbPath);
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new NotatkiPage());
}
}
public static NotatkaDatabase DatabaseWspólna instancja dla całej aplikacji.Path.Combine(FileSystem.AppDataDirectory, "notatki.db3")Plik bazy w prywatnym katalogu aplikacji.
Budowa krok po kroku
NotatkiPage.xaml
Dodaj lub otwórz NotatkiPage.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.
Zaznaczenie wypełnia pola. Dodaj tworzy nową notatkę, Zapisz zmiany aktualizuje zaznaczoną, Usuń usuwa ją z bazy.
<?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.Models"
x:Class="MojaAplikacja.NotatkiPage" Title="Notatki">
<VerticalStackLayout Padding="16" Spacing="12">
<Entry x:Name="TytulEntry" Placeholder="Tytuł notatki" />
<Editor x:Name="TrescEntry" Placeholder="Treść notatki"
AutoSize="TextChanges" HeightRequest="120" Margin="0,4,0,0" />
<Button Text="Dodaj notatkę" Clicked="Dodaj_Click" Margin="0,8,0,0" />
<CollectionView x:Name="ListaNotatek" HeightRequest="300"
SelectionMode="Single" SelectionChanged="Zaznaczenie_Zmienione" Margin="0,12,0,0">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Notatka">
<Border Stroke="Gray" Padding="8" Margin="0,6">
<VerticalStackLayout Spacing="4">
<Label Text="{Binding Tytul}" FontAttributes="Bold" FontSize="16" />
<Label Text="{Binding Tresc}" FontSize="14" LineBreakMode="WordWrap" />
</VerticalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button Text="Zapisz zmiany zaznaczonej" Clicked="Edytuj_Click" />
<Button Text="Usuń zaznaczoną" Clicked="Usun_Click" Margin="0,8,0,0" />
</VerticalStackLayout>
</ContentPage>
- Przeczytaj powiązanie x:Name identyfikuje kontrolkę, a Binding odczytuje właściwość obiektu ustawionego jako BindingContext. To dwa różne mechanizmy.
Budowa krok po kroku
NotatkiPage.xaml.cs
Najpierw odświeżenie listy, potem dodanie, zaznaczenie/edycja i usunięcie. Przy edycji zachowaj Id wybranej notatki, bo Id=0 oznacza nowy rekord.
Pełne dodawanie, odczyt, edycja i usuwanie. Operacje bazy są awaitowane; wyjątki w zdarzeniach mają komunikat. Zaznaczenie wypełnia pola, a edycja zachowuje klucz Id.
using MojaAplikacja.Models;
namespace MojaAplikacja;
public partial class NotatkiPage : ContentPage
{
private Notatka? _wybrana;
public NotatkiPage() => InitializeComponent();
protected override async void OnAppearing()
{
base.OnAppearing();
try { await OdswiezAsync(); }
catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się wczytać notatek.", "OK"); }
}
private async Task OdswiezAsync()
=> ListaNotatek.ItemsSource = await App.Database.GetNotatkiAsync();
private void Zaznaczenie_Zmienione(object sender, SelectionChangedEventArgs e)
{
_wybrana = e.CurrentSelection.FirstOrDefault() as Notatka;
TytulEntry.Text = _wybrana?.Tytul ?? "";
TrescEntry.Text = _wybrana?.Tresc ?? "";
}
private async void Dodaj_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(TytulEntry.Text))
{ await DisplayAlertAsync("Tytuł", "Podaj tytuł.", "OK"); return; }
try
{
await App.Database.ZapiszNotatkeAsync(new Notatka { Tytul = TytulEntry.Text.Trim(), Tresc = TrescEntry.Text ?? "" });
await OdswiezAsync();
}
catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się dodać notatki.", "OK"); }
}
private async void Edytuj_Click(object sender, EventArgs e)
{
if (_wybrana is null || string.IsNullOrWhiteSpace(TytulEntry.Text))
{ await DisplayAlertAsync("Edycja", "Zaznacz notatkę i wpisz tytuł.", "OK"); return; }
try
{
// Zachowujemy Id, aby UpdateAsync zmieniło istniejący rekord.
var zmieniona = new Notatka { Id = _wybrana.Id, Tytul = TytulEntry.Text.Trim(), Tresc = TrescEntry.Text ?? "" };
await App.Database.AktualizujNotatkeAsync(zmieniona);
await OdswiezAsync();
}
catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się zapisać zmian.", "OK"); }
}
private async void Usun_Click(object sender, EventArgs e)
{
if (_wybrana is null) { await DisplayAlertAsync("Usuwanie", "Zaznacz notatkę.", "OK"); return; }
try { await App.Database.UsunNotatkeAsync(_wybrana); await OdswiezAsync(); }
catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się usunąć notatki.", "OK"); }
}
}
SelectionChangedi_wybranaZdarzenie listy daje bieżące zaznaczenie. Brak zaznaczenia oznacza null i blokuje edycję/usunięcie.Id = _wybrana.IdTożsamość rekordu pozostaje ta sama. InsertAsync utworzyłby nowy rekord; do zmiany używamy UpdateAsync.await OdswiezAsync()Ponownie pobiera dane po zapisie; ItemsSource nie odświeża się automatycznie po SQL.
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.
11 · Czego unikać
Cztery błędy z SQLite
Nowe połączenie na każdej stronie
var db = new SQLiteAsyncConnection(sciezka); // w każdej stronieNiepotrzebne zużycie zasobów i możliwe konflikty przy jednoczesnym dostępie.
App.Database.GetNotatkiAsync() // jedna wspólna instancjaJedno połączenie dla całej aplikacji.
Brak `CreateTableAsync`
_database = new SQLiteAsyncConnection(dbPath); // bez tworzenia tabeliBłąd „no such table”, bo tabela nigdy nie powstała.
await InitAsync(); // przed każdym zapytaniem w klasie bazyKażda operacja czeka na jedną zapamiętaną inicjalizację. Nie blokuj wątku UI przez .Wait().
Brak odświeżenia listy po zmianie
await App.Database.ZapiszNotatkeAsync(nowa); // lista bez zmianNotatka jest w bazie, ale nie widać jej na ekranie.
await App.Database.ZapiszNotatkeAsync(nowa);
WczytajNotatki();InsertAsync zmienia bazę, a nie ItemsSource.
Ręczne `Id` przy auto-inkrementacji
var nowa = new Notatka { Id = 5, Tytul = "A" };Możliwe konflikty kluczy.
var nowa = new Notatka { Tytul = "A" }; // Id nada bazaPrzy AutoIncrement numer nadaje baza.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Dodaj potwierdzenie usunięcia wybranej notatki.
Dodaj potwierdzenie usunięcia wybranej notatki. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Przed wywołaniem metody usuwającej otwórz DisplayAlertAsync z Tak i Nie.
Krok 2 — pokaż podpowiedź
Wywołuj bazę dopiero, gdy wynik bool jest true.
Krok 3 — pokaż podpowiedź
Sprawdź anulowanie — rekord i zaznaczenie nie powinny zostać bez powodu skasowane.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
12 · Sprawdź się
Atrybut, metoda czy odświeżenie?
Cztery sytuacje.
Podsumowanie
Co warto zapamiętać
- Baza SQLite w MAUI to jeden plik w
FileSystem.AppDataDirectory. Używasz pakietusqlite-net-pcl. - Klasa modelu = tabela. Atrybuty:
[PrimaryKey, AutoIncrement],[MaxLength],[NotNull],[Ignore]. - Metody:
CreateTableAsync,Table<T>().ToListAsync(),InsertAsync,UpdateAsync,DeleteAsync. - Całą obsługę tabeli trzymaj w jednej klasie, a bazę utwórz raz i udostępnij przez
App.Database. - Po zmianie w bazie odśwież listę ponownym pobraniem danych.
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ę →