Lekcja 15. Praca z plikami w aplikacji WPF (zapis i odczyt danych)
Trudny / egzaminacyjnyPo co się tego uczymy?
Dane trzymane tylko w pamięci (w kolekcji z poprzedniej lekcji) znikają, gdy zamkniesz aplikację. Żeby dane PRZETRWAŁY między uruchomieniami programu, trzeba je zapisać na dysku — do pliku tekstowego, JSON, albo (w kolejnej lekcji) do bazy danych. Ta umiejętność łączy WPF z tym, czego nauczyłeś się już w dziale programowania konsolowego o pracy z plikami.
Teoria
Zasady pracy z plikami w WPF są dokładnie takie same, jak w aplikacji konsolowej — klasy System.IO.File (do prostych operacji: WriteAllText, ReadAllText) oraz System.Text.Json (do zapisu/odczytu całych obiektów jako JSON) działają identycznie. Różnica polega tylko na TYM, KIEDY wywołujemy zapis/odczyt — zwykle: odczyt danych przy starcie okna (w konstruktorze albo zdarzeniu Loaded), a zapis przy zamykaniu okna (zdarzenie Closing) albo po każdej zmianie danych (np. po dodaniu elementu do listy).
WPF dodaje też własne, wygodne okna dialogowe do wyboru pliku przez użytkownika: OpenFileDialog (wybór pliku do otwarcia) i SaveFileDialog (wybór miejsca zapisu) z przestrzeni nazw Microsoft.Win32 — pokazują znajomy, systemowy eksplorator plików Windows, więc użytkownik czuje się jak w każdej innej aplikacji.
Typowy przepływ pracy: użytkownik klika "Zapisz jako" → otwiera się SaveFileDialog → program pobiera wybraną ścieżkę (dialog.FileName) → zapisuje dane pod tą ścieżką metodą File.WriteAllText (albo serializując do JSON).
Przykład z życia
Aplikacja notatnika: przy zamykaniu programu (zdarzenie Closing głównego okna) automatycznie zapisuje treść notatek do pliku na dysku, a przy następnym uruchomieniu (konstruktor okna) wczytuje tę treść z powrotem — użytkownik ma wrażenie, że aplikacja "pamięta" jego pracę, choć tak naprawdę za każdym razem od nowa odczytuje ją z pliku.
Kod (C#)
using System.IO;
using System.Text.Json;
using System.Windows;
using Microsoft.Win32;
using BibliotekaApp.Modele;
using System.Collections.ObjectModel;
namespace BibliotekaApp
{
public partial class MainWindow : Window
{
private const string PlikDanych = "ksiazki.json";
public ObservableCollection<Ksiazka> Ksiazki { get; set; } = new();
public MainWindow()
{
InitializeComponent();
WczytajDane();
ListaKsiazek.ItemsSource = Ksiazki;
// Zapisz dane automatycznie przy zamykaniu okna
this.Closing += (s, e) => ZapiszDane();
}
private void WczytajDane()
{
if (!File.Exists(PlikDanych))
{
return; // pierwsze uruchomienie - jeszcze nie ma pliku
}
string json = File.ReadAllText(PlikDanych);
var wczytane = JsonSerializer.Deserialize<Ksiazka[]>(json);
if (wczytane != null)
{
foreach (var k in wczytane)
{
Ksiazki.Add(k);
}
}
}
private void ZapiszDane()
{
string json = JsonSerializer.Serialize(Ksiazki, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(PlikDanych, json);
}
private void PrzyciskEksportuj_Click(object sender, RoutedEventArgs e)
{
var dialog = new SaveFileDialog
{
Filter = "Pliki tekstowe (*.txt)|*.txt",
FileName = "eksport_ksiazek.txt"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, string.Join("n", Ksiazki));
}
}
}
}
Komentarz i wyjaśnienie kodu
WczytajDane sprawdza najpierw, czy plik w ogóle istnieje (File.Exists) — przy pierwszym uruchomieniu aplikacji plik jeszcze nie istnieje, więc metoda po prostu kończy działanie bez błędu. Jeśli plik istnieje, odczytuje jego zawartość jako tekst i zamienia go z powrotem na tablicę obiektów Ksiazka przez JsonSerializer.Deserialize.
Konstruktor okna podpina this.Closing += (s, e) => ZapiszDane(); — to zapisuje dane automatycznie za każdym razem, gdy użytkownik zamyka okno, bez potrzeby klikania osobnego przycisku "Zapisz".
PrzyciskEksportuj_Click pokazuje użycie SaveFileDialog: tworzymy go z filtrem typów plików i domyślną nazwą, wywołujemy ShowDialog() (to okno MODALNE, program czeka na wybór użytkownika — pamiętasz to z lekcji o oknach?), i dopiero jeśli użytkownik kliknął "Zapisz" (wynik true), zapisujemy dane pod wybraną przez niego ścieżką.
Ćwiczenie samodzielne
Dodaj do aplikacji z listą książek (z poprzedniej lekcji) automatyczny zapis do pliku JSON przy zamykaniu okna i odczyt przy starcie, dokładnie jak w przykładzie. Zamknij i uruchom aplikację ponownie — sprawdź, czy dane "przetrwały".
Zadania do pracy własnej
Dodaj przycisk "Zapisz teraz", który wywołuje
ZapiszDane()ręcznie, niezależnie od automatycznego zapisu przy zamykaniu — przydatne, gdyby aplikacja "zawiesiła się" i użytkownik nie zdążył jej normalnie zamknąć.Dodaj przycisk "Importuj z pliku" wykorzystujący
OpenFileDialogdo wyboru pliku JSON i wczytania z niego dodatkowych książek (dopisywanych do istniejącej listy, nie zastępujących jej).Zbuduj prosty edytor notatek (jedno duże pole
TextBoxzAcceptsReturn="True") z menu "Plik" zawierającym "Nowy", "Otwórz..." (OpenFileDialog) i "Zapisz jako..." (SaveFileDialog) — dokładnie jak w prawdziwym Notatniku Windows, z obsługą plików tekstowych.
Typowe błędy
Brak sprawdzenia File.Exists przed odczytem — próba odczytu nieistniejącego pliku przy pierwszym uruchomieniu aplikacji spowoduje wyjątek FileNotFoundException i awarię programu.
Zapominanie o sprawdzeniu wyniku ShowDialog() dla SaveFileDialog/OpenFileDialog — jeśli użytkownik kliknie "Anuluj", dialog.FileName będzie puste albo nieaktualne; zawsze trzeba sprawdzić, czy wynik to true, zanim użyjemy wybranej ścieżki.
Zapisywanie danych PRZED zamknięciem zamiast W TRAKCIE zamykania — jeśli zapis jest podpięty pod zły przycisk albo w ogóle nie jest wywoływany automatycznie, użytkownik może stracić dane, jeśli zamknie okno "krzyżykiem" bez kliknięcia "Zapisz".
Nawiązanie do egzaminu zawodowego
To bezpośrednie zastosowanie umiejętności z lekcji "Praca z plikami (TXT i JSON)" z działu programowania konsolowego, tym razem w kontekście prawdziwej aplikacji okienkowej — realizuje fragment INF.04.5.3 dotyczący budowy kompletnej, użytecznej aplikacji desktopowej.