Grafikus C# – Windows Forms tanfolyam › 12.10 lecke Windows Forms · 12.10 lecke · záró projekt 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.Mini projekt: karbantartható WinForms alkalmazás

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.
Követelménytábla (100 pont)
| Követelmény | Pont |
|---|---|
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ály | Feladata |
|---|---|
Models.Konyv | Adat és szabály: kölcsönözhető-e, késik-e. |
Services.IKonyvTarolo | Szerződés: Betolt(), Ment(...). |
Services.KonyvtarSzolgaltatas | A könyvtár műveletei és számításai. |
Storage.JsonKonyvTarolo, MemoriaKonyvTarolo | Mentés fájlba / memóriába; TaroloHiba. |
Presenters.KonyvtarPresenter | Kö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év | Beállítás |
|---|---|---|
TextBox ×3, NumericUpDown | txtCim, txtSzerzo, nudEv, btnHozzaad | új könyv felvétele |
TextBox, CheckBox | txtKeres, chkElerheto | szűrés cím/szerző szerint; csak az elérhetők |
ListView (Details) | lvKonyvek | oszlopok: Cím, Szerző, Év, Állapot, Határidő |
TextBox, Button ×3 | txtKolcsonzo, btnKolcsonoz, btnVisszahoz, btnTorles | a kijelölt könyvre |
Button, Label, StatusStrip | btnMent, lblStat, tsslUzenet | mentés (aszinkron), statisztika, üzenet |
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. Hozz létre három projektet ( 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). Az A Még a Form előtt írd meg a szolgáltatás teszteit Az A Állapot szövegesen (Elérhető, Kikölcsönözve: Anna, 05.12-ig, Késik 3 napot), gyorsbillentyűk, 1. lépés: megoldás és projektek
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
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
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
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
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
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
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
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
// ---- 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
// ---- 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)
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.
| # | Teszt | Elvárt eredmény |
|---|---|---|
| 1 | Új könyv felvétele érvényes adatokkal | Megjelenik 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. |
| 4 | Kölcsönzés névvel | Az állapot Kikölcsönözve: név, dátum-ig; a gombok állapota helyes. |
| 5 | Kölcsönzés név nélkül | Hibaüzenet, a könyv elérhető marad. |
| 6 | Kikölcsönzött könyv újrakölcsönzése | A Kölcsönöz gomb tiltott (és a szolgáltatás is kivételt dobna). |
| 7 | Visszahozás | A könyv újra elérhető. |
| 8 | Késés (rögzített idővel a tesztben) | Késik N napot, a statisztikában, a szöveg nem csak színnel jelez. |
| 9 | Kikölcsönzött könyv törlése | Tiltott; elérhető könyv törölhető. |
| 10 | Keresés cím és szerző szerint | Kis-nagybetű és ékezet-érzékenység nélkül működik (kis-nagybetű). |
| 11 | Csak az elérhetők szűrő | A kikölcsönzöttek eltűnnek a listából. |
| 12 | Mentés és újraindítás | Minden könyv és kölcsönzés megvan; az azonosító folytatódik. |
| 13 | Sérült fájl (kézzel elrontott JSON) | Érthető üzenet, a program nem omlik össze. |
| 14 | Olvasásvédett vagy hiányzó mappa | Mentési hiba üzenet, a változások megmaradnak a memóriában. |
| 15 | Bezárás mentetlen változással | Rákérdez; Mégse megállítja a bezárást. |
| 16 | Tároló cseréje (--memoria) | A program a Form módosítása nélkül működik, lemez nélkül. |
| 17 | Tesztek futtatása (Tesztek.Mindet()) | Minden ellenőrzés ok, a hibák száma 0. |
| 18 | Szerkezet-ellenőrzés | A 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
ListViewfrissí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
Konyvvagy egy különDijSzamoloosztály számolja; a tesztek rögzített időt használnak. - Kölcsönzők: külön
Tagmodell, kölcsönzési előzmények (Kolcsonzesosztá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. (a) modell (Megoldás megnyitása
Konyv.Kesett); (b) felület (Form); (c) tároló.
2. Interface
Miért van Hogy a szolgáltatás ne függjön a fájlformátumtól, és lemez nélkül tesztelhető legyen.IKonyvTarolo?Megoldás megnyitása
Közepes szint
3. Rögzített idő
Hogyan teszteled a késést? A szolgáltatás Megoldás megnyitása
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 Hogy az új azonosító ne ütközzön a meglévőkkel; az 1-től indulás ütközést okozna.Max(Id) + 1 a betöltés után?Megoldás megnyitása
Kihívás szint
5. Késedelmi díj
Hol kezelnéd a díjat, és hogyan hat ez a tesztekre? Külön Megoldás megnyitása
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? Új Megoldás megnyitása
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. 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. Csak a felület réteg; a modell, szolgáltatás és tároló nem. Az invariánsokat: érvényes adatok, nem kölcsönözhető kétszer, a visszahozás feltétele. A felületi logika: mit mutasson, mit engedélyezzen, hogyan jelezze a hibát. Az interface mögött, a Hogy színtévesztőknek és képernyőolvasónak is érthető legyen: a szöveg is mondja (Késik 3 napot). A modellt, a szolgáltatást és a presentert (hamis nézettel).1. Mit jelent a karbantartható kód?
2. Melyik réteg ismeri a felületet?
3. Mit ellenőriz a modell?
4. Mi a presenter feladata?
5. Hogyan cserélhető a tároló?
Program.cs egy során.6. Miért nem csak a szín jelzi a késést?
7. Mit teszteltél felület nélkül?
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.

