Grafikus C# – Windows Forms tanfolyam › 12.6 lecke Windows Forms · 12.6 lecke · OOP Mi tartozik egy osztályba, és mi nem? A kezdő programokban a Adatmodellek és felelősségek
Form1 mindent tud: tárolja az adatot, ellenőrzi, számolja, menti, és kirajzolja. Ez működik, amíg kicsi a program, de a változtatás egyre nehezebb: egy új szabály a Form tucatnyi pontját érinti. A megoldás a felelősségek szétválasztása: az adatot és annak szabályait egy modellosztály tartja, a Form csak megjeleníti azt. Ebben a leckében megtanulod, hogyan tervezz modellosztályt, mi az egy felelősség elve, hogyan tedd a modellt kényelmesen használhatóvá (ToString, Equals, rendezés, record), hol a helye az ellenőrzésnek, és hogyan jelenik meg a modell a ListBoxban.
A lecke végére
- modellosztályt tervezel: az adat és a rá vonatkozó szabályok egy helyen;
- felismered a Form túl sok felelősségét, és szétosztod;
- a modellt kényelmessé teszed (
ToString,Equals/GetHashCode,IComparable); - ismered a
recordtípust (új .NET), és tudod, mikor jó; - a szabályokat a modellben ellenőrzöd, a felületen csak megjeleníted a hibát;
- a modellt
ListBoxban,ComboBoxban ésBindingList<T>-vel jeleníted meg.
Mielőtt elkezded
Ehhez a leckéhez a 12.1–12.5 lecke (osztály, konstruktor, egységbezárás) és a 7.x lecke (gyűjtemények) szükséges.
Mi az adatmodell?
A modell a program tárgyának leírása a program saját nyelvén: egy Feladat, egy Névjegy, egy Könyv. Tárolja az adatot, és ismeri a rá vonatkozó szabályokat (mi érvényes, mi számolható belőle). Nem tud a felületről: nincs benne TextBox, MessageBox, vezérlő.
public enum Fontossag { Alacsony, Normal, Magas }
public class Feladat
{
public string Cim { get; private set; }
public DateTime? Hatarido { get; set; }
public Fontossag Fontos { get; set; } = Fontossag.Normal;
public bool Kesz { get; private set; }
public Feladat(string cim)
{
Atnevez(cim);
}
public void Atnevez(string cim)
{
if (string.IsNullOrWhiteSpace(cim)) { throw new ArgumentException("A cím kötelező.", "cim"); }
Cim = cim.Trim();
}
public void Elvegez() { Kesz = true; }
// a modell saját szabálya: ő tudja, mikor lejárt
public bool Lejart(DateTime most)
{
return !Kesz && Hatarido.HasValue && Hatarido.Value < most;
}
}Lejart, Atnevez ellenőrzéssel). Ha a „lejárt” fogalma változik (hétvége nem számít), egyetlen helyen kell átírni.MessageBox, TextBox. Ezért a modell önmagában kipróbálható, és több felületen (WinForms, konzol, web) is használható.DateTime? (Nullable<DateTime>) null is lehet: „nincs határidő”. A HasValue és Value adja az ellenőrzést és az értéket.Fontossag felsorolás a hibás értékeket kizárja (nincs „7. fontosság”), és olvashatóbb a számnál.Jósolj!
A Feladat osztály Lejart metódusa a MessageBox.Show("Lejárt!") hívást is tartalmazza. Miért gond ez?
Jóslat ellenőrzése
A modell a felülethez kötődik: nem hívható háttérszálból vagy tesztből (felugró ablak!), nem használható más felületen, és a megjelenítés módja (ablak, színezés, hang) a modellben dől el, nem a felületen. A modell értéket ad vissza (true), a megjelenítés a Form dolga.
Az egy felelősség elve
Az egy felelősség elve (Single Responsibility Principle): egy osztálynak egyetlen oka legyen a változásra. Ha egy osztály változik, valahányszor (1) a felület kinézete, (2) az üzleti szabály, (3) a fájlformátum is módosul, túl sokat vállal.
Mindent tudó Form (rossz)
- tárolja a feladatokat egy
List-ben, - ellenőrzi a bemenetet,
- számolja a lejártakat,
- menti és tölti a fájlt,
- kirajzolja a listát.
Öt változási ok: egy módosítás sok helyen kell, és semmi sem tesztelhető külön.
Szétválasztva (jó)
Feladat: adat és szabály (modell),FeladatLista: a gyűjtemény műveletei (12.7 lecke),FeladatTarolo: mentés és betöltés (12.8 lecke),Form1: csak megjelenítés és eseménykezelés.
Minden osztály egy dologért felel.
Jósolj!
A „lejárt” szabályt hárman is használják: a lista színezése, az emlékeztető és az export. A szabály háromszor, a Formban van megírva, és most a hétvégét nem számolják. Hány helyen kell módosítani?
Jóslat ellenőrzése
Három helyen (és könnyű egyet kihagyni: ellentmondó viselkedés). Ha a szabály a Feladat.Lejart metódusban él, egy helyen módosítod, és mindhárom használat követi.
Kényelmes modell: ToString, Equals, rendezés, record
public class Nevjegy : IComparable<Nevjegy>, IEquatable<Nevjegy>
{
public string Nev { get; private set; }
public string Email { get; private set; }
public Nevjegy(string nev, string email) { Nev = nev.Trim(); Email = email.Trim().ToLowerInvariant(); }
// 1. ToString: a ListBox ezt jeleníti meg; a hibakereső is ezt mutatja
public override string ToString() { return Nev + " <" + Email + ">"; }
// 2. egyenlőség ÉRTÉK szerint: két névjegy ugyanaz, ha az e-mail egyezik
public bool Equals(Nevjegy mas) { return mas != null && Email == mas.Email; }
public override bool Equals(object o) { return Equals(o as Nevjegy); }
public override int GetHashCode() { return Email.GetHashCode(); } // az Equals-szal összhangban!
// 3. természetes rendezés: név szerint, magyar ábécé szerint
public int CompareTo(Nevjegy mas)
{
return string.Compare(Nev, mas.Nev, new System.Globalization.CultureInfo("hu-HU"), System.Globalization.CompareOptions.IgnoreCase);
}
}
List<Nevjegy> lista = new List<Nevjegy>();
lista.Add(new Nevjegy("Zita", "zita@pelda.hu"));
lista.Add(new Nevjegy("Ádám", "adam@pelda.hu"));
lista.Sort(); // a CompareTo alapján: Ádám, Zita
bool van = lista.Contains(new Nevjegy("Más Név", "ZITA@pelda.hu")); // igaz: az Equals az e-mailt nézi
// record (C# 9, .NET 5 és újabb): az egész automatikus
public record Konyv(string Cim, string Szerzo, int Ev);
Konyv k = new Konyv("Abigél", "Szabó Magda", 1970);
Konyv masolt = k with { Ev = 1972 }; // új példány, egy mező módosítva; az eredeti változatlanListBox az elemek ToString() eredményét jeleníti meg. Alapból az osztály nevét (WindowsFormsApp.Nevjegy): saját ToString kell.Equals a hivatkozást hasonlítja (12.1 lecke). Ha „két névjegy ugyanaz, ha az e-mail egyezik”, felülírod az Equals-t, és mindig együtt a GetHashCode-ot (különben a Dictionary és HashSet hibásan működik).CompareTo a természetes sorrendet adja: a Sort() ezt használja. Más sorrendhez: lista.Sort((a, b) => ...) vagy LINQ OrderBy.record adat-osztály: az Equals, GetHashCode, ToString automatikus (érték szerinti), a tulajdonságok alapból csak init (nem módosíthatók létrehozás után); a with módosított másolatot ad. Csak újabb .NET-ben (5+, C# 9); .NET Frameworkben nem.Jósolj!
A Nevjegy osztály felülírja az Equals metódust, de a GetHashCode-ot nem. Egy HashSet<Nevjegy>-be kétszer ugyanazt az e-mail-t tartalmazó (két külön) névjegyet teszel. Hány elem lesz?
Jóslat ellenőrzése
Valószínűleg kettő: az alapértelmezett GetHashCode a hivatkozásból számol, így a két objektum különböző „fiókba” kerül, és az Equals-ot meg sem hívja a halmaz. Az Equals és a GetHashCode mindig együtt, azonos mezőkre épüljön.
Validáció és megjelenítés: ki mit tesz?
// A MODELL: eldönti, érvényes-e, és megmondja, MIÉRT nem (szöveg, nem MessageBox)
public static string Hiba(string nev, string email)
{
if (string.IsNullOrWhiteSpace(nev)) { return "A név kötelező."; }
if (email == null || !email.Contains("@")) { return "Az e-mail cím hibás."; }
return null; // nincs hiba
}
// A FORM: beolvassa a mezőket, megkérdezi a modellt, megjeleníti a választ
private void btnHozzaad_Click(object sender, EventArgs e)
{
string hiba = Nevjegy.Hiba(txtNev.Text, txtEmail.Text);
if (hiba != null) { errorProvider1.SetError(txtNev, hiba); return; } // megjelenítés: a Form dolga
nevjegyek.Add(new Nevjegy(txtNev.Text, txtEmail.Text));
}
// modell megjelenítése: BindingList + DataSource: a lista változása automatikusan frissíti a ListBoxot
private BindingList<Nevjegy> nevjegyek = new BindingList<Nevjegy>();
private void Form1_Load(object sender, EventArgs e)
{
lstNevjegyek.DataSource = nevjegyek; // a ListBox ezt a listát tükrözi
lstNevjegyek.DisplayMember = "Nev"; // melyik tulajdonságot mutassa (ToString helyett)
}
private void lstNevjegyek_SelectedIndexChanged(object sender, EventArgs e)
{
Nevjegy n = lstNevjegyek.SelectedItem as Nevjegy; // a kiválasztott: maga a MODELL-objektum, nem szöveg
if (n != null) { lblReszlet.Text = n.ToString(); }
}ErrorProvider, szín, MessageBox), a Form dolga. A modell csak szöveget ad.DataSource a vezérlőt a listához köti: a ListBox elemei a lista elemei (objektumok, nem szövegek). A SelectedItem az eredeti modell-objektumot adja: nincs szöveg-elemzés.List<T> változásáról a vezérlő nem értesül (újra kell kötni). A BindingList<T> jelez, ezért a Add/Remove után a lista magától frissül. (System.ComponentModel.) DisplayMember a tulajdonság neve szövegként.Jósolj!
A ListBox.DataSource egy sima List<Nevjegy>. Hozzáadsz egy elemet a listához (lista.Add(...)). Mit mutat a ListBox?
Jóslat ellenőrzése
Nem változik: a List<T> nem értesíti a vezérlőt. Vagy BindingList<T> kell, vagy újra be kell állítani a DataSource-ot (= null, majd = lista).
Mit tegyél ide: a kontaktok programja: a lista, a részletek és a modell hibaüzenete az ErrorProviderben.Javasolt fájlnév: winforms-12-6-kontaktok.webp · Alt szöveg: A kontaktok programja
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 ListBox az osztály nevét mutatja
Az elemek helyén MiProjekt.Nevjegy áll. Nincs felülírt Ok és javítás
ToString(). Írd meg, vagy állítsd be a DisplayMember értékét.
2. A Contains nem találja meg
lista.Contains(new Nevjegy("Zita", "zita@pelda.hu")); // hamis, pedig van ilyenHiba és javítás
Az alap Equals a hivatkozást hasonlítja. Írd felül az Equals és GetHashCode metódust az azonosító mezőre.
3. A szótár hibásan működik
Az Hiányzik a Equals felülírva, a Dictionary mégis duplán tárolja ugyanazt.Ok és javítás
GetHashCode felülírása (vagy más mezőkre épül, mint az Equals). Mindkettő ugyanazokat a mezőket használja.
4. A modell MessageBoxot hív
if (nev == "") { MessageBox.Show("Hibás név"); } // a Nevjegy osztálybanHiba és javítás
A modell a felületre támaszkodik. Dobj kivételt, vagy adj vissza hibaszöveget; a Form jelenítse meg.
5. A lista nem frissül a hozzáadás után
List<Nevjegy> lista = new List<Nevjegy>();
lst.DataSource = lista;
lista.Add(n); // a ListBox nem változikHiba és javítás
BindingList<Nevjegy>-et használj, vagy kösd újra a DataSource-ot.
6. A modell minden mezője nyilvánosan írható
Egy hibás nev.Nev = "" sor érvénytelenné teszi az objektumot.Ok és javítás
private set, és a módosítás ellenőrző metóduson (Atnevez) át (12.5 lecke).
Mini projekt: kontaktok modellje
Készíts kontaktlistát, ahol a modell és a felület szigorúan külön él:
- Nevjegy modell:
Nev,Email,Telefon,Kategoria(enum: Barát, Család, Iskola, Munka);private set; fő konstruktor ellenőrzéssel;static string Hiba(...)az érvényesség ellenőrzéséhez (a Form is ezt használja előzetes ellenőrzésre). - Érték szerinti egyenlőség: az e-mail alapján (
Equals,GetHashCode,IEquatable<Nevjegy>); az e-mail kisbetűre normalizálva. - Rendezés:
IComparable<Nevjegy>magyar ábécé szerint; a Form Rendez gombja és egyComboBox(név / kategória / e-mail szerint). - Felület:
lstNevjegyek(DataSourceegyBindingList<Nevjegy>),txtNev,txtEmail,txtTelefon,cmbKategoria, Hozzáad, Törlés,txtKeres(szűrés);lblReszlet;ErrorProvider. - Hozzáadás: a Form előbb a
Nevjegy.Hibametódust hívja (a hibát azErrorProvidermutatja); ismétlődő e-mail: Ez az e-mail már szerepel (aContainssegítségével). - Szűrés: a keresőmező szerint a név vagy az e-mail része (kis-nagybetű nélkül); a szűrt lista külön
BindingList(a modell lista változatlan); üres keresésnél mindenki. - Statisztika a modell oldaláról: kategóriánkénti darabszám (LINQ
GroupBy) alblStatban; a számolás a modell oldalán (staticmetódus egy listára), a Form csak kiírja. - Szétválasztás ellenőrzése: a
Nevjegyfájlban nincsusing System.Windows.Forms;. - Teszteld: hibás adatok, ismétlődő e-mail (kis-nagybetű eltéréssel), rendezés ékezetes nevekkel (Ádám, Zita, Éva), szűrés, törlés, üres lista, kijelölés nélküli törlés.
Megoldás megnyitása (próbáld ki előbb önállóan!)
// ---- Nevjegy.cs: NINCS using System.Windows.Forms ----
public enum Kategoria { Barat, Csalad, Iskola, Munka }
public class Nevjegy : IComparable<Nevjegy>, IEquatable<Nevjegy>
{
private static readonly CultureInfo hu = new CultureInfo("hu-HU");
public string Nev { get; private set; }
public string Email { get; private set; }
public string Telefon { get; private set; }
public Kategoria Kat { get; private set; }
public Nevjegy(string nev, string email, string telefon, Kategoria kat)
{
string hiba = Hiba(nev, email);
if (hiba != null) { throw new ArgumentException(hiba); }
Nev = nev.Trim();
Email = email.Trim().ToLowerInvariant();
Telefon = (telefon ?? "").Trim();
Kat = kat;
}
public static string Hiba(string nev, string email)
{
if (string.IsNullOrWhiteSpace(nev)) { return "A név kötelező."; }
if (string.IsNullOrWhiteSpace(email)) { return "Az e-mail kötelező."; }
int at = email.IndexOf('@');
if (at <= 0 || email.IndexOf('.', at) < at + 2 || email.EndsWith(".")) { return "Az e-mail cím hibás."; }
return null;
}
public static Dictionary<Kategoria, int> Statisztika(IEnumerable<Nevjegy> lista)
{
return lista.GroupBy(n => n.Kat).ToDictionary(g => g.Key, g => g.Count());
}
public bool Egyezik(string keres)
{
return keres.Length == 0 || Nev.IndexOf(keres, StringComparison.CurrentCultureIgnoreCase) >= 0 || Email.IndexOf(keres, StringComparison.OrdinalIgnoreCase) >= 0;
}
public override string ToString() { return Nev + " <" + Email + ">"; }
public bool Equals(Nevjegy mas) { return mas != null && Email == mas.Email; }
public override bool Equals(object o) { return Equals(o as Nevjegy); }
public override int GetHashCode() { return Email.GetHashCode(); }
public int CompareTo(Nevjegy mas) { return string.Compare(Nev, mas.Nev, hu, CompareOptions.IgnoreCase); }
}
// ---- Form1.cs ----
private readonly List<Nevjegy> mind = new List<Nevjegy>();
private readonly BindingList<Nevjegy> latszik = new BindingList<Nevjegy>();
private void Form1_Load(object sender, EventArgs e)
{
cmbKategoria.DataSource = Enum.GetValues(typeof(Kategoria));
cmbRendez.Items.AddRange(new object[] { "Név", "Kategória", "E-mail" });
cmbRendez.SelectedIndex = 0;
lstNevjegyek.DataSource = latszik;
}
private void Szur()
{
string k = txtKeres.Text.Trim();
latszik.RaiseListChangedEvents = false; // egyszerre frissítünk
latszik.Clear();
foreach (Nevjegy n in mind.Where(x => x.Egyezik(k))) { latszik.Add(n); }
latszik.RaiseListChangedEvents = true;
latszik.ResetBindings();
Statisztika();
}
private void Statisztika()
{
lblStat.Text = string.Join(", ", Nevjegy.Statisztika(mind).Select(p => p.Key + ": " + p.Value));
}
private void btnHozzaad_Click(object sender, EventArgs e)
{
errorProvider1.Clear();
string hiba = Nevjegy.Hiba(txtNev.Text, txtEmail.Text); // a szabály a MODELLBEN él
if (hiba != null) { errorProvider1.SetError(hiba.Contains("név") || hiba.Contains("Név") ? txtNev : txtEmail, hiba); return; }
Nevjegy uj = new Nevjegy(txtNev.Text, txtEmail.Text, txtTelefon.Text, (Kategoria)cmbKategoria.SelectedItem);
if (mind.Contains(uj)) { errorProvider1.SetError(txtEmail, "Ez az e-mail már szerepel."); return; }
mind.Add(uj);
Szur();
}
private void btnTorles_Click(object sender, EventArgs e)
{
Nevjegy n = lstNevjegyek.SelectedItem as Nevjegy;
if (n == null) { tsslUzenet.Text = "Válassz ki egy névjegyet."; return; }
mind.Remove(n);
Szur();
}
private void btnRendez_Click(object sender, EventArgs e)
{
switch (cmbRendez.SelectedIndex)
{
case 0: mind.Sort(); break; // IComparable
case 1: mind.Sort((a, b) => a.Kat.CompareTo(b.Kat)); break;
default: mind.Sort((a, b) => string.Compare(a.Email, b.Email, StringComparison.Ordinal)); break;
}
Szur();
}
private void txtKeres_TextChanged(object sender, EventArgs e) { Szur(); }
private void lstNevjegyek_SelectedIndexChanged(object sender, EventArgs e)
{
Nevjegy n = lstNevjegyek.SelectedItem as Nevjegy;
lblReszlet.Text = n == null ? "" : n.Nev + "\n" + n.Email + "\n" + n.Telefon + "\n" + n.Kat;
}Mit tegyél ide: a kész kontaktlista szűréssel és statisztikával.Javasolt fájlnév: winforms-12-6-kesz.webp · Alt szöveg: A kész kontaktlista
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. Könyv record
Írj record Konyv(string Cim, string Szerzo, int Ev) típust, és hozz létre egy módosított másolatot.Egy lehetséges megoldás
var b = a with { Ev = 2000 }; Az eredeti változatlan, a a == b hamis (más Ev), az a == new Konyv(...azonos...) igaz (érték szerinti egyenlőség).
2. Felelősségek
Egy Legalább négyre: Form1 tárolja a tanulókat, számolja az átlagot, fájlba ment és megjeleníti őket. Hány osztályra bontanád?Egy lehetséges megoldás
Tanulo (modell), Osztaly (gyűjtemény, átlag), TanuloTarolo (fájl), Form1 (megjelenítés).
3. Rendezés több szempont szerint
Rendezz Tanulo objektumokat átlag szerint csökkenően, azonos átlagnál név szerint.Egy lehetséges megoldás
lista.OrderByDescending(t => t.Atlag).ThenBy(t => t.Nev).ToList(), vagy Sort összetett összehasonlítóval.
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. Mi a modell?
Fogalmazd meg egy mondatban. A program tárgyának adatai és szabályai egy osztályban, felületi kód nélkül.Megoldás megnyitása
2. Hová való?
Modellbe vagy Formba: (a) „lejárt-e a határidő”; (b) a lejárt feladat piros színe? (a) modell; (b) Form (megjelenítés).Megoldás megnyitása
Közepes szint
3. ToString vagy DisplayMember?
Mikor melyiket? A Megoldás megnyitása
ToString általános szöveges alak (naplóhoz, hibakeresőhöz is jó); a DisplayMember az adott vezérlő egyetlen tulajdonságát mutatja.
4. Equals és GetHashCode
Miért kell együtt felülírni? A hash-alapú gyűjtemények (Megoldás megnyitása
Dictionary, HashSet) előbb a hash-et nézik: az egyenlő objektumoknak azonos hash kell.
Kihívás szint
5. Módosíthatatlan modell
Hogyan készítesz olyan modellt, amely létrehozás után nem módosítható? Csak Megoldás megnyitása
get (vagy init) tulajdonságok és konstruktor; vagy record. A módosítás új példány (with).
6. Modell és hibák
Kivétel vagy A Hiba() szöveg: melyiket mikor?Megoldás megnyitása
Hiba() a felületi előzetes ellenőrzéshez (a felhasználó gépelése hibás lehet: ez nem kivételes); a konstruktor kivétele a programozói hibára (érvénytelen objektum létrehozása).
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Felületi kód (vezérlők, Egy osztálynak egyetlen oka legyen a változásra. Az elemek Adat-osztály automatikus érték szerinti egyenlőséggel, Változáskor értesíti a kötött vezérlőt, a A modell ellenőriz és hibaszöveget ad; a Form megjeleníti. Az eredeti modell-objektumot adja, nem szöveget.1. Mi nincs a modellben?
MessageBox).2. Mi az egy felelősség elve?
3. Mit mutat a ListBox?
ToString() eredményét (vagy a DisplayMember tulajdonságot).4. Mi a record?
with másolattal (C# 9, .NET 5+).5. Miért BindingList?
List<T> nem.6. Ki ellenőriz, ki jelenít meg?
7. Mi a SelectedItem előnye?
Mit tudsz már?
- Modellosztályt tervezel adattal és szabályokkal, felületi kód nélkül.
- Felismered és szétosztod a Form túl sok felelősségét.
- A modellt kényelmessé teszed (
ToString,Equals, rendezés,record). - A modellt
DataSourceésBindingListsegítségével jeleníted meg.
Következő lecke: Programlogika külön osztályban
A számítás és a műveletek kiemelése a Formból egy külön, tesztelhető osztályba.



