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.

Ta interakcja wymaga włączonego JavaScriptu.

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.

PlikRola w ćwiczeniu
TxtPage.xamlWygląd strony lub kontrolki
TxtPage.xaml.csLogika i obsługa danych
JsonPage.xamlWygląd strony lub kontrolki
JsonPage.xaml.csLogika 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.

MainPage.xaml — etap tymczasowyxaml
<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>
  1. 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

ZadanieZapis w C#Uwagi
Ścieżka do katalogu aplikacjiFileSystem.AppDataDirectoryPrywatny katalog w piaskownicy.
Złożenie ścieżkiPath.Combine(katalog, "plik.txt")Sam dobiera separator dla platformy.
Zapis tekstuawait File.WriteAllTextAsync(sciezka, tekst)Nadpisuje plik.
Odczyt tekstuawait File.ReadAllTextAsync(sciezka)Zwraca string.
Czy plik istniejeFile.Exists(sciezka)Zawsze przed odczytem.
Obiekt → JSONJsonSerializer.Serialize(obiekt)Zachowuje strukturę danych.
JSON → obiektJsonSerializer.Deserialize<T>(json)Wynik bywa null.
TXT wystarcza dla jednego tekstu (notatka). Dla listy lub obiektów użyj JSON, który zachowuje strukturę.

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.

App.xaml.cscsharp
namespace MojaAplikacja;

public partial class App : Application
{
    public App() => InitializeComponent();
    protected override Window CreateWindow(IActivationState? activationState)
        => new Window(new NavigationPage(new TxtPage()));
}
  1. CreateWindow Tworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.
  2. 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.

TxtPage.xamlxaml
<?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>
  1. 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.

TxtPage.xaml.cscsharp
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");
        }
    }
}
  1. Path.Combine(FileSystem.AppDataDirectory, "notatka.txt") Ten sam kod działa na Androidzie, iOS i Windows, bo katalog i separator dobiera platforma.
  2. async void i await File.WriteAllTextAsync(...) Operacje na plikach są asynchroniczne, więc nie blokują interfejsu.
  3. NotatkaEditor.Text ?? "" Zamienia null na pusty napis.
  4. if (File.Exists(_path)) Przy pierwszym uruchomieniu pliku jeszcze nie ma. Bez sprawdzenia odczyt zgłosi wyjątek i aplikacja się zamknie.
  5. try/catch wokół 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.

JsonPage.xamlxaml
<?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>
  1. <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.

JsonPage.xaml.cscsharp
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");
        }
    }
}
  1. Lista.ItemsSource = null; Lista.ItemsSource = _imiona; Zwykła List<T> nie powiadamia o zmianach, więc odświeżamy widok ręcznie. Z ObservableCollection<T> nie trzeba.
  2. JsonSerializer.Serialize(_imiona) Zamienia listę na tekst JSON, który zapisujemy do pliku.
  3. Deserialize<List<string>>(json) ?? new List<string>() Wczytuje listę, a gdy wynik to null, zaczyna od pustej.
  4. DisplayAlertAsync(...) Potwierdza zapis albo informuje o braku pliku.
  5. try/catch wokół 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
Tak nie
File.WriteAllText(@"C:\dane\notatka.txt", tekst);

Na telefonie taki folder nie istnieje albo aplikacja nie ma do niego uprawnień.

Tak
var sciezka = Path.Combine(FileSystem.AppDataDirectory, "notatka.txt");

Zawsze używaj prywatnego katalogu aplikacji.

Odczyt bez `File.Exists`
Tak nie
var tekst = await File.ReadAllTextAsync(_path);

Przy pierwszym uruchomieniu plik nie istnieje, więc aplikacja zgłasza wyjątek i się zamyka.

Tak
if (File.Exists(_path)) { ... }

Pierwszemu uruchomieniu nie towarzyszył żaden zapis.

Brak odświeżenia `List<T>`
Tak nie
_imiona.Add(imie);   // bez zmiany ItemsSource

Nowy element nie pojawia się na ekranie.

Tak
Lista.ItemsSource = null;
Lista.ItemsSource = _imiona;   // albo ObservableCollection

Zwykła lista nie powiadamia widoku o zmianach.

Pamięć to nie zapis
Tak nie
Dodałem element do listy, więc jest zapisany.

Po zamknięciu aplikacji dane znikają.

Tak
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.

Oczekiwany rezultat
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.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Aplikacja mobilna działa w piaskownicy: pliki zapisuj w FileSystem.AppDataDirectory (Path.Combine).
  2. TXT: WriteAllTextAsync i ReadAllTextAsync. Lista i obiekty: JSON (Serialize, Deserialize<T>).
  3. Przed odczytem sprawdzaj File.Exists.
  4. Zwykła List<T> wymaga ręcznego odświeżenia ItemsSource (albo użyj ObservableCollection).
  5. 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ę →
Źródła i dalsza nauka