Zapisywanie i odczytywanie danych w plikach TXT i JSON w C#

1) Wprowadzenie – po co nam pliki TXT i JSON

Kiedy aplikacja ma coś zapamiętać na później (np. listę wyników, ustawienia, notatki), potrzebuje trwałego magazynu. Najprościej: pliki. Dwa najczęstsze formaty w praktyce to TXT (zwykły tekst) i JSON (tekst, ale o strukturze obiektów). TXT jest jak prosty notatnik – czytelny dla człowieka, szybki do ogarnięcia. JSON to notatnik + logiczne szuflady: pola, listy, obiekty.

W aplikacjach desktopowych (WinForms/WPF) czy mobilnych (.NET MAUI) zasada jest ta sama: zapis/odczyt to System.IO.*, a JSON to System.Text.Json. Różnice? Głównie lokalizacja plików (ścieżki) i asynchroniczność (na telefonie blokowanie wątku UI to grzech).

Dobrze od razu zapamiętać trzy filary:

  1. Ścieżka – gdzie zapisać (katalog dostępny dla aplikacji).
  2. Kodowanie – używaj UTF-8, żeby „ąęłń” nie zamieniło się w krzaczki.
  3. Obsługa błędów – pliku może nie być; odczyt może się nie udać.

2) TXT – zwykły tekst: kiedy wystarczy i jak to ugryźć

TXT sprawdza się, gdy dane są proste: linie tekstu, lista rekordów „pola rozdzielone przecinkiem”, logi. Plusy: minimalny narzut, zero dodatkowych bibliotek, łatwo otworzyć w Notatniku. Minus: musisz sam umówić się na format, np. „imię, wynik”.

Najprostszy zapis/odczyt jednego łańcucha:

using System.Text; // dla UTF-8

string text = "Jan Kowalski,10\nPiotr Nowak,2\nJan Iksiński,33";
File.WriteAllText("dane.txt", text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); // UTF-8 bez BOM

string odczytane = File.ReadAllText("dane.txt", Encoding.UTF8);
Console.WriteLine(odczytane);

Gdy dane są „rekordami po liniach”, czytamy linia po linii i „rozcinamy”:

string[] lines = File.ReadAllLines("dane.txt", Encoding.UTF8);
foreach (string line in lines)
{
    if (string.IsNullOrWhiteSpace(line)) continue;
    string[] pola = line.Split(',', StringSplitOptions.TrimEntries);
    if (pola.Length >= 2)
        Console.WriteLine($"Imię: {pola[0]}, Wynik: {pola[1]}");
}

Pułapki TXT:
• Ustal separator (przecinek, średnik, tab) i trzymaj się go.
• Używaj Encoding.UTF8, żeby polskie znaki były OK.
• Waliduj dane: pusta linia, brak przecinka, błędny wynik – to się zdarza.


3) JSON – zapis obiektów i list obiektów jak człowiek

JSON to tekst, ale z kluczami i wartościami, tablicami i zagnieżdżeniami. Idealny do konfiguracji, API, list rekordów. W .NET używamy System.Text.Json (szybki, wbudowany). Zasada: „obiekt ↔ JSON” to serializacja, a w drugą stronę deserializacja.

Klasa + zapis jednego obiektu:

using System.Text.Json;

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

// SERIALIZACJA (zapis)
var weather = new WeatherForecast
{
    Date = DateTimeOffset.Now,
    TemperatureCelsius = 18,
    Summary = "Chłodno"
};

var options = new JsonSerializerOptions { WriteIndented = true }; // czytelniej
string json = JsonSerializer.Serialize(weather, options);
File.WriteAllText("pogoda.json", json, new UTF8Encoding(false));

Odczyt (deserializacja):

string odczytanyJson = File.ReadAllText("pogoda.json", Encoding.UTF8);
WeatherForecast? pogoda = JsonSerializer.Deserialize<WeatherForecast>(odczytanyJson);
Console.WriteLine($"{pogoda?.Date:yyyy-MM-dd HH:mm} | {pogoda?.TemperatureCelsius}°C | {pogoda?.Summary}");

Najczęstszy case w projektach: lista obiektów (np. wyniki uczniów):

public record Score(string Name, int Points);

var scores = new List<Score>
{
    new("Jan", 10),
    new("Piotr", 2),
    new("Ala", 33),
};

string jsonList = JsonSerializer.Serialize(scores, new JsonSerializerOptions{ WriteIndented = true });
File.WriteAllText("scores.json", jsonList, new UTF8Encoding(false));

// ...odczyt później:
string read = File.ReadAllText("scores.json", Encoding.UTF8);
List<Score>? loaded = JsonSerializer.Deserialize<List<Score>>(read) ?? new();
Console.WriteLine($"Wczytano {loaded.Count} rekordów");

Tipy JSON:
WriteIndented = true – plik czytelny dla człowieka.
• Nazwy pól camelCase? options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
• Własne nazwy pól: [JsonPropertyName("tempC")] nad właściwością.
• Daty: trzymaj jeden format (tu DateTimeOffset bezpieczniejszy niż DateTime).


4) Wersje asynchroniczne – szczególnie ważne w .NET MAUI / UI

Plik bywa „wolny”. W aplikacjach z interfejsem nie blokuj wątku UI. Użyj ReadAllTextAsync/WriteAllTextAsync albo strumieni (FileStream) z await.

// ZAPIS ASYNC
await File.WriteAllTextAsync("dane.txt", "Hello async!", new UTF8Encoding(false));

// ODCZYT ASYNC
string txt = await File.ReadAllTextAsync("dane.txt", Encoding.UTF8);
Console.WriteLine(txt);

Ze strumieniem (większe pliki, kontrola buforów):

await using var fs = new FileStream("pogoda.json", FileMode.Create, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(fs, weather, new JsonSerializerOptions{ WriteIndented = true });

await using var fs2 = new FileStream("pogoda.json", FileMode.Open, FileAccess.Read, FileShare.Read);
var loadedWeather = await JsonSerializer.DeserializeAsync<WeatherForecast>(fs2);

5) Gdzie zapisywać pliki – ścieżki w desktopie i MAUI

Najczęstszy błąd początkujących: zapisywanie „obok EXE” albo w C:\. Dziś systemy mają polityki uprawnień – zapisuj do katalogu użytkownika/aplikacji.

Windows (WPF/WinForms):

string folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// np. C:\Users\Uzytkownik\AppData\Local
string path = Path.Combine(folder, "MojaAplikacja", "scores.json");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, json, new UTF8Encoding(false));

.NET MAUI (Android/iOS/Windows/macOS):

using Microsoft.Maui.Storage;

string appDir = FileSystem.AppDataDirectory; // katalog prywatny aplikacji
string path = Path.Combine(appDir, "settings.json");
await File.WriteAllTextAsync(path, json, new UTF8Encoding(false));

Uwagi:
• Zawsze Directory.CreateDirectory(...) przed zapisem.
• Do ścieżek używaj Path.Combine.
• Nie zakładaj, że plik istnieje – sprawdzaj.


6) Obsługa błędów i odporność na realny świat

Pliku może nie być. JSON może być uszkodzony. Użytkownik mógł zamknąć aplikację w połowie zapisu. Pisz defensywnie.

try
{
    if (!File.Exists("scores.json"))
    {
        Console.WriteLine("Brak pliku – tworzę pustą listę.");
    }
    else
    {
        string data = File.ReadAllText("scores.json", Encoding.UTF8);
        var scores = JsonSerializer.Deserialize<List<Score>>(data) ?? new();
        Console.WriteLine($"OK, wczytano {scores.Count} rekordów.");
    }
}
catch (JsonException ex)
{
    Console.WriteLine("Błąd JSON – plik uszkodzony? " + ex.Message);
}
catch (IOException ex)
{
    Console.WriteLine("Błąd dysku/ścieżki: " + ex.Message);
}

Dobre nawyki:
• Twórz kopię zapasową przed nadpisaniem: zapisz do *.tmp, potem File.Replace.
• Waliduj dane po wczytaniu (np. punktów nie ujemnych).
• Loguj błędy – nawet do prostego log.txt.


7) TXT czy JSON? – szybka mapa decyzji

Wybierz TXT, gdy:
• chcesz po prostu zrzucić log albo listę wartości;
• dane są „jednowymiarowe” (np. jedna wartość na linię);
• wymieniasz plik z użytkownikiem „ręcznie” (łatwo edytuje).

Wybierz JSON, gdy:
• masz obiekty z polami i listy obiektów;
• chcesz łatwo mapować z/do klas (serializacja);
• myślisz o API i kompatybilności z innymi aplikacjami.


8) Kilka przydatnych sztuczek (warto znać na lekcji i maturze)

Własne nazwy pól / ignorowanie pól:

public class Person
{
    [JsonPropertyName("fullName")]
    public string Name { get; set; } = "";

    [JsonIgnore]
    public string? SecretNote { get; set; }
}

Formatowanie dat (string, jeśli chcesz mieć kontrolę):

public class Event
{
    public string Title { get; set; } = "";
    public string DateIso { get; set; } = DateTimeOffset.Now.ToString("O"); // ISO 8601
}

Ładowanie „bez wybuchu”, gdy plik nie istnieje:

static T LoadOrDefault<T>(string path, T @default) where T : class
{
    if (!File.Exists(path)) return @default;
    try
    {
        string s = File.ReadAllText(path, Encoding.UTF8);
        return JsonSerializer.Deserialize<T>(s) ?? @default;
    }
    catch { return @default; }
}

CSV jako „bogatszy TXT” (często wystarcza):

string csv = "name;points\nJan;10\nAla;33";
File.WriteAllText("wyniki.csv", csv, new UTF8Encoding(false));

9) Prosty „mini-projekt”: wyniki uczniów (TXT i JSON)

Ta sama funkcjonalność w dwóch wersjach – uczniowie zobaczą różnicę.

TXT – zapis i odczyt:

public record Score(string Name, int Points);

static void SaveTxt(string path, IEnumerable<Score> scores)
{
    var lines = scores.Select(s => $"{s.Name},{s.Points}");
    File.WriteAllLines(path, lines, new UTF8Encoding(false));
}

static List<Score> LoadTxt(string path)
{
    var list = new List<Score>();
    if (!File.Exists(path)) return list;
    foreach (var line in File.ReadAllLines(path, Encoding.UTF8))
    {
        var p = line.Split(',', StringSplitOptions.TrimEntries);
        if (p.Length == 2 && int.TryParse(p[1], out int pts))
            list.Add(new Score(p[0], pts));
    }
    return list;
}

JSON – zapis i odczyt:

static async Task SaveJsonAsync(string path, List<Score> scores)
{
    Directory.CreateDirectory(Path.GetDirectoryName(path)!);
    await using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
    await JsonSerializer.SerializeAsync(fs, scores, new JsonSerializerOptions{ WriteIndented = true });
}

static async Task<List<Score>> LoadJsonAsync(string path)
{
    if (!File.Exists(path)) return new();
    await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
    return await JsonSerializer.DeserializeAsync<List<Score>>(fs) ?? new();
}

10) Aplikacje mobilne (MAUI) vs desktop (WPF) – co się różni w praktyce

W WPF zapiszesz najczęściej do LocalApplicationData (patrz wyżej). W MAUI użyj FileSystem.AppDataDirectory. Na Androidzie/iOS pliki trzymamy w prywatnym katalogu aplikacji – nie zakładaj, że użytkownik „zobaczy” je w systemowym menedżerze plików.

W MAUI zawsze używaj wersji asynchronicznych – przewijanie listy i jednoczesny zapis JSON-a? Da się, jeśli nie blokujesz wątku UI. Pamiętaj też, że w mobilkach ścieżki zmieniają się między platformami – FileSystem.AppDataDirectory wszystko załatwia.

Na desktopie bonus: możesz użyć OpenFileDialog/SaveFileDialog (WPF) i pozwolić użytkownikowi wskazać miejsce zapisu. Na mobilkach – zwykle nie.


11) Podsumowanie – co zabrać „na maturę”

• TXT – najprostszy zapis tekstu; sam pilnujesz formatu.
• JSON – naturalny zapis obiektów i list; łatwa serializacja.
• Zawsze dbaj o UTF-8 i obsługę błędów.
• W aplikacjach z UI – async.
• Zapisuj do katalogu użytkownika/aplikacji, nie do C:\.
• Potrafisz zapisać i odczytać listę obiektów? To realne punkty.


12) Mini-zadania (pod klasę / pod maturę)

  1. TXT → obiekt: Wczytaj dane.txt z liniami „Imię,Punkty”, zmapuj do List<Score>, wypisz TOP-3.
  2. JSON → edycja → JSON: Wczytaj scores.json, dodaj nowy rekord i zapisz ładnie sformatowany (WriteIndented).
  3. Asynchroniczność: Zaimplementuj SaveJsonAsync/LoadJsonAsync w MAUI (użyj FileSystem.AppDataDirectory).
  4. Odporność: Zabezpiecz odczyt przed uszkodzonym JSON – w razie błędu zwróć pustą listę i zaloguj błąd.
  5. Własne nazwy pól: Zmień nazwy właściwości w JSON za pomocą [JsonPropertyName].
  6. Daty: Zapisz listę zdarzeń z datą ISO 8601, wczytaj i posortuj po dacie malejąco.