Aplikacje desktopowe · Pliki

Praca z plikami w WPF: zapis i odczyt danych

Dane trzymane tylko w pamięci znikają po zamknięciu programu. Żeby przetrwały między uruchomieniami, trzeba je zapisać na dysku. Nauczysz się robić to w aplikacji okienkowej: odczyt przy starcie, zapis przy zamykaniu i okna wyboru pliku.

  • około 50 minut
  • WPF · File · JSON · SaveFileDialog
  • INF.04.5.3

Po tej lekcji: wczytać dane przy starcie i zapisać je w zdarzeniu Closing, sprawdzić File.Exists, użyć SaveFileDialog i zadbać o poprawną ścieżkę pliku.

Zacznijmy od notatnika

Aplikacja, która „pamięta”

Notatnik zapisuje treść przy zamknięciu i wczytuje ją przy następnym uruchomieniu. Użytkownik ma wrażenie, że program pamięta jego pracę, ale naprawdę za każdym razem odczytuje ją z pliku.

Zasady pracy z plikami w WPF są takie same jak w aplikacji konsolowej: używasz File.ReadAllText, File.WriteAllText i System.Text.Json. Zmienia się tylko to, kiedy je wywołujesz: odczyt przy starcie okna, zapis przy jego zamykaniu albo po każdej zmianie.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Sprawdź w działaniu

Kiedy dane przetrwają

Uruchom program, dodaj kilka książek, zamknij okno i uruchom program ponownie. Potem wyłącz zapis w Closing i powtórz: dane przepadną. Wyłącz też File.Exists na świeżym starcie, aby zobaczyć awarię.

Ta interakcja wymaga włączonego JavaScriptu.

Dane są bezpieczne dopiero wtedy, gdy trafią na dysk. Wszystko, co zostało tylko w pamięci, znika po zamknięciu programu.

2 · Zapis i odczyt

MainWindow.xaml.cs

Trzy zadania: wczytać dane przy starcie, zapisać przy zamykaniu i pozwolić użytkownikowi wyeksportować je do wybranego pliku.

MainWindow.xaml.cscsharp
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
using BibliotekaApp.Modele;
using Microsoft.Win32;

namespace BibliotekaApp;

public partial class MainWindow : Window
{
    // Plik obok programu (a nie w „bieżącym katalogu”, który bywa inny)
    private static readonly string PlikDanych = Path.Combine(AppContext.BaseDirectory, "ksiazki.json");

    public ObservableCollection<Ksiazka> Ksiazki { get; set; } = new();

    public MainWindow()
    {
        InitializeComponent();
        WczytajDane();
        ListaKsiazek.ItemsSource = Ksiazki;

        // Zapisz dane automatycznie przy zamykaniu okna
        Closing += (s, e) => ZapiszDane();
    }

    private void WczytajDane()
    {
        if (!File.Exists(PlikDanych))
        {
            return;   // pierwsze uruchomienie: pliku jeszcze nie ma
        }

        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 PrzyciskZapisz_Click(object sender, RoutedEventArgs e)
    {
        ZapiszDane();
    }

    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)
        {
            string tekst = string.Join("\n", Ksiazki.Select(k => $"{k.Tytul};{k.Autor};{k.Status}"));
            File.WriteAllText(dialog.FileName, tekst);
        }
    }
}
  1. Path.Combine(AppContext.BaseDirectory, "ksiazki.json") Plik leży obok programu. Sama nazwa "ksiazki.json" oznacza „bieżący katalog”, który zależy od sposobu uruchomienia i bywa inny niż myślisz.
  2. File.Exists(PlikDanych) Przy pierwszym uruchomieniu pliku jeszcze nie ma, więc metoda kończy się bez błędu.
  3. JsonSerializer.Deserialize<Ksiazka[]>(json) Zamienia tekst JSON z powrotem na tablicę obiektów. Wynik może być null, więc sprawdzamy go.
  4. Closing += (s, e) => ZapiszDane() Zapis uruchamia się za każdym razem, gdy okno jest zamykane, nawet krzyżykiem.
  5. WriteIndented = true Plik JSON ma czytelne wcięcia, dzięki czemu można go obejrzeć w edytorze.
  6. dialog.ShowDialog() == true Okno wyboru pliku jest modalne. Ścieżkę używamy dopiero po kliknięciu „Zapisz”, a nie „Anuluj”.
  7. string.Join("\n", ...) Łączy linie znakiem nowej linii. \n w cudzysłowie to znak końca linii, a samo n byłoby zwykłą literą.

3 · Wygląd

MainWindow.xaml

Tabela z książkami i dwa przyciski: ręczny zapis oraz eksport do pliku tekstowego.

MainWindow.xamlxaml
<Window x:Class="BibliotekaApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Biblioteka" Height="380" Width="460">
    <DockPanel Margin="15">
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,10,0,0">
            <Button Content="Zapisz teraz" Click="PrzyciskZapisz_Click" Margin="0,0,8,0" />
            <Button Content="Eksportuj do TXT" Click="PrzyciskEksportuj_Click" />
        </StackPanel>
        <DataGrid x:Name="ListaKsiazek" AutoGenerateColumns="True" IsReadOnly="True" />
    </DockPanel>
</Window>
  1. Click="PrzyciskZapisz_Click" Ręczny zapis na wypadek awarii przed zamknięciem okna.

4 · Ściągawka

Najważniejsze klasy i metody

ZadanieZapis w C#Uwagi
Czy plik istniejeFile.Exists(sciezka)Zawsze przed odczytem.
Odczyt całego tekstuFile.ReadAllText(sciezka)Zwraca string.
Zapis całego tekstuFile.WriteAllText(sciezka, tekst)Nadpisuje plik.
Obiekt → JSONJsonSerializer.Serialize(obiekt)Przestrzeń System.Text.Json.
JSON → obiektJsonSerializer.Deserialize<T>(json)Wynik bywa null.
Wybór miejsca zapisunew SaveFileDialog().ShowDialog()Microsoft.Win32, sprawdź wynik == true.
Wybór pliku do otwarcianew OpenFileDialog().ShowDialog()Ścieżka w dialog.FileName.
To te same klasy co w aplikacjach konsolowych. Nowe są tylko okna dialogowe wyboru pliku.

5 · Czego unikać

Cztery błędy z plikami

Odczyt bez `File.Exists`
Tak nie
string json = File.ReadAllText(PlikDanych);

Przy pierwszym uruchomieniu: FileNotFoundException i awaria programu.

Tak
if (!File.Exists(PlikDanych)) return;
string json = File.ReadAllText(PlikDanych);

Pierwszego uruchomienia nie poprzedza żaden zapis, więc pliku nie ma.

Ścieżka względna zależna od miejsca uruchomienia
Tak nie
const string PlikDanych = "ksiazki.json";

Plik ląduje w innym katalogu, zależnie od sposobu uruchomienia, i dane „znikają”.

Tak
static readonly string PlikDanych = Path.Combine(AppContext.BaseDirectory, "ksiazki.json");

Jawna ścieżka obok programu jest taka sama za każdym razem.

Ignorowanie wyniku `ShowDialog()`
Tak nie
dialog.ShowDialog();
File.WriteAllText(dialog.FileName, tekst);

Po kliknięciu „Anuluj” FileName jest puste, więc zapis kończy się błędem.

Tak
if (dialog.ShowDialog() == true)
{
    File.WriteAllText(dialog.FileName, tekst);
}

Wynik true oznacza, że użytkownik wybrał plik i zatwierdził.

Zapis tylko po kliknięciu przycisku
Tak nie
// zapis tylko w PrzyciskZapisz_Click

Użytkownik zamyka okno krzyżykiem bez klikania „Zapisz” i traci dane.

Tak
Closing += (s, e) => ZapiszDane();

Automatyczny zapis przy zamykaniu chroni przed utratą pracy.

6 · Sprawdź się

Kiedy odczyt, a kiedy zapis?

Cztery sytuacje z plikami.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Dane z pamięci znikają po zamknięciu programu. Żeby przetrwały, zapisz je na dysku.
  2. Odczyt robisz przy starcie okna, zapis przy zamykaniu (Closing) lub po zmianie danych.
  3. Przed odczytem sprawdzaj File.Exists, bo przy pierwszym uruchomieniu pliku nie ma.
  4. Ścieżkę buduj z AppContext.BaseDirectory, a nie z samej nazwy pliku.
  5. Wynik SaveFileDialog i OpenFileDialog sprawdzaj jako == true.

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