Grafikus C# – Windows Forms tanfolyam › 12.9 lecke

Windows Forms · 12.9 lecke · OOP

Egyszerű alkalmazásarchitektúra

A 12.6–12.8 leckékben szétválasztottad a modellt, a logikát és a tárolást. Most összefoglaljuk és rendszerbe foglaljuk: az alkalmazás rétegekből áll, a rétegek közötti függőségek iránya meghatározott, és a kód kiszámítható helyen él. Ez az architektúra: nem bonyolult elméletek, hanem néhány egyszerű szabály arról, hogy „mi hova való”, és „ki kit ismerhet”. Megtanulod a rétegeket, a mappaszerkezetet és névtereket, a többprojektes megoldás előnyét, egy egyszerű nézet–presenter mintát, és azt, hogyan követed végig egy felhasználói művelet útját az alkalmazásban.

  • kb. 110 perc
  • haladó szint
  • architektúra
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • megnevezed az alkalmazás rétegeit, és tudod, melyik mit tartalmaz;
  • a függőségek irányát helyesen tartod (a modell senkit sem ismer, a felület a legkülső réteg);
  • logikus mappa- és névtér-szerkezetet alakítasz ki;
  • többprojektes megoldást hozol létre, ahol a fordító kikényszeríti a szabályokat;
  • egyszerű nézet–presenter mintát használsz, ha a Form logikája bonyolult;
  • végigköveted egy művelet útját, és felismered a tipikus építészeti hibákat.

Mielőtt elkezded

Ehhez a leckéhez a 12.1–12.8 lecke szükséges (különösen az interface és a függőség átadása).

12.8 Felület és adatkezelés →

A rétegek és a függőségek iránya

RétegMi van benne?Kit ismerhet?
Modell (Models)Adatok és a rájuk vonatkozó szabályok (Feladat, Nevjegy).Senkit (csak a .NET alap osztályait).
Szolgáltatás (Services)Az alkalmazás műveletei és számításai (FeladatLista, HitelSzamolo).A modellt és a tároló interface-ét.
Tároló (Storage)Mentés és betöltés (JsonFeladatTarolo).A modellt és a saját interface-ét.
Felület (Forms)A Formok és a megjelenítés, a felhasználói események.A szolgáltatást és a modellt (de nem a tárolót).
Program.csAz összeállítás (composition root).Mindenkit: itt készülnek a példányok.
A függőségek iránya (nyíl: „ismeri”)ábra
   Forms (felület) ───────▶ Services (szolgáltatás) ───────▶ Models (modell)
│ ▲
▼ │
IFeladatTarolo (interface) ◀─── Storage (tároló)

A modell SENKIT nem ismer.
A szolgáltatás az INTERFACE-t ismeri, nem a konkrét tárolót.
A Forms nem ismeri a Storage réteget; csak a Program.cs köti össze őket.
A fő szabályA függőség befelé mutat: a külső rétegek (felület, tároló) ismerik a belsőket (modell, szolgáltatás), de a belsők nem ismerik a külsőket. A modell a legbelső: tőle semmi sem függ visszafelé.
Miért?A külső részek változnak a leggyakrabban (új felület, másik tároló). Ha a belső réteg nem ismeri őket, a cseréjük nem törik el a szabályokat és a számításokat.
Az interface hol él?A tároló interface-e a szolgáltatás oldalán van (mert ő igényli), a megvalósítás a Storage rétegben. Így a függőség megfordul: a tároló ismeri a szolgáltatás szerződését, nem fordítva.
Nem szentírásKis programnál három réteg is elég (Forms, Logika, Adat). A lényeg a mi hova való kérdésre adott egyértelmű válasz, nem a rétegek száma.

Jósolj!

A Feladat modell Elvegez() metódusa a végén MainForm.Instance.Frissit() hívást tartalmaz, hogy a felület frissüljön. Melyik szabályt sérti ez?

Jóslat ellenőrzése

A függőség irányát: a legbelső réteg (modell) ismeri a legkülsőt (felület). A modell többé nem használható felület nélkül, nem tesztelhető, és körkörös függőség jön létre. A modell eseménnyel jelezhet; a felület feliratkozik rá.

Mappaszerkezet, névterek, többprojektes megoldás

Solution Explorerszerkezet
Naplo.sln
├── Naplo.Core (osztálykönyvtár: NINCS hivatkozása a Windows Forms-ra)
│ ├── Models/ NaploBejegyzes.cs
│ ├── Services/ NaploSzolgaltatas.cs, INaploTarolo.cs
│ └── Storage/ JsonNaploTarolo.cs, MemoriaNaploTarolo.cs
├── Naplo.App (Windows Forms alkalmazás; hivatkozik a Naplo.Core-ra)
│ ├── Forms/ NaploForm.cs, SzerkesztoForm.cs
│ ├── Presenters/ NaploPresenter.cs
│ └── Program.cs az összeállítás
└── Naplo.Tests (tesztprojekt; hivatkozik a Naplo.Core-ra)
└── NaploSzolgaltatasTesztek.cs
NévterekC#
// a névtér a mappát követi: minden fájl elején megmutatja, melyik rétegé
namespace Naplo.Core.Models
{
public class NaploBejegyzes { /* ... */ }
}

namespace Naplo.Core.Services
{
using Naplo.Core.Models; // a szolgáltatás a modellt ismeri

public interface INaploTarolo { /* ... */ }
}

namespace Naplo.App.Forms
{
using Naplo.Core.Services; // a felület a szolgáltatást ismeri; Storage-ra NINCS using
}
Mappák és névterekA mappa neve = a névtér része. Az egységes szerkezet megmutatja, hol keressünk valamit, és az új fájl helye egyértelmű. A Visual Studio az új fájlhoz a mappa szerinti névteret adja.
Több projektAz osztálykönyvtárra (Naplo.Core) a Windows Forms projekt hivatkozik, fordítva nem. Így a fordító kikényszeríti, hogy a Core nem ismeri a felületet: ha valaki MessageBox-ot írna a Core-ba, nem fordul.
Hogyan?Visual Studio: Add → New Project → Class Library (Naplo.Core); a Forms projekten Add → Project Reference → Naplo.Core. A tesztprojekt is a Core-ra hivatkozik: a logikát felület nélkül próbálja ki.
Nem kötelezőKis programnál elég egyetlen projekt mappákkal és névterekkel; a többprojektes megoldás nagyobb alkalmazásnál, vagy ha a logikát több felületen (WinForms, konzol, web) használnád, éri meg.

Jósolj!

A Naplo.Core osztálykönyvtárban valaki MessageBox.Show(...) hívást ír. A projekt nem hivatkozik a System.Windows.Forms-ra. Mi történik?

Jóslat ellenőrzése

Fordítási hiba (CS0103 vagy CS0234: a MessageBox / System.Windows.Forms nem létezik). A szerkezet maga védi az architektúrát: nem a fegyelemre, hanem a fordítóra épít.

Nézet és presenter: egyszerű MVP

INaploNezet.cs, NaploPresenter.cs, NaploForm.csC#
// A NÉZET szerződése: mit tud a felület (nincs benne vezérlő-típus!)
public interface INaploNezet
{
string Szoveg { get; set; } // a beírt szöveg
void MutatBejegyzesek(IEnumerable<string> sorok);
void MutatUzenet(string uzenet);
event EventHandler HozzaadKerve; // a felület jelez, nem hív
}

// A PRESENTER: a nézet és a szolgáltatás között közvetít; semmi WinForms-specifikus
public class NaploPresenter
{
private readonly INaploNezet nezet;
private readonly NaploSzolgaltatas szolgaltatas;

public NaploPresenter(INaploNezet nezet, NaploSzolgaltatas szolgaltatas)
{
this.nezet = nezet;
this.szolgaltatas = szolgaltatas;

nezet.HozzaadKerve += (s, e) => Hozzaad();
Frissit();
}

private void Hozzaad()
{
try
{
szolgaltatas.Hozzaad(nezet.Szoveg);
nezet.Szoveg = "";
Frissit();
}
catch (ArgumentException ex) { nezet.MutatUzenet(ex.Message); }
}

private void Frissit()
{
nezet.MutatBejegyzesek(szolgaltatas.Bejegyzesek.Select(b => b.Idopont.ToString("yyyy.MM.dd HH:mm") + " " + b.Szoveg));
}
}

// A FORM: megvalósítja a nézetet; vékony: csak továbbítja és megjeleníti
public partial class NaploForm : Form, INaploNezet
{
public event EventHandler HozzaadKerve;

public NaploForm() { InitializeComponent(); }

public string Szoveg { get { return txtSzoveg.Text; } set { txtSzoveg.Text = value; } }

public void MutatBejegyzesek(IEnumerable<string> sorok) { lstNaplo.Items.Clear(); lstNaplo.Items.AddRange(sorok.ToArray()); }
public void MutatUzenet(string uzenet) { tsslUzenet.Text = uzenet; }

private void btnHozzaad_Click(object sender, EventArgs e) { HozzaadKerve?.Invoke(this, EventArgs.Empty); }
}

// Program.cs: összeállítás
NaploForm form = new NaploForm();
NaploPresenter presenter = new NaploPresenter(form, new NaploSzolgaltatas(new JsonNaploTarolo(ut)));
Application.Run(form);
Mit nyersz vele?A Form logikája (mit jelenítsünk meg, mi történik gombnyomásra, hogyan reagáljunk a hibára) a presenterben él, amely felület nélkül tesztelhető: egy hamis nézettel (egyszerű osztály, amely az INaploNezet-et valósítja meg) kipróbálod a viselkedést.
Vékony FormA Form csak a vezérlők és az interface között fordít: nincs döntés, nincs if. Egy ilyen Formban kevés a hiba, és a rossz részeket a Designer sem bonyolítja.
Mikor éri meg?Összetett felületi logikánál (sok állapot, feltételek: mit mikor lehet engedélyezni). Egyszerű, 2–3 gombos ablaknál elég, ha a Form hívja a szolgáltatást: ne építs presentert, ha nincs rá szükség.
Más mintákAz MVP (Model–View–Presenter) a Windows Forms-hoz jól illő minta; az MVVM (adatkötéssel) a WPF-ben gyakoribb. A közös elv ugyanaz: a felületi logika nem a Formban él.

Jósolj!

Tesztet írsz a NaploPresenter-re, és egy HamisNezet osztályt készítesz (az INaploNezet megvalósítása egyszerű mezőkkel). A teszt beállítja a Szoveg-et, és kivált egy HozzaadKerve eseményt. Mit ellenőrizhetsz egyetlen vezérlő nélkül?

Jóslat ellenőrzése

Szinte mindent a felületi viselkedésből: a HamisNezet.MutatottSorok tartalmát (frissült-e a lista), az üzenetet üres szövegnél (hibaüzenet érkezett-e), és hogy a Szoveg kiürült-e a hozzáadás után. Ablak, Application.Run, kattintás nélkül.

Egy művelet útja az alkalmazásban

A „Hozzáad” gomb útja

  1. Form: a gomb Click eseménye kiváltja a HozzaadKerve eseményt.
  2. Presenter: beolvassa a nezet.Szoveg értékét, és meghívja a szolgáltatást.
  3. Szolgáltatás: létrehozza a NaploBejegyzes modellt (ellenőrzés a modellben), hozzáadja a listához, Modositott = true.
  4. Tároló: a mentéskor Ment() (a szolgáltatás hívja az interface-en át).
  5. Vissza: a presenter frissíti a nézetet (MutatBejegyzesek), a Form megjeleníti.

A hiba útja

  1. Modell: üres szövegnél ArgumentException (a szabály itt él).
  2. Szolgáltatás: nem kezeli (nem tudja, hogyan kell megjeleníteni), továbbengedi.
  3. Presenter: elkapja, és a nezet.MutatUzenet hívja.
  4. Form: megjeleníti az állapotsorban.

A hiba kifelé halad, a réteg-határokon, és ott kezelik, ahol a megjelenítés eldől.

A szabály: kit ismerhet?Minden nyíl a réteg szabályai szerint halad: Form → Presenter → Szolgáltatás → Modell/Interface. A Tároló a háttérben, az interface mögött. Ha egy hívás ellenkező irányba menne (modell → Form), architektúra-hiba.
Ahol a kód megtalálhatóHa egy hibát keresel („miért nem jelenik meg a hibaüzenet?”), a művelet útját követed: mindegyik lépésnél egy hely, egy felelős. Nem kell az egész programot átnézni.
Gyakori építészeti hibákIsten-Form (minden a Formban), globális static állapot, modell a felületet hívja, körkörös függőség, a Form közvetlenül a tárolót hívja, a szolgáltatás MessageBox-ot hív.
Átalakítás lépésenkéntMeglévő programot nem kell egyszerre átalakítani: egy funkciót választasz, kiemeled (12.7), mögé teszed a tárolót (12.8), és haladsz tovább. Minden lépés után a program futnia kell.

Jósolj!

A szolgáltatás Hozzaad metódusa hiba esetén MessageBox.Show(...)-ot hív, és nem dob kivételt. A presenter tesztjét hamis nézettel futtatod. Mi történik?

Jóslat ellenőrzése

A teszt felugró ablakot nyit (és vár a gombnyomásra), vagy a Core projektben nem is fordul. A szolgáltatás nem ismerheti a felületet: kivételt dob, és a presenter jelzi a nézeten (MutatUzenet), amit a hamis nézet rögzít, és a teszt ellenőrizhet.

📷 Képernyőkép helye

Mit tegyél ide: a napló-alkalmazás Solution Explorer nézete a három projekttel és a mappaszerkezettel.Javasolt fájlnév: winforms-12-9-szerkezet.webp · Alt szöveg: A napló-alkalmazás projektszerkezete

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 modell a Formot hívja

public void Elvegez() { Kesz = true; Application.OpenForms[0].Refresh(); }
Hiba és javítás

A belső réteg ismeri a külsőt. Váltson ki eseményt (Valtozott), amelyre a felület (vagy presenter) feliratkozik.

2. A Form közvetlenül a tárolót hívja

new JsonNaploTarolo("naplo.json").Ment(lista);   // a NaploForm-ban
Hiba és javítás

A felület átugrotta a szolgáltatást, és a konkrét tárolót ismeri. Hívja a szolgáltatás Ment() metódusát; a tárolót a Program.cs adja át.

3. Körkörös hivatkozás

A Visual Studio azt jelzi: A circular dependency was detected a két projekt között.

Ok és javítás

A Core hivatkozik az App-ra, az App a Core-ra. A Core ne hivatkozzon a felületre; a közös szerződéseket (interface) tedd a Core-ba.

4. Az interface a tárolóval együtt él

A szolgáltatás a Storage névtérre hivatkozik, hogy elérje az ITarolo interface-t.

Ok és javítás

Az interface a szolgáltatás oldalán lakjon (Services), a megvalósítás a Storage-ban. Így a szolgáltatás nem függ a tároló rétegtől.

5. Minden egy projektben és mappában

Száz fájl áll egy mappában, és nem látszik, mi mivel függ össze.

Ok és javítás

Rendezd rétegek szerint mappákba, és a névterek kövessék a mappákat. Nagyobb programnál osztálykönyvtárba tedd a logikát.

6. Túlbonyolított kis program

Egy háromgombos programhoz presenter, nézet-interface és három projekt készült.

Ok és javítás

Az architektúra a változás kezelésére való. Kis, rövid életű programnál elég a Form + logikai osztály; bővíts, ha a program nő (és a fájlok kezelhetetlenek lesznek).

Mini projekt: napló-alkalmazás váza

Építs egy bejegyzés-naplót, amelynek a lényege az architektúra: három projekt, jól elhatárolt rétegek, nézet–presenter minta:

  1. Megoldás: Naplo.Core (osztálykönyvtár), Naplo.App (Windows Forms), Naplo.Tests (tesztprojekt vagy konzol-app); a hivatkozások iránya a leckében látott.
  2. Models: NaploBejegyzes (Idopont, Szoveg, Hangulat enum); a konstruktor ellenőrzi a szöveget (nem üres, legfeljebb 500 karakter).
  3. Services: INaploTarolo (Betolt, Ment); NaploSzolgaltatas (Hozzaad, Torol, Kereses(szoveg), Bejegyzesek, Modositott, Valtozott esemény; HangulatStatisztika()).
  4. Storage: JsonNaploTarolo (DTO, biztonságos mentés, TaroloHiba) és MemoriaNaploTarolo.
  5. Nézet és presenter: INaploNezet (Szoveg, Hangulat, Keres, MutatBejegyzesek, MutatUzenet, MutatStatisztika, események: HozzaadKerve, TorlesKerve, KeresesValtozott, MentesKerve); NaploPresenter.
  6. Form: NaploForm megvalósítja a nézetet; vékony; nincs if az üzleti szabályokról; a mentés aszinkron (11. fejezet).
  7. Program.cs: a composition root; kapcsolóval (--memoria) a memória-tároló.
  8. Tesztek: a szolgáltatás tesztje MemoriaNaploTarolo-val; a presenter tesztje HamisNezet-tel (hozzáadás üres szöveggel: hibaüzenet; hozzáadás érvényes szöveggel: a lista frissül, a szövegmező kiürül).
  9. Architektúra-ellenőrzés: írj egy ARCHITEKTURA.md jegyzetet a rétegekről és a hivatkozásokról; ellenőrizd: a Naplo.Core nem hivatkozik System.Windows.Forms-ra, a Formok nem használnak using Naplo.Core.Storage; sort.
  10. Teszteld: az összes művelet, a hibás bemenetek, a tároló cseréje, a tesztek futtatása a Test Explorerben (vagy a Debug kimenetben).
Megoldás megnyitása (próbáld ki előbb önállóan!)
A kulcsrészek: modell, szolgáltatás, presenter, hamis nézet és teszt (using System.Collections.Generic; using System.Linq;)C#
// ---- Naplo.Core/Models/NaploBejegyzes.cs ----
namespace Naplo.Core.Models
{
public enum Hangulat { Rossz, Kozepes, Jo }

public class NaploBejegyzes
{
public DateTime Idopont { get; private set; }
public string Szoveg { get; private set; }
public Hangulat Hangulat { get; private set; }

public NaploBejegyzes(string szoveg, Hangulat hangulat, DateTime idopont)
{
if (string.IsNullOrWhiteSpace(szoveg)) { throw new ArgumentException("A szöveg kötelező.", "szoveg"); }
if (szoveg.Length > 500) { throw new ArgumentException("Legfeljebb 500 karakter lehet.", "szoveg"); }

Szoveg = szoveg.Trim();
Hangulat = hangulat;
Idopont = idopont;
}
}
}

// ---- Naplo.Core/Services/NaploSzolgaltatas.cs ----
namespace Naplo.Core.Services
{
public interface INaploTarolo
{
List<NaploBejegyzes> Betolt();
void Ment(IEnumerable<NaploBejegyzes> bejegyzesek);
}

public class NaploSzolgaltatas
{
private readonly INaploTarolo tarolo;
private readonly List<NaploBejegyzes> lista = new List<NaploBejegyzes>();
private readonly Func<DateTime> ora;

public bool Modositott { get; private set; }
public IReadOnlyList<NaploBejegyzes> Bejegyzesek { get { return lista; } }
public event EventHandler Valtozott;

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

this.tarolo = tarolo;
this.ora = ora ?? (() => DateTime.Now); // az idő függőség: tesztben rögzíthető
}

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

public void Hozzaad(string szoveg, Hangulat hangulat)
{
lista.Insert(0, new NaploBejegyzes(szoveg, hangulat, ora()));
Modositott = true;
Jelez();
}

public void Torol(NaploBejegyzes b) { if (lista.Remove(b)) { Modositott = true; Jelez(); } }

public IEnumerable<NaploBejegyzes> Kereses(string szoveg)
{
if (string.IsNullOrWhiteSpace(szoveg)) { return lista; }

return lista.Where(b => b.Szoveg.IndexOf(szoveg.Trim(), StringComparison.CurrentCultureIgnoreCase) >= 0);
}

public Dictionary<Hangulat, int> HangulatStatisztika()
{
return lista.GroupBy(b => b.Hangulat).ToDictionary(g => g.Key, g => g.Count());
}

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

// ---- Naplo.App/Presenters/NaploPresenter.cs ----
public interface INaploNezet
{
string Szoveg { get; set; }
Hangulat Hangulat { get; }
string Keres { get; }
void MutatBejegyzesek(IEnumerable<string> sorok);
void MutatUzenet(string uzenet);
void MutatStatisztika(string szoveg);
event EventHandler HozzaadKerve;
event EventHandler KeresesValtozott;
}

public class NaploPresenter
{
private readonly INaploNezet nezet;
private readonly NaploSzolgaltatas szolgaltatas;

public NaploPresenter(INaploNezet nezet, NaploSzolgaltatas szolgaltatas)
{
this.nezet = nezet;
this.szolgaltatas = szolgaltatas;

nezet.HozzaadKerve += (s, e) => Hozzaad();
nezet.KeresesValtozott += (s, e) => Frissit();
szolgaltatas.Valtozott += (s, e) => Frissit();

Frissit();
}

private void Hozzaad()
{
try { szolgaltatas.Hozzaad(nezet.Szoveg, nezet.Hangulat); nezet.Szoveg = ""; nezet.MutatUzenet(""); }
catch (ArgumentException ex) { nezet.MutatUzenet(ex.Message); }
}

private void Frissit()
{
nezet.MutatBejegyzesek(szolgaltatas.Kereses(nezet.Keres).Select(b => b.Idopont.ToString("yyyy.MM.dd HH:mm") + " " + b.Szoveg));

nezet.MutatStatisztika(string.Join(", ", szolgaltatas.HangulatStatisztika().Select(p => p.Key + ": " + p.Value)));
}
}

// ---- Tesztek: hamis nézet ----
public class HamisNezet : INaploNezet
{
public string Szoveg { get; set; }
public Hangulat Hangulat { get; set; }
public string Keres { get; set; }
public List<string> Sorok = new List<string>();
public string Uzenet = "";
public string Statisztika = "";
public event EventHandler HozzaadKerve;
public event EventHandler KeresesValtozott;

public void MutatBejegyzesek(IEnumerable<string> sorok) { Sorok = sorok.ToList(); }
public void MutatUzenet(string uzenet) { Uzenet = uzenet; }
public void MutatStatisztika(string szoveg) { Statisztika = szoveg; }

public void KivaltHozzaad() { HozzaadKerve?.Invoke(this, EventArgs.Empty); }
}

public static class PresenterTesztek
{
public static int Mindet()
{
int hibak = 0;
Action<bool, string> igaz = (f, nev) => { if (!f) { hibak++; System.Diagnostics.Debug.WriteLine("HIBA " + nev); } else { System.Diagnostics.Debug.WriteLine("ok " + nev); } };

HamisNezet nezet = new HamisNezet { Szoveg = " ", Keres = "" };
NaploSzolgaltatas szolg = new NaploSzolgaltatas(new MemoriaNaploTarolo(), () => new DateTime(2025, 1, 2, 8, 30, 0));
NaploPresenter p = new NaploPresenter(nezet, szolg);

nezet.KivaltHozzaad();
igaz(nezet.Uzenet.Length > 0 && nezet.Sorok.Count == 0, "üres szövegnél hibaüzenet, nincs bejegyzés");

nezet.Szoveg = "Jó nap volt";
nezet.KivaltHozzaad();
igaz(nezet.Sorok.Count == 1 && nezet.Sorok[0].StartsWith("2025.01.02 08:30"), "érvényes szövegnél a lista frissül, rögzített idővel");
igaz(nezet.Szoveg == "", "a szövegmező kiürül");
igaz(nezet.Uzenet == "", "a hibaüzenet törlődik");

return hibak;
}
}
📷 Képernyőkép helye

Mit tegyél ide: a kész napló-alkalmazás és a Test Explorer zöld tesztekkel.Javasolt fájlnév: winforms-12-9-kesz.webp · Alt szöveg: A kész napló-alkalmazás

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. Rétegek meghatározása

Egy régebbi projektedben sorolj be 5 osztályt a megfelelő rétegbe.

Egy lehetséges megoldás

Adat és szabály: modell; számítás, műveletek: szolgáltatás; fájl/adatbázis: tároló; Form, MessageBox, TextBox: felület. Ahol egy osztály több rétegbe is „lóg”, ott van a bontás helye.

2. Függőségi nyilak

Rajzold le a napló-alkalmazás függőségi nyilait, és jelöld, melyik szabálysértő.

Egy lehetséges megoldás

Helyes: Form → Presenter → Szolgáltatás → Modell; Tároló → Interface. Szabálysértő például: Szolgáltatás → Form, Modell → Szolgáltatás, Form → JsonTarolo.

3. Új funkció

Új funkció: a naplóbejegyzések exportálása CSV-be. Hova kerül az egyes részek?

Egy lehetséges megoldás

A CSV szöveg előállítása: tároló vagy külön CsvExport osztály (Storage); a művelet hívása: szolgáltatás (Exportal(utvonal)); a fájlválasztó ablak és az üzenet: a Form/Presenter. A logika a Core-ban teszteléssel.

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. A rétegek

Sorold fel a négy réteget, és mondd el, mi van bennük.

Megoldás megnyitása

Modell (adat és szabály), szolgáltatás (műveletek), tároló (mentés), felület (Formok).

2. Ki kit ismer?

Ismerheti-e a modell a szolgáltatást?

Megoldás megnyitása

Nem: a függőség befelé mutat, a modell a legbelső réteg.

Közepes szint

3. Hol él az interface?

Hová teszed az INaploTarolo interface-t, és miért?

Megoldás megnyitása

A szolgáltatás oldalára (Core/Services): a szolgáltatás igényli; a tároló megvalósítása a Storage-ban ismeri a szerződést.

4. Mit nyersz a nézet-interface-szel?

Mi a nézet-interface és a presenter előnye?

Megoldás megnyitása

A felületi logika felület nélkül tesztelhető (hamis nézettel), a Form vékony marad.

Kihívás szint

5. Új felület

Szeretnél konzolos felületet is ugyanahhoz a naplóhoz. Mi kell?

Megoldás megnyitása

Csak új felület: egy konzolos osztály, amely az INaploNezet-et megvalósítja (vagy közvetlenül a szolgáltatást hívja); a Core változatlan. Ez a szétválasztás haszna.

6. Mikor túl sok?

Mikor felesleges a presenter?

Megoldás megnyitása

Nagyon egyszerű Formnál, ahol nincs számottevő felületi logika: ott az a hozzáadott fájl csak bonyolít. Az architektúra a változás kezelésére való, ne mindenre.

Gyors önellenőrzés

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

1. Merre mutat a függőség?

Befelé: a külső rétegek ismerik a belsőket.

2. Mit ismer a modell?

Senkit, csak az alap .NET-et.

3. Miért több projekt?

A fordító kikényszeríti a szabályt: a Core nem hivatkozhat a felületre.

4. Mi a composition root?

A Program.cs: itt áll össze az alkalmazás.

5. Mit csinál a presenter?

Közvetít a nézet és a szolgáltatás között; a felületi logikát tartalmazza.

6. Merre halad a hiba?

Kifelé: modell → szolgáltatás → presenter → Form (ott jelenik meg).

7. Mi az isten-Form?

Olyan Form, amelyben minden logika, tárolás és megjelenítés egyben él.

Mit tudsz már?

  • Megnevezed az alkalmazás rétegeit, és helyesen tartod a függőségek irányát.
  • Mappa-, névtér- és többprojektes szerkezetet alakítasz ki.
  • Nézet–presenter mintát használsz, és hamis nézettel teszted.
  • Végigköveted egy művelet útját, és felismered az építészeti hibákat.

Következő lecke: Mini projekt: karbantartható WinForms alkalmazás

A 12. fejezet záró projektje: egy teljes, rétegzett, tesztelt alkalmazás követelményekkel és önértékeléssel.