Aplikacje mobilne · Pliki
Obsługa plików TXT i JSON w .NET MAUI
Bez trwałego zapisu aplikacja traci dane po zamknięciu i każde uruchomienie zaczyna od zera. Pliki pozwalają zachować dane na urządzeniu. Zapiszesz i wczytasz tekst (TXT) oraz listę (JSON) w bezpiecznym katalogu aplikacji, bo na telefonie nie można zapisywać „gdziekolwiek”.
- około 55 minut
- .NET MAUI · File · FileSystem.AppDataDirectory · JSON
- INF.04.6.2
Po tej lekcji: wyznaczyć ścieżkę przez FileSystem.AppDataDirectory, zapisać i odczytać TXT oraz JSON (JsonSerializer), sprawdzić File.Exists i odświeżyć CollectionView.
Notatka, która przeżywa zamknięcie programu
Pole tekstowe przechowuje treść tylko podczas pracy aplikacji. Zapiszemy ją do pliku, a potem pokażemy, kiedy zamiast zwykłego tekstu przydaje się JSON.
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
Kiedy dane przetrwają zamknięcie aplikacji
Uruchom aplikację, dodaj kilka imion, zapisz je i zamknij aplikację. Uruchom ponownie. Potem wyłącz zapis w OnDisappearing, dopisz imiona i zamknij: dane przepadną. Wyłącz też File.Exists na świeżym starcie, aby zobaczyć awarię.
Najpierw przewidź, potem sprawdź: W modelu plików porównaj zawartość TXT i JSON. Wskaż, co jest samą treścią, a co nazwą właściwości w strukturze danych. Model nie zapisuje pliku na Twoim urządzeniu.
Dodanie do listy w pamięci (RAM) nie jest zapisem na dysk. Dane przetrwają dopiero po WriteAllTextAsync. 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 |
|---|---|
| TxtPage.xaml | Wygląd strony lub kontrolki |
| TxtPage.xaml.cs | Logika i obsługa danych |
| JsonPage.xaml | Wygląd strony lub kontrolki |
| JsonPage.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="Notatnik TXT" FontSize="24" />
<Editor Placeholder="Wpisz notatkę" HeightRequest="150" />
<Label Text="Ten tekst jeszcze nie jest zapisany w pliku." />
</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 telefonu
Piaskownica zamiast dowolnego folderu
W odróżnieniu od aplikacji desktopowej nie wolno zapisywać plików w dowolnym miejscu. Aplikacje na Androidzie i iOS działają w piaskownicy (sandbox) z ograniczonym dostępem do systemu plików, ze względów bezpieczeństwa. Każda aplikacja ma swój prywatny katalog danych, a jego ścieżkę podaje FileSystem.AppDataDirectory.
To bezpośrednia realizacja wymagania „przechowywanie danych”, jednego z najczęściej sprawdzanych na egzaminie.
2 · Ściągawka
Najważniejsze metody
| Zadanie | Zapis w C# | Uwagi |
|---|---|---|
| Ścieżka do katalogu aplikacji | FileSystem.AppDataDirectory | Prywatny katalog w piaskownicy. |
| Złożenie ścieżki | Path.Combine(katalog, "plik.txt") | Sam dobiera separator dla platformy. |
| Zapis tekstu | await File.WriteAllTextAsync(sciezka, tekst) | Nadpisuje plik. |
| Odczyt tekstu | await File.ReadAllTextAsync(sciezka) | Zwraca string. |
| Czy plik istnieje | File.Exists(sciezka) | Zawsze przed odczytem. |
| Obiekt → JSON | JsonSerializer.Serialize(obiekt) | Zachowuje strukturę danych. |
| JSON → obiekt | JsonSerializer.Deserialize<T>(json) | Wynik bywa null. |
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 TxtPage, 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 TxtPage()));
}
CreateWindowTworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.new TxtPage()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
TxtPage.xaml
Dodaj lub otwórz TxtPage.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.
Editor na notatkę, dwa przyciski i etykieta statusu.
<?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"
x:Class="MojaAplikacja.TxtPage" Title="Notatnik TXT">
<VerticalStackLayout Padding="16" Spacing="12">
<Editor x:Name="NotatkaEditor" HeightRequest="150" Placeholder="Wpisz notatkę..." />
<HorizontalStackLayout Spacing="8">
<Button Text="Zapisz" Clicked="Zapisz_Click" />
<Button Text="Wczytaj" Clicked="Wczytaj_Click" />
</HorizontalStackLayout>
<Label x:Name="Status" TextColor="Gray" />
</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
TxtPage.xaml.cs
Path.Combine buduje ścieżkę w prywatnym katalogu aplikacji. WriteAllTextAsync zastępuje zawartość pliku, a ReadAllTextAsync ją odczytuje. File.Exists oddziela brak pliku od odczytu.
Ścieżka jest polem klasy, więc obliczasz ją raz i używasz w obu metodach.
namespace MojaAplikacja;
public partial class TxtPage : ContentPage
{
// Pełna ścieżka do pliku w bezpiecznym katalogu danych aplikacji
private readonly string _path = Path.Combine(FileSystem.AppDataDirectory, "notatka.txt");
public TxtPage()
{
InitializeComponent();
}
private async void Zapisz_Click(object sender, EventArgs e)
{
try
{
await File.WriteAllTextAsync(_path, NotatkaEditor.Text ?? "");
Status.Text = "Zapisano plik.";
}
catch (Exception)
{
await DisplayAlertAsync("Plik", "Nie udało się odczytać lub zapisać pliku. Sprawdź dostęp i format danych.", "OK");
}
}
private async void Wczytaj_Click(object sender, EventArgs e)
{
try
{
if (File.Exists(_path))
{
NotatkaEditor.Text = await File.ReadAllTextAsync(_path);
Status.Text = "Wczytano plik.";
}
else
{
Status.Text = "Plik nie istnieje (zapisz coś najpierw).";
}
}
catch (Exception)
{
await DisplayAlertAsync("Plik", "Nie udało się odczytać lub zapisać pliku. Sprawdź dostęp i format danych.", "OK");
}
}
}
Path.Combine(FileSystem.AppDataDirectory, "notatka.txt")Ten sam kod działa na Androidzie, iOS i Windows, bo katalog i separator dobiera platforma.async voidiawait File.WriteAllTextAsync(...)Operacje na plikach są asynchroniczne, więc nie blokują interfejsu.NotatkaEditor.Text ?? ""Zamienianullna pusty napis.if (File.Exists(_path))Przy pierwszym uruchomieniu pliku jeszcze nie ma. Bez sprawdzenia odczyt zgłosi wyjątek i aplikacja się zamknie.try/catchwokół operacji plikowej File.Exists nie gwarantuje odczytu ani poprawności JSON. Błąd pliku ma komunikat, nie ucieka z handlera async void.
Zatrzymaj się przed kolejnym wariantem
Nie zastępuj jeszcze działającej wersji. Wykonaj tę próbę i dopiero potem przejdź do dodatku.
Dopiero teraz: drugi wariant lub rozszerzenie
To następny etap tej lekcji, nie warunek pierwszego uruchomienia. Porównaj jego rolę z działającą bazą. Jeśli brakuje czasu, wróć do niego po sprawdzeniu podstawowej wersji.
Dodatek do działającej wersji
JsonPage.xaml
Dodaj lub otwórz JsonPage.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.
Pole, przycisk „Dodaj”, CollectionView i przyciski zapisu oraz odczytu.
<?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"
x:Class="MojaAplikacja.JsonPage" Title="Lista JSON">
<VerticalStackLayout Padding="16" Spacing="10">
<Entry x:Name="ImieEntry" Placeholder="Wpisz imię" />
<Button Text="Dodaj" Clicked="Dodaj_Click" />
<CollectionView x:Name="Lista" HeightRequest="200">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Label Text="{Binding .}" FontSize="18" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<HorizontalStackLayout Spacing="8">
<Button Text="Zapisz JSON" Clicked="ZapiszJson_Click" />
<Button Text="Wczytaj JSON" Clicked="WczytajJson_Click" />
</HorizontalStackLayout>
</VerticalStackLayout>
</ContentPage>
<Label Text="{Binding .}" />Kropka oznacza cały element. Tu to zwykły napis.
Dodatek do działającej wersji
JsonPage.xaml.cs
Serializujemy List<string>, czyli listę imion, nie pojedynczy model formularza. Deserialize<List<string>> musi odtworzyć ten sam kształt. Po wczytaniu ponownie przypisujemy ItemsSource.
Dodanie imienia zmienia tylko listę w pamięci. Dopiero „Zapisz JSON” utrwala ją w pliku.
using System.Text.Json;
namespace MojaAplikacja;
public partial class JsonPage : ContentPage
{
private readonly string _path = Path.Combine(FileSystem.AppDataDirectory, "imiona.json");
private List<string> _imiona = new();
public JsonPage()
{
InitializeComponent();
}
private void Dodaj_Click(object sender, EventArgs e)
{
if (!string.IsNullOrWhiteSpace(ImieEntry.Text))
{
_imiona.Add(ImieEntry.Text.Trim());
// Wymuszenie odświeżenia CollectionView dla zwykłej List<T>
Lista.ItemsSource = null;
Lista.ItemsSource = _imiona;
ImieEntry.Text = "";
}
}
private async void ZapiszJson_Click(object sender, EventArgs e)
{
try
{
var json = JsonSerializer.Serialize(_imiona);
await File.WriteAllTextAsync(_path, json);
await DisplayAlertAsync("Informacja", "Zapisano dane do JSON.", "OK");
}
catch (Exception)
{
await DisplayAlertAsync("Plik", "Nie udało się odczytać lub zapisać pliku. Sprawdź dostęp i format danych.", "OK");
}
}
private async void WczytajJson_Click(object sender, EventArgs e)
{
try
{
if (File.Exists(_path))
{
var json = await File.ReadAllTextAsync(_path);
_imiona = JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
Lista.ItemsSource = null;
Lista.ItemsSource = _imiona;
}
else
{
await DisplayAlertAsync("Informacja", "Plik nie istnieje. Zapisz dane najpierw.", "OK");
}
}
catch (Exception)
{
await DisplayAlertAsync("Plik", "Nie udało się odczytać lub zapisać pliku. Sprawdź dostęp i format danych.", "OK");
}
}
}
Lista.ItemsSource = null; Lista.ItemsSource = _imiona;ZwykłaList<T>nie powiadamia o zmianach, więc odświeżamy widok ręcznie. ZObservableCollection<T>nie trzeba.JsonSerializer.Serialize(_imiona)Zamienia listę na tekst JSON, który zapisujemy do pliku.Deserialize<List<string>>(json) ?? new List<string>()Wczytuje listę, a gdy wynik tonull, zaczyna od pustej.DisplayAlertAsync(...)Potwierdza zapis albo informuje o braku pliku.try/catchwokół operacji plikowej File.Exists nie gwarantuje odczytu ani poprawności JSON. Błąd pliku ma komunikat, nie ucieka z handlera async void.
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.
7 · Czego unikać
Cztery błędy z plikami w aplikacji mobilnej
Zapis w „wymyślonym” folderze
File.WriteAllText(@"C:\dane\notatka.txt", tekst);Na telefonie taki folder nie istnieje albo aplikacja nie ma do niego uprawnień.
var sciezka = Path.Combine(FileSystem.AppDataDirectory, "notatka.txt");Zawsze używaj prywatnego katalogu aplikacji.
Odczyt bez `File.Exists`
var tekst = await File.ReadAllTextAsync(_path);Przy pierwszym uruchomieniu plik nie istnieje, więc aplikacja zgłasza wyjątek i się zamyka.
if (File.Exists(_path)) { ... }Pierwszemu uruchomieniu nie towarzyszył żaden zapis.
Brak odświeżenia `List<T>`
_imiona.Add(imie); // bez zmiany ItemsSourceNowy element nie pojawia się na ekranie.
Lista.ItemsSource = null;
Lista.ItemsSource = _imiona; // albo ObservableCollectionZwykła lista nie powiadamia widoku o zmianach.
Pamięć to nie zapis
Dodałem element do listy, więc jest zapisany.Po zamknięciu aplikacji dane znikają.
Wywołaj zapis (przycisk albo automatycznie po zmianie, np. w `OnDisappearing`).RAM znika razem z procesem aplikacji.
Ćwiczenie z pomocą · samodzielna praca
6. Mała zmiana, którą zrobisz samodzielnie
Dodaj do JsonPage przycisk usuwający ostatnie imię z listy.
Dodaj do JsonPage przycisk usuwający ostatnie imię z listy. Dotychczasowe działanie ma pozostać poprawne.
? Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź
Sprawdź _imiona.Count przed usunięciem, żeby pusta lista nie dawała błędu.
Krok 2 — pokaż podpowiedź
RemoveAt(_imiona.Count - 1) usuwa ostatni element; odśwież ItemsSource jak po dodaniu.
Krok 3 — pokaż podpowiedź
Zapisz JSON, zamknij proces i wczytaj ponownie. Usunięte imię nie powinno wrócić.
Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.
8 · Sprawdź się
Gdzie i jak zapisać dane?
Cztery sytuacje.
Podsumowanie
Co warto zapamiętać
- Aplikacja mobilna działa w piaskownicy: pliki zapisuj w
FileSystem.AppDataDirectory(Path.Combine). - TXT:
WriteAllTextAsynciReadAllTextAsync. Lista i obiekty: JSON (Serialize,Deserialize<T>). - Przed odczytem sprawdzaj
File.Exists. - Zwykła
List<T>wymaga ręcznego odświeżeniaItemsSource(albo użyjObservableCollection). - Zmiana w pamięci to nie zapis. Dane utrwalasz wywołaniem zapisu.
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ę →