Grafikus C# – Windows Forms tanfolyam › 12.10 lecke

Windows Forms · 12.10 lecke · záró projekt

Mini projekt: karbantartható WinForms alkalmazás

A 12. fejezet záró projektjében mindent egy programba építesz, amit a rendezett kódról tanultál: az iskolai könyvtár nyilvántartóját. A cél nem csak az, hogy működjön, hanem az, hogy karbantartható legyen: a modell őrzi a szabályait, a szolgáltatás végzi a műveleteket, a tároló interface mögött cserélhető, a Form vékony, és a logika tesztelhető felület nélkül. Egy új funkció (például késedelmi díj) így egyetlen helyen illeszthető be, és nem borítja fel az egész programot.

  • kb. 5–6 óra
  • záró projekt
  • könyvtár-nyilvántartó
Diáklány C# programot készít laptopon

A projekt végére

  • rétegzett alkalmazást építesz (modell, szolgáltatás, tároló, felület);
  • a modellben őrzöd az invariánsokat (nem kölcsönözhető kétszer a könyv);
  • a tárolót interface mögé teszed, és két megvalósítással használod;
  • nézet–presenter mintával tartod vékonyan a Formot;
  • a logikát és a presentert tesztekkel ellenőrzöd;
  • önértékeled a kódod karbantarthatóságát.

Mielőtt elkezded

Ehhez a projekthez a 12.1–12.9 lecke szükséges, és a 10.6 (JSON), 11.5 (async) lecke.

12.9 Architektúra →

Követelménytábla (100 pont)

KövetelményPont
Modell: Konyv (azonosító, cím, szerző, év, kölcsönző, határidő); a konstruktor és a műveletek ellenőriznek (Kolcsonoz, Visszahoz, Kesett); privát setterek; nincs felületi kód.15
Szolgáltatás: KonyvtarSzolgaltatas: hozzáadás, törlés (kikölcsönzött nem törölhető), kölcsönzés, visszahozás, keresés, késő könyvek, statisztika; az idő Func<DateTime> függőség; Modositott és Valtozott.20
Tároló: IKonyvTarolo; JsonKonyvTarolo (DTO, biztonságos mentés, TaroloHiba) és MemoriaKonyvTarolo; az interface a szolgáltatás oldalán.15
Nézet és presenter: IKonyvtarNezet, KonyvtarPresenter; vékony Form; a felületi logika a presenterben.15
Felület: könyvlista (állapottal: elérhető / kikölcsönzött / késik), keresőmező, szűrő (csak elérhetők), hozzáadó mezők, Kölcsönöz, Visszahoz, Törlés, Mentés (aszinkron); statisztika; állapotsor; bezáráskor mentési kérdés.10
Szerkezet: a rétegek és névterek rendezettek; a Core nem ismeri a felületet; a Program.cs az összeállítás; a Formban nincs new Json...Tarolo.10
Tesztek: a szolgáltatás és a presenter tesztjei lemez és ablak nélkül (memória-tároló, hamis nézet): legalább 10 ellenőrzés, a határesetekkel.10
Hibatűrés és akadálymentesség: érthető hibaüzenetek, sérült fájl kezelése, nincs kivétel; gyorsbillentyűk, AccessibleName; az állapot szövegesen is jelezve (nem csak színnel).5

Terv: rétegek és felület

Réteg / osztályFeladata
Models.KonyvAdat és szabály: kölcsönözhető-e, késik-e.
Services.IKonyvTaroloSzerződés: Betolt(), Ment(...).
Services.KonyvtarSzolgaltatasA könyvtár műveletei és számításai.
Storage.JsonKonyvTarolo, MemoriaKonyvTaroloMentés fájlba / memóriába; TaroloHiba.
Presenters.KonyvtarPresenterKözvetít a nézet és a szolgáltatás között.
Forms.KonyvtarForm (+ IKonyvtarNezet)Megjelenítés és események; vékony.
Program.csÖsszeállítás (composition root).
VezérlőNévBeállítás
TextBox ×3, NumericUpDowntxtCim, txtSzerzo, nudEv, btnHozzaadúj könyv felvétele
TextBox, CheckBoxtxtKeres, chkElerhetoszűrés cím/szerző szerint; csak az elérhetők
ListView (Details)lvKonyvekoszlopok: Cím, Szerző, Év, Állapot, Határidő
TextBox, Button ×3txtKolcsonzo, btnKolcsonoz, btnVisszahoz, btnTorlesa kijelölt könyvre
Button, Label, StatusStripbtnMent, lblStat, tsslUzenetmentés (aszinkron), statisztika, üzenet
📷 Képernyőkép helye

Mit tegyél ide: a felületterv vázlata vagy a kész könyvtár-nyilvántartó.Javasolt fájlnév: winforms-12-10-felulet.webp · Alt szöveg: A könyvtár-nyilvántartó felülete

A 8 lépés támponttal

Haladj lépésről lépésre, és minden lépés után próbáld ki a programot (vagy a tesztet). A támpont segít elindulni, de a kódot magad írd meg.

1. lépés: megoldás és projektek

Hozz létre három projektet (Konyvtar.Core, Konyvtar.App, Konyvtar.Tests), a hivatkozások iránya: App → Core, Tests → Core. A Core mappái: Models, Services, Storage. Támpont: 12.9 lecke.

2. lépés: a Konyv modell

Privát setterek, konstruktor ellenőrzéssel (a cím és a szerző kötelező, az év 1400 és a jövő év közt). Kolcsonoz(nev, mikor, napok) kivételt dob, ha már kikölcsönzött; Visszahoz(); Kesett(mikor); KesesNap(mikor). Támpont: 12.5–12.6 lecke.

3. lépés: az interface és a szolgáltatás

Az IKonyvTarolo a Services mappában. A szolgáltatás az azonosítókat adja ki (kovetkezoId, betöltéskor a legnagyobb után folytatva). A Torol nem törölhet kikölcsönzött könyvet. Az idő Func<DateTime> paraméter. Támpont: 12.7 lecke.

4. lépés: a tárolók

A KonyvAdat (DTO) a lemezre kerülő forma. A JsonKonyvTarolo: ideiglenes fájl + átnevezés, üres lista, ha nincs fájl; a technikai kivételekből TaroloHiba. A MemoriaKonyvTarolo a teszthez. Támpont: 12.8 lecke.

5. lépés: tesztek a Core-hoz

Még a Form előtt írd meg a szolgáltatás teszteit MemoriaKonyvTarolo-val és rögzített idővel: kölcsönzés, dupla kölcsönzés (kivétel), visszahozás, késés (a határidő után), törlés kikölcsönzöttre, keresés, mentés és újratöltés.

6. lépés: nézet és presenter

Az IKonyvtarNezet: Cim, Szerzo, Ev, Keres, CsakElerheto, Kolcsonzo, KivalasztottId, megjelenítő metódusok és események. A presenter minden döntést meghoz (mit engedélyez, mit mutat). Hamis nézetes tesztek. Támpont: 12.9 lecke.

7. lépés: a Form és az összeállítás

A KonyvtarForm megvalósítja a nézetet: a vezérlők és az interface között fordít. A mentés await Task.Run (11.5 lecke); a FormClosing mentetlen változásnál rákérdez. A Program.cs állítja össze a tárolót, a szolgáltatást, a presentert és a Formot.

8. lépés: finomítás és ellenőrzés

Állapot szövegesen (Elérhető, Kikölcsönözve: Anna, 05.12-ig, Késik 3 napot), gyorsbillentyűk, AccessibleName. Ellenőrizd: a Core nem hivatkozik a Windows Forms-ra, a Formok nem használnak Storage névteret. Írd meg az ARCHITEKTURA.md jegyzetet.

Mintamegoldás

Csak akkor nyisd meg, ha már megpróbáltad. A megoldás egyik lehetséges változat; a vezérlőket a Terv neveivel hozd létre.

Core: modell, szolgáltatás, tárolók
Konyvtar.Core (using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json;)C#
// ---- Models/Konyv.cs ----
namespace Konyvtar.Core.Models
{
public class Konyv
{
public int Id { get; private set; }
public string Cim { get; private set; }
public string Szerzo { get; private set; }
public int Ev { get; private set; }
public string Kolcsonzo { get; private set; }
public DateTime? Hatarido { get; private set; }

public bool Kikolcsonozve { get { return Kolcsonzo != null; } }

public Konyv(int id, string cim, string szerzo, int ev)
{
if (string.IsNullOrWhiteSpace(cim)) { throw new ArgumentException("A cím kötelező.", "cim"); }
if (string.IsNullOrWhiteSpace(szerzo)) { throw new ArgumentException("A szerző kötelező.", "szerzo"); }
if (ev < 1400 || ev > DateTime.Now.Year + 1) { throw new ArgumentException("Az év 1400 és " + (DateTime.Now.Year + 1) + " között lehet.", "ev"); }

Id = id; Cim = cim.Trim(); Szerzo = szerzo.Trim(); Ev = ev;
}

public void Kolcsonoz(string nev, DateTime mikor, int napok = 14)
{
if (Kikolcsonozve) { throw new InvalidOperationException("A könyv már ki van kölcsönözve (" + Kolcsonzo + ")."); }
if (string.IsNullOrWhiteSpace(nev)) { throw new ArgumentException("A kölcsönző neve kötelező.", "nev"); }
if (napok < 1) { throw new ArgumentOutOfRangeException("napok"); }

Kolcsonzo = nev.Trim();
Hatarido = mikor.Date.AddDays(napok);
}

public void Visszahoz()
{
if (!Kikolcsonozve) { throw new InvalidOperationException("A könyv nincs kikölcsönözve."); }

Kolcsonzo = null; Hatarido = null;
}

public bool Kesett(DateTime mikor) { return Hatarido.HasValue && mikor.Date > Hatarido.Value; }
public int KesesNap(DateTime mikor) { return Kesett(mikor) ? (mikor.Date - Hatarido.Value).Days : 0; }

public void Visszaallit(string kolcsonzo, DateTime? hatarido) // csak betöltéshez
{
Kolcsonzo = string.IsNullOrWhiteSpace(kolcsonzo) ? null : kolcsonzo;
Hatarido = Kolcsonzo == null ? null : hatarido;
}

public string Allapot(DateTime mikor)
{
if (!Kikolcsonozve) { return "Elérhető"; }
if (Kesett(mikor)) { return "Késik " + KesesNap(mikor) + " napot (" + Kolcsonzo + ")"; }

return "Kikölcsönözve: " + Kolcsonzo + ", " + Hatarido.Value.ToString("MM.dd") + "-ig";
}
}
}

// ---- Services/KonyvtarSzolgaltatas.cs ----
namespace Konyvtar.Core.Services
{
public interface IKonyvTarolo
{
List<Konyv> Betolt();
void Ment(IEnumerable<Konyv> konyvek);
}

public class KonyvtarSzolgaltatas
{
private readonly IKonyvTarolo tarolo;
private readonly Func<DateTime> ora;
private readonly List<Konyv> konyvek = new List<Konyv>();
private int kovetkezoId = 1;

public bool Modositott { get; private set; }
public IReadOnlyList<Konyv> Konyvek { get { return konyvek; } }
public DateTime Most { get { return ora(); } }
public event EventHandler Valtozott;

public KonyvtarSzolgaltatas(IKonyvTarolo tarolo, Func<DateTime> ora = null)
{
if (tarolo == null) { throw new ArgumentNullException("tarolo"); }

this.tarolo = tarolo;
this.ora = ora ?? (() => DateTime.Now);
}

public void Betolt()
{
konyvek.Clear();
konyvek.AddRange(tarolo.Betolt());
kovetkezoId = konyvek.Count == 0 ? 1 : konyvek.Max(k => k.Id) + 1;
Modositott = false;
Jelez();
}

public void Ment() { tarolo.Ment(konyvek.ToList()); Modositott = false; }

public Konyv Hozzaad(string cim, string szerzo, int ev)
{
Konyv k = new Konyv(kovetkezoId, cim, szerzo, ev);

kovetkezoId++;
konyvek.Add(k);
Valtozas();

return k;
}

public void Torol(int id)
{
Konyv k = Keres(id);

if (k.Kikolcsonozve) { throw new InvalidOperationException("Kikölcsönzött könyv nem törölhető."); }

konyvek.Remove(k);
Valtozas();
}

public void Kolcsonoz(int id, string nev) { Keres(id).Kolcsonoz(nev, ora()); Valtozas(); }
public void Visszahoz(int id) { Keres(id).Visszahoz(); Valtozas(); }

public IEnumerable<Konyv> Kereses(string szoveg, bool csakElerheto)
{
IEnumerable<Konyv> q = konyvek;

if (!string.IsNullOrWhiteSpace(szoveg))
{
string s = szoveg.Trim();

q = q.Where(k => k.Cim.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) >= 0 || k.Szerzo.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) >= 0);
}

if (csakElerheto) { q = q.Where(k => !k.Kikolcsonozve); }

return q.OrderBy(k => k.Cim, StringComparer.CurrentCultureIgnoreCase);
}

public IEnumerable<Konyv> Kesettek() { return konyvek.Where(k => k.Kesett(ora())); }

public string Statisztika()
{
int ki = konyvek.Count(k => k.Kikolcsonozve);

return "Összes: " + konyvek.Count + ", kikölcsönözve: " + ki + ", késik: " + Kesettek().Count();
}

private Konyv Keres(int id)
{
Konyv k = konyvek.FirstOrDefault(x => x.Id == id);

if (k == null) { throw new ArgumentException("Nincs ilyen könyv.", "id"); }

return k;
}

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

// ---- Storage ----
namespace Konyvtar.Core.Storage
{
public class TaroloHiba : Exception { public TaroloHiba(string uzenet, Exception ok) : base(uzenet, ok) { } }

public class KonyvAdat
{
public int Id { get; set; }
public string Cim { get; set; }
public string Szerzo { get; set; }
public int Ev { get; set; }
public string Kolcsonzo { get; set; }
public DateTime? Hatarido { get; set; }
}

internal static class Fordito
{
public static KonyvAdat Adatta(Konyv k) { return new KonyvAdat { Id = k.Id, Cim = k.Cim, Szerzo = k.Szerzo, Ev = k.Ev, Kolcsonzo = k.Kolcsonzo, Hatarido = k.Hatarido }; }

public static Konyv Modellre(KonyvAdat a)
{
Konyv k = new Konyv(a.Id, a.Cim, a.Szerzo, a.Ev);

k.Visszaallit(a.Kolcsonzo, a.Hatarido);

return k;
}
}

public class MemoriaKonyvTarolo : IKonyvTarolo
{
private List<KonyvAdat> adatok = new List<KonyvAdat>();

public int MentesekSzama { get; private set; }

public List<Konyv> Betolt() { return adatok.Select(Fordito.Modellre).ToList(); }
public void Ment(IEnumerable<Konyv> konyvek) { adatok = konyvek.Select(Fordito.Adatta).ToList(); MentesekSzama++; }
}

public class JsonKonyvTarolo : IKonyvTarolo
{
private readonly string ut;

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

public List<Konyv> Betolt()
{
if (!File.Exists(ut)) { return new List<Konyv>(); }

try
{
List<KonyvAdat> adatok = JsonSerializer.Deserialize<List<KonyvAdat>>(File.ReadAllText(ut, Encoding.UTF8)) ?? new List<KonyvAdat>();

return adatok.Select(Fordito.Modellre).ToList();
}
catch (JsonException ex) { throw new TaroloHiba("A könyvtárfájl sérült, nem olvasható.", ex); }
catch (ArgumentException ex) { throw new TaroloHiba("A könyvtárfájl érvénytelen adatot tartalmaz: " + ex.Message, ex); }
catch (UnauthorizedAccessException ex) { throw new TaroloHiba("Nincs jogosultság a könyvtárfájlhoz.", ex); }
catch (IOException ex) { throw new TaroloHiba("A könyvtárfájl nem érhető el: " + ex.Message, ex); }
}

public void Ment(IEnumerable<Konyv> konyvek)
{
try
{
string json = JsonSerializer.Serialize(konyvek.Select(Fordito.Adatta).ToList(), 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); }
}
}
}
App: nézet, presenter, Form, összeállítás
Konyvtar.App (using System.Linq; using System.Threading.Tasks;)C#
// ---- Presenters/KonyvtarPresenter.cs ----
public interface IKonyvtarNezet
{
string Cim { get; set; }
string Szerzo { get; set; }
int Ev { get; }
string Keres { get; }
bool CsakElerheto { get; }
string Kolcsonzo { get; set; }
int? KivalasztottId { get; }

void MutatKonyvek(IEnumerable<KonyvSor> sorok);
void MutatStatisztika(string szoveg);
void MutatUzenet(string uzenet);
void GombokAllapota(bool kolcsonozhet, bool visszahozhat, bool torolhet, bool menthet);

event EventHandler HozzaadKerve, KolcsonozKerve, VisszahozKerve, TorlesKerve, SzuresValtozott, KijelolesValtozott;
}

public class KonyvSor { public int Id; public string Cim, Szerzo, Allapot; public int Ev; public bool Figyelmeztet; }

public class KonyvtarPresenter
{
private readonly IKonyvtarNezet nezet;
private readonly KonyvtarSzolgaltatas szolg;

public KonyvtarPresenter(IKonyvtarNezet nezet, KonyvtarSzolgaltatas szolg)
{
this.nezet = nezet; this.szolg = szolg;

nezet.HozzaadKerve += (s, e) => Vegrehajt(() => { szolg.Hozzaad(nezet.Cim, nezet.Szerzo, nezet.Ev); nezet.Cim = ""; nezet.Szerzo = ""; });
nezet.KolcsonozKerve += (s, e) => Vegrehajt(() => { szolg.Kolcsonoz(nezet.KivalasztottId.Value, nezet.Kolcsonzo); nezet.Kolcsonzo = ""; });
nezet.VisszahozKerve += (s, e) => Vegrehajt(() => szolg.Visszahoz(nezet.KivalasztottId.Value));
nezet.TorlesKerve += (s, e) => Vegrehajt(() => szolg.Torol(nezet.KivalasztottId.Value));
nezet.SzuresValtozott += (s, e) => Frissit();
nezet.KijelolesValtozott += (s, e) => Gombok();
szolg.Valtozott += (s, e) => Frissit();

Frissit();
}

private void Vegrehajt(Action muvelet)
{
try { muvelet(); nezet.MutatUzenet(""); }
catch (ArgumentException ex) { nezet.MutatUzenet(ex.Message); }
catch (InvalidOperationException ex) { nezet.MutatUzenet(ex.Message); }
}

private void Frissit()
{
DateTime most = szolg.Most;

nezet.MutatKonyvek(szolg.Kereses(nezet.Keres, nezet.CsakElerheto).Select(k => new KonyvSor { Id = k.Id, Cim = k.Cim, Szerzo = k.Szerzo, Ev = k.Ev, Allapot = k.Allapot(most), Figyelmeztet = k.Kesett(most) }));
nezet.MutatStatisztika(szolg.Statisztika());
Gombok();
}

private void Gombok()
{
Konyv k = nezet.KivalasztottId == null ? null : szolg.Konyvek.FirstOrDefault(x => x.Id == nezet.KivalasztottId.Value);

nezet.GombokAllapota(k != null && !k.Kikolcsonozve, k != null && k.Kikolcsonozve, k != null && !k.Kikolcsonozve, szolg.Modositott);
}
}

// ---- Program.cs ----
static void Main(string[] args)
{
Application.EnableVisualStyles();

IKonyvTarolo tarolo;

if (args.Contains("--memoria")) { tarolo = new MemoriaKonyvTarolo(); }
else
{
string mappa = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Konyvtar");

Directory.CreateDirectory(mappa);
tarolo = new JsonKonyvTarolo(Path.Combine(mappa, "konyvek.json"));
}

KonyvtarSzolgaltatas szolg = new KonyvtarSzolgaltatas(tarolo);
KonyvtarForm form = new KonyvtarForm(szolg);

try { szolg.Betolt(); }
catch (TaroloHiba ex) { MessageBox.Show(ex.Message, "Betöltési hiba"); }

new KonyvtarPresenter(form, szolg);
Application.Run(form);
}

// ---- Forms/KonyvtarForm.cs (a nézet megvalósítása, vékony) ----
public partial class KonyvtarForm : Form, IKonyvtarNezet
{
private readonly KonyvtarSzolgaltatas szolg;

public event EventHandler HozzaadKerve, KolcsonozKerve, VisszahozKerve, TorlesKerve, SzuresValtozott, KijelolesValtozott;

public KonyvtarForm(KonyvtarSzolgaltatas szolg) { InitializeComponent(); this.szolg = szolg; }

public string Cim { get { return txtCim.Text; } set { txtCim.Text = value; } }
public string Szerzo { get { return txtSzerzo.Text; } set { txtSzerzo.Text = value; } }
public int Ev { get { return (int)nudEv.Value; } }
public string Keres { get { return txtKeres.Text; } }
public bool CsakElerheto { get { return chkElerheto.Checked; } }
public string Kolcsonzo { get { return txtKolcsonzo.Text; } set { txtKolcsonzo.Text = value; } }
public int? KivalasztottId { get { return lvKonyvek.SelectedItems.Count == 0 ? (int?)null : (int)lvKonyvek.SelectedItems[0].Tag; } }

public void MutatKonyvek(IEnumerable<KonyvSor> sorok)
{
int? kijelolt = KivalasztottId;

lvKonyvek.BeginUpdate();
lvKonyvek.Items.Clear();

foreach (KonyvSor s in sorok)
{
ListViewItem it = new ListViewItem(new string[] { s.Cim, s.Szerzo, s.Ev.ToString(), s.Allapot }) { Tag = s.Id };

if (s.Figyelmeztet) { it.ForeColor = Color.Firebrick; it.Font = new Font(lvKonyvek.Font, FontStyle.Bold); } // a szöveg is jelzi ("Késik"): nem csak a szín

lvKonyvek.Items.Add(it);
if (kijelolt == s.Id) { it.Selected = true; }
}

lvKonyvek.EndUpdate();
}

public void MutatStatisztika(string szoveg) { lblStat.Text = szoveg; }
public void MutatUzenet(string uzenet) { tsslUzenet.Text = uzenet; }

public void GombokAllapota(bool kolcsonozhet, bool visszahozhat, bool torolhet, bool menthet)
{
btnKolcsonoz.Enabled = kolcsonozhet; btnVisszahoz.Enabled = visszahozhat; btnTorles.Enabled = torolhet; btnMent.Enabled = menthet;
}

private void btnHozzaad_Click(object s, EventArgs e) { HozzaadKerve?.Invoke(this, e); }
private void btnKolcsonoz_Click(object s, EventArgs e) { KolcsonozKerve?.Invoke(this, e); }
private void btnVisszahoz_Click(object s, EventArgs e) { VisszahozKerve?.Invoke(this, e); }
private void btnTorles_Click(object s, EventArgs e) { TorlesKerve?.Invoke(this, e); }
private void txtKeres_TextChanged(object s, EventArgs e) { SzuresValtozott?.Invoke(this, e); }
private void chkElerheto_CheckedChanged(object s, EventArgs e) { SzuresValtozott?.Invoke(this, e); }
private void lvKonyvek_SelectedIndexChanged(object s, EventArgs e) { KijelolesValtozott?.Invoke(this, e); }

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

try { await Task.Run(() => szolg.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 = szolg.Modositott; }
}

private void KonyvtarForm_FormClosing(object s, FormClosingEventArgs e)
{
if (!szolg.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 { szolg.Ment(); }
catch (TaroloHiba ex) { MessageBox.Show(this, ex.Message, "Mentési hiba"); e.Cancel = true; }
}
}
}
Tesztek (lemez és ablak nélkül)
Konyvtar.TestsC#
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); }
}

private static bool Kivetel<T>(Action a) where T : Exception
{
try { a(); return false; } catch (T) { return true; }
}

public static int Mindet()
{
hibak = 0;

DateTime ido = new DateTime(2025, 3, 1);
MemoriaKonyvTarolo tarolo = new MemoriaKonyvTarolo();
KonyvtarSzolgaltatas sz = new KonyvtarSzolgaltatas(tarolo, () => ido); // az idő a teszt kezében van

Konyv a = sz.Hozzaad("Abigél", "Szabó Magda", 1970);
Konyv b = sz.Hozzaad("Az ember tragédiája", "Madách Imre", 1861);

Igaz(a.Id == 1 && b.Id == 2, "azonosítók egymás után");
Igaz(Kivetel<ArgumentException>(() => sz.Hozzaad("", "X", 2000)), "üres cím kivétel");
Igaz(Kivetel<ArgumentException>(() => sz.Hozzaad("Régi", "X", 1200)), "túl régi év kivétel");

sz.Kolcsonoz(a.Id, "Anna");

Igaz(a.Kikolcsonozve && a.Hatarido == new DateTime(2025, 3, 15), "kölcsönzés: 14 napos határidő");
Igaz(Kivetel<InvalidOperationException>(() => sz.Kolcsonoz(a.Id, "Béla")), "dupla kölcsönzés kivétel");
Igaz(Kivetel<InvalidOperationException>(() => sz.Torol(a.Id)), "kikölcsönzött nem törölhető");
Igaz(!a.Kesett(new DateTime(2025, 3, 15)), "a határidő napján nem késik");
Igaz(a.Kesett(new DateTime(2025, 3, 16)) && a.KesesNap(new DateTime(2025, 3, 18)) == 3, "késés napokban");

ido = new DateTime(2025, 3, 20);

Igaz(sz.Kesettek().Count() == 1, "a késő könyvek listája");
Igaz(sz.Kereses("szabó", false).Count() == 1, "keresés szerző szerint, kis-nagybetű nélkül");
Igaz(sz.Kereses("", true).Count() == 1, "csak az elérhetők");

sz.Visszahoz(a.Id);

Igaz(!a.Kikolcsonozve && sz.Kesettek().Count() == 0, "visszahozás után nem késik");

sz.Kolcsonoz(b.Id, "Cili");
sz.Ment();

KonyvtarSzolgaltatas ujra = new KonyvtarSzolgaltatas(tarolo, () => ido);

ujra.Betolt();

Igaz(ujra.Konyvek.Count == 2 && ujra.Konyvek.First(k => k.Id == 2).Kolcsonzo == "Cili", "mentés és újratöltés megőrzi a kölcsönzést");
Igaz(ujra.Hozzaad("Új", "Szerző", 2000).Id == 3, "az azonosító folytatódik a betöltés után");
Igaz(!ujra.Modositott || ujra.Konyvek.Count == 3, "a hozzáadás módosítottnak jelöli a listát");

return hibak;
}
}

Tesztlista (18 pont)

Minden pontnál próbáld ki, és jegyezd fel az eredményt.

#TesztElvárt eredmény
1Új könyv felvétele érvényes adatokkalMegjelenik a listában, Elérhető állapottal, a statisztika frissül.
2Üres cím vagy szerzőÉrthető hibaüzenet, nem kerül a listába.
3Érvénytelen év (1200, jövő)Hibaüzenet az állapotsorban.
4Kölcsönzés névvelAz állapot Kikölcsönözve: név, dátum-ig; a gombok állapota helyes.
5Kölcsönzés név nélkülHibaüzenet, a könyv elérhető marad.
6Kikölcsönzött könyv újrakölcsönzéseA Kölcsönöz gomb tiltott (és a szolgáltatás is kivételt dobna).
7VisszahozásA könyv újra elérhető.
8Késés (rögzített idővel a tesztben)Késik N napot, a statisztikában, a szöveg nem csak színnel jelez.
9Kikölcsönzött könyv törléseTiltott; elérhető könyv törölhető.
10Keresés cím és szerző szerintKis-nagybetű és ékezet-érzékenység nélkül működik (kis-nagybetű).
11Csak az elérhetők szűrőA kikölcsönzöttek eltűnnek a listából.
12Mentés és újraindításMinden könyv és kölcsönzés megvan; az azonosító folytatódik.
13Sérült fájl (kézzel elrontott JSON)Érthető üzenet, a program nem omlik össze.
14Olvasásvédett vagy hiányzó mappaMentési hiba üzenet, a változások megmaradnak a memóriában.
15Bezárás mentetlen változássalRákérdez; Mégse megállítja a bezárást.
16Tároló cseréje (--memoria)A program a Form módosítása nélkül működik, lemez nélkül.
17Tesztek futtatása (Tesztek.Mindet())Minden ellenőrzés ok, a hibák száma 0.
18Szerkezet-ellenőrzésA Core nem hivatkozik a System.Windows.Forms-ra; a Formok nem használják a Storage névteret.

Gyakori hibák és önértékelés

Gyakori hibák

  • A szolgáltatás vagy a modell MessageBox-ot hív.
  • A Form közvetlenül a tárolót példányosítja.
  • Az idő (DateTime.Now) rejtett függőségként a modellben, ezért nem tesztelhető a késés.
  • Az azonosító betöltés után újra 1-ről indul (ütközés).
  • A ListView frissítésekor a kijelölés elvész.
  • A List<T> közvetlenül kiadva (kívülről módosítható).
  • A késést csak piros szín jelzi.

Önértékelés

Pontozd magad a követelménytábla szerint, majd válaszolj:

  • Hol van a „kikölcsönzött könyv nem törölhető” szabály, és miért ott?
  • Mit kellene módosítanod, ha XML-be mentenél? És ha késedelmi díj kellene?
  • Hány fájl változna, ha a Konyv-hoz új mező (ISBN) érkezne?
  • Mit tesztelsz felület nélkül, és mit nem tudsz?

Bővítési ötletek

  • Késedelmi díj: napi díj a Konyv vagy egy külön DijSzamolo osztály számolja; a tesztek rögzített időt használnak.
  • Kölcsönzők: külön Tag modell, kölcsönzési előzmények (Kolcsonzes osztály).
  • Exportálás: CSV vagy JSON export (10.5–10.6 lecke), mentési hibakezeléssel.
  • Visszavonás: az utolsó művelet visszavonása (parancs objektumok listája).
  • Beállítások: a kölcsönzési idő (napok) és a tároló útvonala beállításokból (10.8 lecke).
  • Egységteszt-projekt: a Tesztek.Mindet() átírása MSTest vagy xUnit keretrendszerre.

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. Hová tartozik?

Rétegezd: (a) „késik-e a könyv”; (b) a piros sor; (c) a JSON fájl írása.

Megoldás megnyitása

(a) modell (Konyv.Kesett); (b) felület (Form); (c) tároló.

2. Interface

Miért van IKonyvTarolo?

Megoldás megnyitása

Hogy a szolgáltatás ne függjön a fájlformátumtól, és lemez nélkül tesztelhető legyen.

Közepes szint

3. Rögzített idő

Hogyan teszteled a késést?

Megoldás megnyitása

A szolgáltatás Func<DateTime> órát kap; a teszt egy rögzített, módosítható időt ad át.

4. Azonosító betöltés után

Miért Max(Id) + 1 a betöltés után?

Megoldás megnyitása

Hogy az új azonosító ne ütközzön a meglévőkkel; az 1-től indulás ütközést okozna.

Kihívás szint

5. Késedelmi díj

Hol kezelnéd a díjat, és hogyan hat ez a tesztekre?

Megoldás megnyitása

Külön DijSzamolo (tiszta függvény: késés napjai és díjszabás → összeg) a Core-ban; a szolgáltatás hívja; a tesztek ismert értékekkel ellenőrzik. A modell és a Form nem változik.

6. XML tároló

Mit módosítasz, ha XML kell JSON helyett?

Megoldás megnyitása

Új XmlKonyvTarolo : IKonyvTarolo osztály a Storage-ban, és egy sor a Program.cs-ben. A szolgáltatás, a presenter, a Form és a tesztek változatlanok.

Gyors önellenőrzés

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

1. Mit jelent a karbantartható kód?

Könnyen érthető és módosítható: a változás kevés helyet érint, és a hibák felület nélkül is megtalálhatók.

2. Melyik réteg ismeri a felületet?

Csak a felület réteg; a modell, szolgáltatás és tároló nem.

3. Mit ellenőriz a modell?

Az invariánsokat: érvényes adatok, nem kölcsönözhető kétszer, a visszahozás feltétele.

4. Mi a presenter feladata?

A felületi logika: mit mutasson, mit engedélyezzen, hogyan jelezze a hibát.

5. Hogyan cserélhető a tároló?

Az interface mögött, a Program.cs egy során.

6. Miért nem csak a szín jelzi a késést?

Hogy színtévesztőknek és képernyőolvasónak is érthető legyen: a szöveg is mondja (Késik 3 napot).

7. Mit teszteltél felület nélkül?

A modellt, a szolgáltatást és a presentert (hamis nézettel).

A 12. fejezet összefoglalása: mit tudsz már?

  • Érted az osztály–objektum–referencia fogalmakat, és a Form felépítését (partial, Designer).
  • Mezőket, tulajdonságokat, konstruktort és hozzáférési szinteket tudatosan használsz.
  • Modellt, szolgáltatást és tárolót tervezel, felelősségek szerint szétválasztva.
  • Interface mögé teszed a cserélhető részeket, és a függőséget kívülről adod át.
  • Rétegzett, tesztelhető, karbantartható alkalmazást építesz.

Következő fejezet: Hibakeresés, tesztelés és használhatóság

A 13. fejezetben a hibákat felderíted és javítod, nem csak újrafuttatod: a hibakereső, a naplózás, a tesztelés és a használhatóság témái következnek.