Grafikus C# – Windows Forms tanfolyam › 12.8 lecke

Windows Forms · 12.8 lecke · OOP

Felület és adatkezelés különválasztása

A 12.7 leckében a számítást kiemelted a Formból. Van még egy dolog, amit a Form nem tehet: közvetlenül fájlokat írni és olvasni. Ha a Form a File.WriteAllText hívást és a JSON formátumot is ismeri, akkor a fájlformátum módosítása (JSON helyett XML, vagy adatbázis) a Form minden pontját érinti, és mentés nélkül nem próbálható ki a program. A megoldás egy tároló osztály (repository), amely a mentést és betöltést végzi, és egy interface, amely megmondja, mit tud a tároló, anélkül hogy megmondaná, hogyan. Ebben a leckében megtanulod az interface használatát, a több megvalósítást (fájl és memória), a függőség konstruktoron át történő átadását, a hibakezelést és az aszinkron mentést.

  • kb. 110 perc
  • haladó szint
  • tároló és interface
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

A lecke végére

  • tároló osztályt írsz, amely elrejti a mentés és betöltés részleteit;
  • interface-t definiálsz és valósítasz meg, és érted, mire jó a szerződés;
  • két tárolót (JSON fájl, memória) cserélsz anélkül, hogy a program többi része változna;
  • a függőséget a konstruktoron át adod át (dependency injection, egyszerűen), és a Program.cs-ben állítod össze;
  • a tároló hibáit saját kivétellel jelzed, és a Form érthetően jeleníti meg;
  • a mentést aszinkron végzed, és a változások nyomon követésével kerülöd a fölösleges mentést.

Mielőtt elkezded

Ehhez a leckéhez a 12.1–12.7 lecke, a 10.6 lecke (JSON), a 10.9 lecke (mentési hibák) és a 11.5 lecke (async) szükséges.

12.7 Programlogika →

A tároló osztály

Form1.cs: ELŐTTE (a Form ismeri a fájlt és a formátumot)C#
private void btnMent_Click(object sender, EventArgs e)
{
string json = JsonSerializer.Serialize(feladatok);
File.WriteAllText("feladatok.json", json); // útvonal, formátum, hibakezelés: mind a Formban
}
JsonFeladatTarolo.cs: UTÁNA (a részlet egy osztályban)C#
// a lemezre kerülő forma: egyszerű, nyilvános tulajdonságokkal (a modell és a tárolt forma szétválik)
public class FeladatAdat
{
public string Cim { get; set; } = "";
public DateTime? Hatarido { get; set; }
public Fontossag Fontos { get; set; }
public bool Kesz { get; set; }
}

public class JsonFeladatTarolo
{
private readonly string ut;

public JsonFeladatTarolo(string ut) { this.ut = ut; }

public List<Feladat> Betolt()
{
if (!File.Exists(ut)) { return new List<Feladat>(); } // első indítás: üres lista, nem hiba

string json = File.ReadAllText(ut, Encoding.UTF8);
List<FeladatAdat> adatok = JsonSerializer.Deserialize<List<FeladatAdat>>(json) ?? new List<FeladatAdat>();

List<Feladat> lista = new List<Feladat>();

foreach (FeladatAdat a in adatok)
{
Feladat f = new Feladat(a.Cim) { Hatarido = a.Hatarido, Fontos = a.Fontos };

if (a.Kesz) { f.Elvegez(); }
lista.Add(f);
}

return lista;
}

public void Ment(IEnumerable<Feladat> feladatok)
{
List<FeladatAdat> adatok = feladatok.Select(f => new FeladatAdat { Cim = f.Cim, Hatarido = f.Hatarido, Fontos = f.Fontos, Kesz = f.Kesz }).ToList();

string json = JsonSerializer.Serialize(adatok, new JsonSerializerOptions { WriteIndented = true });

string ideiglenes = ut + ".tmp";

File.WriteAllText(ideiglenes, json, new UTF8Encoding(false)); // előbb ideiglenes fájlba (10.9 lecke)

if (File.Exists(ut)) { File.Delete(ut); }

File.Move(ideiglenes, ut); // majd átnevezés: a félbeszakadt mentés nem rontja el a régi fájlt
}
}
Mit tud a tároló?Csak két dolgot: Betolt() és Ment(). Ismeri a fájl útvonalát és a formátumot, de nem ismeri a felületet és a számítási szabályokat.
Adat-osztály (DTO)A FeladatAdat a lemezre kerülő, egyszerű, nyilvános tulajdonságú forma. A Feladat modell védett (private set, ellenőrzés): a tároló a kettő között fordít. Így a fájlformátum változása nem rontja el a modell védelmét.
Első indításHa a fájl még nem létezik, az üres lista a helyes válasz (nem hiba): a program első indításakor nincs mit betölteni.
Biztonságos mentésAz ideiglenes fájlba írás és átnevezés megvédi a régi adatot, ha a mentés közben megszakad az áram (10.9 lecke). A File.Replace atomi cserét ad (Windowsos, azonos meghajtón).

Jósolj!

A Feladat osztály Cim tulajdonságának private set setterje van, és a JsonSerializer.Deserialize<List<Feladat>> közvetlenül a modellt töltené be. Mit tesz a System.Text.Json?

Jóslat ellenőrzése

Alapból nem állítja be a privát setterű tulajdonságokat (és paraméteres konstruktorral is csak egyeztetett nevek esetén dolgozik): a Cim üres maradna. Ezért jó a külön FeladatAdat: a tárolt forma szabadon szerializálható, a modell védett.

Interface: szerződés a megvalósítás helyett

IFeladatTarolo.cs és megvalósításokC#
// az INTERFACE: megmondja, MIT tud a tároló (nincs benne kód)
public interface IFeladatTarolo
{
List<Feladat> Betolt();
void Ment(IEnumerable<Feladat> feladatok);
}

// 1. megvalósítás: fájlba ment
public class JsonFeladatTarolo : IFeladatTarolo { /* Betolt, Ment: lásd fent */ }

// 2. megvalósítás: csak a memóriában tart (teszthez, vagy ideiglenes használathoz)
public class MemoriaFeladatTarolo : IFeladatTarolo
{
private List<FeladatAdat> adatok = new List<FeladatAdat>();

public int MentesekSzama { get; private set; } // a teszt ellenőrizheti, hányszor mentettek

public List<Feladat> Betolt()
{
return adatok.Select(a => Fordit(a)).ToList();
}

public void Ment(IEnumerable<Feladat> feladatok)
{
adatok = feladatok.Select(f => new FeladatAdat { Cim = f.Cim, Hatarido = f.Hatarido, Fontos = f.Fontos, Kesz = f.Kesz }).ToList();
MentesekSzama++;
}

private static Feladat Fordit(FeladatAdat a)
{
Feladat f = new Feladat(a.Cim) { Hatarido = a.Hatarido, Fontos = a.Fontos };

if (a.Kesz) { f.Elvegez(); }

return f;
}
}
Mi az interface?Tisztán szerződés: tagok listája (metódusok, tulajdonságok) törzs nélkül. Az osztály, amely megvalósítja (: IFeladatTarolo), vállalja, hogy minden tagot megírja. A neve konvenció szerint I-vel kezdődik.
Mire jó?Aki az interface-t használja, nem tudja és nem is kell tudnia, melyik megvalósítás áll mögötte. Fájlról adatbázisra váltáskor új osztályt írsz, a használó kód változatlan.
TesztelhetőségA MemoriaFeladatTarolo lemez nélkül működik: a tesztek gyorsak, nem hagynak fájlt, és nem függnek a mappa jogosultságaitól. A MentesekSzama megmondja, hányszor mentettek.
Mikor kell?Ha egy dolognak több lehetséges megvalósítása van (fájl, adatbázis, memória), vagy ki kell cserélni teszteléshez. Egyetlen, soha nem változó megvalósításnál az interface felesleges bonyodalom.

Jósolj!

Az IFeladatTarolo interface-hez egy új metódust adsz (void Torol()), de a MemoriaFeladatTarolo osztályban nem írod meg. Mi történik?

Jóslat ellenőrzése

Fordítási hiba (CS0535: a MemoriaFeladatTarolo nem valósítja meg az interface Torol() tagját). A fordító kikényszeríti a szerződést: minden megvalósításnak teljesen kell követnie.

A függőség átadása és az összeállítás

FeladatLista.cs, Form1.cs, Program.csC#
// A FELADATLISTA (szolgáltatás): az interface-t kapja, nem a konkrét tárolót
public class FeladatLista
{
private readonly IFeladatTarolo tarolo;
private readonly List<Feladat> feladatok = new List<Feladat>();

public bool Modositott { get; private set; }
public IReadOnlyList<Feladat> Elemek { get { return feladatok; } }
public event EventHandler Valtozott;

public FeladatLista(IFeladatTarolo tarolo) // KONSTRUKTOR-injektálás: kívülről kapja
{
if (tarolo == null) { throw new ArgumentNullException("tarolo"); }

this.tarolo = tarolo;
}

public void Betolt() { feladatok.Clear(); feladatok.AddRange(tarolo.Betolt()); Modositott = false; Jelez(); }
public void Ment() { tarolo.Ment(feladatok); Modositott = false; }

public void Hozzaad(Feladat f) { feladatok.Add(f); Modositott = true; Jelez(); }
public void Torol(Feladat f) { if (feladatok.Remove(f)) { Modositott = true; Jelez(); } }

private void Jelez() { Valtozott?.Invoke(this, EventArgs.Empty); }
}

// A FORM: a szolgáltatást kapja a konstruktorban
public partial class Form1 : Form
{
private readonly FeladatLista lista;

public Form1(FeladatLista lista)
{
InitializeComponent();

this.lista = lista;
lista.Valtozott += (s, e) => Frissit();
}
}

// A PROGRAM.CS: itt áll össze minden ("composition root"): ez az EGYETLEN hely, ahol a konkrét osztályt választod
static void Main()
{
Application.EnableVisualStyles();

string ut = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Feladatlista", "feladatok.json");
Directory.CreateDirectory(Path.GetDirectoryName(ut));

IFeladatTarolo tarolo = new JsonFeladatTarolo(ut); // itt cserélhető: new MemoriaFeladatTarolo()
FeladatLista lista = new FeladatLista(tarolo);

Application.Run(new Form1(lista));
}
Dependency injectionAz osztály nem maga hozza létre a függőségét (new JsonFeladatTarolo()), hanem kívülről kapja (a konstruktorban). Így a használó osztály nem függ a konkrét megvalósítástól.
Composition rootEgy helyen (itt: Main) áll össze az alkalmazás: te választod a tárolót, a szolgáltatást és a Formot. Ha tárolót cserélsz, csak ezt a sort írod át.
Modositott jelzőA Modositott jelzi, hogy van mentetlen változás: a FormClosing rákérdezhet (Mented?), és a Mentés gomb tiltható, ha nincs mit menteni.
Esemény a változásrólA Valtozott esemény értesíti a Formot: a lista frissítése így egy helyen van, nem minden módosítás után külön. A szolgáltatás nem ismeri a Formot (laza csatolás, 12.4 lecke).

Jósolj!

A FeladatLista konstruktora new JsonFeladatTarolo("feladatok.json") hívással maga hozza létre a tárolót. Mit nem tudsz megtenni?

Jóslat ellenőrzése

Nem tudod lemez nélkül tesztelni a FeladatLista-t (mindig a valódi fájlt írja), és nem tudod cserélni a tárolót anélkül, hogy a FeladatLista kódját átírnád. A kívülről kapott függőség mindkettőt megoldja.

Hibák és aszinkron mentés

Hibakezelés a tárolóban és a FormbanC#
// saját kivétel: a Form ne ismerje a JsonException, IOException típusokat
public class TaroloHiba : Exception
{
public TaroloHiba(string uzenet, Exception ok) : base(uzenet, ok) { }
}

// a tárolóban: a technikai hibát a felhasználónak szóló üzenetté alakítjuk
public List<Feladat> Betolt()
{
try { /* olvasás és fordítás */ }
catch (JsonException ex) { throw new TaroloHiba("A feladatfájl sérült, nem olvasható.", ex); }
catch (UnauthorizedAccessException ex) { throw new TaroloHiba("Nincs jogosultság a feladatfájlhoz.", ex); }
catch (IOException ex) { throw new TaroloHiba("A feladatfájl nem érhető el: " + ex.Message, ex); }
}

// a Formban: egyetlen catch, érthető üzenet; az eredeti kivétel az InnerException-ben megmarad a naplóhoz
private async void btnMent_Click(object sender, EventArgs e)
{
btnMent.Enabled = false;

try
{
await Task.Run(() => lista.Ment()); // a mentés a háttérben: a felület nem akad (11. fejezet)

tsslUzenet.Text = "Mentve: " + DateTime.Now.ToString("HH:mm:ss");
}
catch (TaroloHiba ex)
{
tsslUzenet.Text = ex.Message;
MessageBox.Show(this, ex.Message, "Mentési hiba", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
finally { btnMent.Enabled = lista.Modositott; }
}
Saját kivételtípusA TaroloHiba elrejti, hogy fájl, adatbázis vagy hálózat van-e mögötte. A Form egyetlen típust kezel. Az eredeti kivétel (InnerException) a naplóhoz megmarad.
Aszinkron mentésA nagy fájl mentése megakaszthatja a felületet: await Task.Run(...) a háttérbe viszi (11.5 lecke). Figyelj: mentés közben a lista ne módosuljon (letiltott szerkesztés, vagy a mentés másolatot kap).
Mentés közbeni módosításHa a felhasználó a mentés alatt módosít, a háttérszál és a felület ugyanazt a listát éri el (11.7 lecke). A legegyszerűbb: a mentés idejére a felület szerkesztő vezérlői tiltva, vagy a Ment a lista másolatával dolgozik.
Hová ment?A felhasználói adatok helye Environment.SpecialFolder.ApplicationData (felhasználónként), nem a program mappája (a Program Files alá írni nem szabad).

Jósolj!

A tároló a JsonException-t nem alakítja át, és a Form csak TaroloHiba-t kap el. A feladatfájl sérült. Mi történik a program indításakor?

Jóslat ellenőrzése

A JsonException kezeletlenül kilép: a program összeomlik (kezeletlen kivétel ablak). Ezért kell a tárolónak a technikai kivételeket a saját típusára alakítania, és a Formnak minden indításkori betöltést try–catch blokkba tennie.

📷 Képernyőkép helye

Mit tegyél ide: a feladatlista, a tároló cseréjével (JSON és memória) és a mentési hiba üzenetével.Javasolt fájlnév: winforms-12-8-tarolo.webp · Alt szöveg: A feladatlista cserélhető tárolóval

Hibakeresés: te vagy a hibadetektív

Mi a baj, és hogyan javítod?

Gondold végig az okot, és csak utána nyisd le a megoldást.

1. A betöltött feladatok mind üres címűek

JsonSerializer.Deserialize<List<Feladat>>(json);   // a modell privát setterekkel
Hiba és javítás

A System.Text.Json nem állítja be a privát settereket. Használj külön adat-osztályt (FeladatAdat), és fordítsd át a modellre.

2. CS0535: nincs megvalósítva az interface tagja

Az új tároló osztály nem fordul.

Ok és javítás

Az osztály nem írta meg az interface minden tagját. Visual Studio: Ctrl+. → Implement interface.

3. A program összeomlik sérült fájlnál

A JsonException kezeletlenül kilép.

Ok és javítás

A tároló alakítsa TaroloHiba típusra, a Form pedig kapja el a betöltést. Sérült fájlnál ajánlj felhasználói döntést (üres lista, vagy a mentett másolat visszaállítása: 10.9 lecke).

4. A teszt a lemezt módosítja

public FeladatLista() { tarolo = new JsonFeladatTarolo("feladatok.json"); }
Hiba és javítás

A függőség beégetett. Add át a konstruktorban (IFeladatTarolo), és a tesztben MemoriaFeladatTarolo-t használj.

5. A mentés közben kivétel a lista módosításakor

Az InvalidOperationException jelenik meg: a gyűjtemény módosult.

Ok és javítás

A háttérszál a listát járja be, a felület módosítja. Tiltsd a szerkesztést mentés közben, vagy mentés előtt másolatot készíts (feladatok.ToList()).

6. A mentett fájl mindig elveszik a program mappájából

Telepítés után a mentés UnauthorizedAccessException-t dob.

Ok és javítás

A program a saját mappájába ír (Program Files: nincs írási jog). Használd az ApplicationData mappát.

Mini projekt: feladatlista cserélhető tárolóval

Készíts feladatlistát, amelynek a tárolása interface mögött van, és két tárolóval is működik:

  1. Modell: a Feladat (12.6 lecke: Cim, Hatarido, Fontos, Kesz, Elvegez(), Lejart(most)).
  2. FeladatAdat (DTO), és az interface: IFeladatTarolo (Betolt, Ment).
  3. JsonFeladatTarolo: JSON fájl, ideiglenes fájllal és átnevezéssel; az első indításnál üres lista; a hibák TaroloHibavá alakítva; sérült fájl esetén a régi mentés (.bak) megőrzése.
  4. MemoriaFeladatTarolo: a MentesekSzama számlálóval.
  5. FeladatLista szolgáltatás: konstruktorban kapja a tárolót; Hozzaad, Torol, Elvegez, Betolt, Ment, Modositott, Valtozott esemény; a lejártak száma (LejartDb(most)).
  6. Form: lstFeladatok (CheckedListBox vagy ListBox), txtCim, dtpHatarido, Hozzáad, Kész, Törlés, Mentés (aszinkron); lblLejart; a FormClosing mentetlen változásnál rákérdez (Mented?).
  7. Program.cs: a cmbTarolo helyett a parancssori kapcsoló (--memoria) vagy egy konstans dönti el, melyik tárolót adja át; a Formban nincs new JsonFeladatTarolo.
  8. Tesztek (MemoriaFeladatTarolo segítségével, lemez nélkül): hozzáadás után Modositott igaz, mentés után hamis; a Ment és újra Betolt ugyanazokat a feladatokat adja (cím, kész, fontosság); MentesekSzama; üres lista; a Torol nem létező elemre nem módosít.
  9. Teszteld: a program újraindítása után a feladatok megvannak; sérült JSON (kézzel elrontva); olvasásvédett fájl; a tároló cseréje (nincs változás a Formban); a bezárás mentetlen változással.
Megoldás megnyitása (próbáld ki előbb önállóan!)
FeladatLista.cs, tesztek és Form1.cs (using System.Collections.Generic; using System.Linq;)C#
// A JsonFeladatTarolo, MemoriaFeladatTarolo, FeladatAdat, IFeladatTarolo, TaroloHiba a leckében van.
// A JsonFeladatTarolo.Ment kiegészítése a biztonsági másolattal:
public void Ment(IEnumerable<Feladat> feladatok)
{
try
{
List<FeladatAdat> adatok = feladatok.Select(f => new FeladatAdat { Cim = f.Cim, Hatarido = f.Hatarido, Fontos = f.Fontos, Kesz = f.Kesz }).ToList();
string json = JsonSerializer.Serialize(adatok, new JsonSerializerOptions { WriteIndented = true });
string tmp = ut + ".tmp";

File.WriteAllText(tmp, json, new UTF8Encoding(false));

if (File.Exists(ut)) { File.Copy(ut, ut + ".bak", true); File.Delete(ut); }

File.Move(tmp, ut);
}
catch (UnauthorizedAccessException ex) { throw new TaroloHiba("Nincs jogosultság a mentéshez.", ex); }
catch (IOException ex) { throw new TaroloHiba("A mentés nem sikerült: " + ex.Message, ex); }
}

// FeladatLista: lásd a leckében, kiegészítve:
public void Elvegez(Feladat f) { f.Elvegez(); Modositott = true; Jelez(); }
public int LejartDb(DateTime most) { return feladatok.Count(f => f.Lejart(most)); }

// ---- Tesztek (lemez nélkül) ----
public static class Tesztek
{
private static int hibak;

private static void Igaz(bool f, string nev)
{
if (!f) { hibak++; System.Diagnostics.Debug.WriteLine("HIBA " + nev); }
else { System.Diagnostics.Debug.WriteLine("ok " + nev); }
}

public static int Mindet()
{
hibak = 0;

MemoriaFeladatTarolo tarolo = new MemoriaFeladatTarolo();
FeladatLista lista = new FeladatLista(tarolo);

Igaz(!lista.Modositott, "új lista nem módosított");

lista.Hozzaad(new Feladat("Házi feladat") { Fontos = Fontossag.Magas });
lista.Hozzaad(new Feladat("Bevásárlás"));

Igaz(lista.Modositott, "hozzáadás után módosított");

lista.Elvegez(lista.Elemek[1]);
lista.Ment();

Igaz(!lista.Modositott, "mentés után nem módosított");
Igaz(tarolo.MentesekSzama == 1, "egyszer mentettek");

FeladatLista masik = new FeladatLista(tarolo);

masik.Betolt();

Igaz(masik.Elemek.Count == 2, "két feladat töltődött vissza");
Igaz(masik.Elemek[0].Cim == "Házi feladat" && masik.Elemek[0].Fontos == Fontossag.Magas, "az első feladat adatai");
Igaz(masik.Elemek[1].Kesz, "a kész jelző megmaradt");

masik.Torol(new Feladat("nem létezik"));

Igaz(!masik.Modositott, "nem létező törlés nem módosít");

return hibak;
}
}

// ---- Form1.cs (részlet) ----
private readonly FeladatLista lista;

public Form1(FeladatLista lista)
{
InitializeComponent();

this.lista = lista;
lista.Valtozott += (s, e) => Frissit();

try { lista.Betolt(); }
catch (TaroloHiba ex) { tsslUzenet.Text = ex.Message; }
}

private void Frissit()
{
lstFeladatok.Items.Clear();

foreach (Feladat f in lista.Elemek)
{
lstFeladatok.Items.Add((f.Kesz ? "✔ " : f.Lejart(DateTime.Now) ? "⚠ " : " ") + f.Cim, f.Kesz);
}

lblLejart.Text = "Lejárt: " + lista.LejartDb(DateTime.Now);
btnMent.Enabled = lista.Modositott;
}

private void btnHozzaad_Click(object sender, EventArgs e)
{
try { lista.Hozzaad(new Feladat(txtCim.Text) { Hatarido = dtpHatarido.Checked ? (DateTime?)dtpHatarido.Value : null }); txtCim.Clear(); }
catch (ArgumentException ex) { tsslUzenet.Text = ex.Message; }
}

private void btnKesz_Click(object sender, EventArgs e)
{
int i = lstFeladatok.SelectedIndex;

if (i >= 0) { lista.Elvegez(lista.Elemek[i]); }
}

private void btnTorles_Click(object sender, EventArgs e)
{
int i = lstFeladatok.SelectedIndex;

if (i >= 0) { lista.Torol(lista.Elemek[i]); }
}

private async void btnMent_Click(object sender, EventArgs e)
{
btnMent.Enabled = false;

try
{
await Task.Run(() => lista.Ment());
tsslUzenet.Text = "Mentve: " + DateTime.Now.ToString("HH:mm:ss");
}
catch (TaroloHiba ex) { tsslUzenet.Text = ex.Message; MessageBox.Show(this, ex.Message, "Mentési hiba", MessageBoxButtons.OK, MessageBoxIcon.Warning); }
finally { btnMent.Enabled = lista.Modositott; }
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (!lista.Modositott) { return; }

DialogResult v = MessageBox.Show(this, "Mentetlen változások vannak. Mented?", Text, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);

if (v == DialogResult.Cancel) { e.Cancel = true; return; }

if (v == DialogResult.Yes)
{
try { lista.Ment(); }
catch (TaroloHiba ex) { MessageBox.Show(this, ex.Message, "Mentési hiba"); e.Cancel = true; }
}
}
📷 Képernyőkép helye

Mit tegyél ide: a kész feladatlista.Javasolt fájlnév: winforms-12-8-kesz.webp · Alt szöveg: A kész feladatlista

Három önálló gyakorlófeladat

Egyszerre csak egyet csinálj: tervezd meg, írd meg, futtasd, ellenőrizd, és csak utána nyisd ki a megoldást.

1. CSV tároló

Írj CsvFeladatTarolo osztályt ugyanahhoz az interface-hez (10.5 lecke). Mi változik a Formban?

Egy lehetséges megoldás

Semmi: csak a Program.cs egy sora (new CsvFeladatTarolo(ut)). Ez az interface lényege.

2. Naplózó dekorátor

Írj tárolót, amely egy másik tárolót csomagol, és naplózza a mentéseket.

Egy lehetséges megoldás

NaplozoTarolo : IFeladatTarolo, konstruktorban kapja a belső tárolót; a Ment naplóz, majd továbbhív. Az új viselkedés nem módosít meglévő osztályt.

3. Automatikus mentés

5 percenként mentsen a program, ha van módosítás. Hová teszed?

Egy lehetséges megoldás

A Form egy Timer-e (11.1 lecke) a Modositott jelző alapján hívja az aszinkron mentést; a FeladatLista nem tud az időzítésről.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Interface

Mi az interface?

Megoldás megnyitása

Szerződés: megmondja, mit tud egy osztály, a megvalósítás nélkül.

2. Mit tud a tároló?

Mi a tároló felelőssége, és mi nem?

Megoldás megnyitása

Mentés és betöltés, a formátum és a hibák kezelése. Nem ismeri a felületet és az üzleti szabályokat.

Közepes szint

3. Hová tartozik?

Hová kerül: (a) new JsonFeladatTarolo(...); (b) a „nincs mentve” jelző?

Megoldás megnyitása

(a) Program.cs (composition root); (b) a FeladatLista (Modositott).

4. DTO

Miért külön FeladatAdat?

Megoldás megnyitása

A tárolt forma szabadon szerializálható, a modell védett marad (privát setter, ellenőrzés); a fájlformátum változása nem érinti a modellt.

Kihívás szint

5. Interface vagy absztrakt osztály?

Mikor melyiket?

Megoldás megnyitása

Az interface csak szerződés (több is megvalósítható egy osztályban); az absztrakt osztály közös kódot is tarthat, de csak egy öröklődhet. Kezdd interface-szel.

6. Betöltés a konstruktorban?

Miért rossz, ha a FeladatLista konstruktora tölt be?

Megoldás megnyitása

A konstruktor kivételt dobhat (sérült fájl), nem aszinkron, és a tesztelés is nehezebb. Külön Betolt() metódus jobb, amelyet a hívó kezelhet.

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért nem ír a Form közvetlenül fájlt?

Mert a formátum és az útvonal módosítása a Form minden pontját érintené, és nem lenne tesztelhető.

2. Mi a dependency injection?

Az osztály a függőségét kívülről kapja (például a konstruktorban), nem maga hozza létre.

3. Mi a composition root?

Az a hely (Main), ahol az alkalmazás részei összeállnak.

4. Miért van MemoriaFeladatTarolo?

Lemez nélküli, gyors tesztekhez.

5. Mi a TaroloHiba haszna?

A Form egyetlen típust kezel, és nem ismeri a technikai kivételeket.

6. Hová ment a program felhasználói adatot?

Az ApplicationData mappába, nem a program mappájába.

7. Mire jó a Modositott jelző?

Mentetlen változás jelzésére: rákérdezés bezáráskor, a Mentés gomb állapota.

Mit tudsz már?

  • Tároló osztályt írsz, amely elrejti a mentés részleteit.
  • Interface-t definiálsz és több megvalósítással használod.
  • A függőséget a konstruktoron át adod át, és a Program.cs-ben állítod össze.
  • A tároló hibáit saját kivétellel jelzed, és a mentést aszinkron végzed.

Következő lecke: Egyszerű alkalmazásarchitektúra

A rétegek (modell, szolgáltatás, tároló, felület) összefoglalása, a mappaszerkezet, a függőségek iránya és egy egyszerű felület-szétválasztó minta.