Grafikus C# – Windows Forms tanfolyam › 5.7 lecke Windows Forms · 5.7 lecke · feliratkozás és leiratkozás Az 5.1 leckében megtanultad a Események összekapcsolása, feliratkozás és leiratkozás
+= és a -= operátort. Most a következményeit nézzük: a feliratkozás egy kapcsolat, amely addig tart, amíg le nem bontod. Ha egy bezárt ablak feliratkozva marad, az esemény továbbra is hívja, és az ablak elavult vezérlőit próbálja használni: hibák keletkeznek, a memória pedig nem szabadul fel. Ebben a leckében megtanulod, mikor kell leiratkozni, hogyan írsz önleiratkozó kezelőt, hogyan váltasz ki biztonságosan eseményt, és mire figyelj a kódból létrehozott vezérlőknél.
A lecke végére
- érted, hogy a kibocsátó hivatkozást tart a feliratkozóra, és ez milyen hibákat okozhat;
- párban kezeled a feliratkozást és a leiratkozást (
Load/FormClosed); - önleiratkozó (egyszer lefutó) kezelőt írsz;
- hibatűrően váltasz ki eseményt (
GetInvocationList), és megszámolod a feliratkozókat; - kódból létrehozott vezérlőknél helyesen iratkozol le, és felszabadítod őket (
Dispose); - felismered a gyakori hibákat (halmozódó feliratkozás, bezárt ablak kezelője,
Controls.Clearcsapdája).
Mielőtt elkezded
Ehhez a leckéhez az 5.1 lecke (események, +=, -=, saját esemény), az 5.6 lecke (Load, FormClosed, gyermekablakok) és a 3.9 lecke (osztályok) szükséges. Új projektben próbáld ki a példákat.
A feliratkozás mint kapcsolat
Amikor azt írod, hogy forras.Esemeny += Kezelo;, a kibocsátó (forras) eltárolja a kezelőre mutató hivatkozást (a delegáltat). Mivel a kezelő egy feliratkozó objektum metódusa (például egy ablaké), a kibocsátó egyben az objektumra is hivatkozik. Két következmény van:
1. A feliratkozó „életben marad”
A .NET szemétgyűjtője csak azt az objektumot szabadítja fel, amelyre már semmi nem hivatkozik. Ha egy hosszú életű kibocsátó (a főablak, egy statikus osztály) hivatkozik egy rövid életű feliratkozóra (egy gyermekablakra), az utóbbi nem szabadulhat fel, még bezárva sem. Ez a memóriaszivárgás.
2. A bezárt feliratkozó is megkapja az eseményt
Az esemény kiváltásakor a kibocsátó a bezárt ablak kezelőjét is meghívja. A kezelő az ablak már megsemmisített vezérlőit használná, és ObjectDisposedException (vagy más hiba) keletkezik.
Fontos különbség: ha a kibocsátó és a feliratkozó azonos élettartamú (például egy ablak a saját gombjára iratkozik fel), nincs gond: együtt szűnnek meg. A gond a hosszú életű kibocsátó + rövid életű feliratkozó párosnál van. Szemléltessük egy statikus „hírközpont” osztállyal, amely az egész program élete alatt létezik:
public static class Hirkozlo
{
// statikus esemény: az egész program életében létezik
public static event EventHandler<HirEventArgs> HirErkezett;
public static void Kuld(string szoveg)
{
HirErkezett?.Invoke(null, new HirEventArgs { Szoveg = szoveg });
}
}
public class HirEventArgs : EventArgs
{
public string Szoveg { get; set; }
}private void FigyeloAblak_Load(object sender, EventArgs e)
{
Hirkozlo.HirErkezett += HirKezelo; // feliratkozunk, de sosem iratkozunk le
}
private void HirKezelo(object sender, HirEventArgs e)
{
lstHirek.Items.Add(e.Szoveg); // az ablak bezárása után ez már megsemmisített vezérlő!
}Jósolj!
Megnyitsz egy FigyeloAblak ablakot, majd bezárod. Ezután a főablakból meghívod a Hirkozlo.Kuld("Szia") metódust. Mi történik?
Jóslat ellenőrzése
A Hirkozlo továbbra is hivatkozik a bezárt ablak HirKezelo metódusára, ezért meghívja. A kezelő az ablak már megsemmisített lstHirek vezérlőjére ír, és ObjectDisposedException (vagy InvalidOperationException) keletkezik. A hiba a küldő kódban jelentkezik, nem a bezárt ablakban, ezért nehéz a forrását megtalálni. Közben a bezárt ablak a memóriában marad.
Párban: ahol feliratkozol, ott iratkozz le
A szabály egyszerű: a feliratkozást és a leiratkozást ugyanaz a kód végezze, szimmetrikus helyen. Ablaknál ez a Load és a FormClosed páros (5.6 lecke):
private void FigyeloAblak_Load(object sender, EventArgs e)
{
Hirkozlo.HirErkezett += HirKezelo;
}
private void FigyeloAblak_FormClosed(object sender, FormClosedEventArgs e)
{
Hirkozlo.HirErkezett -= HirKezelo; // a pár: az ablak többé nem figyel
}
private void HirKezelo(object sender, HirEventArgs e)
{
lstHirek.Items.Add(DateTime.Now.ToString("HH:mm:ss") + " – " + e.Szoveg);
}FormClosed biztos bezárás (a FormClosing megszakítható, ott ne iratkozz le).-= HirKezelo működik: a névvel megadott metódus minden alkalommal egyenlő delegáltat ad (ugyanaz a metódus ugyanazon az objektumon), ezért a leiratkozás megtalálja a feliratkozást.Leiratkozas() metódusba) gyűjteni.btnMent.Click += ...), nem kell leiratkozni: a vezérlő és az ablak együtt szűnik meg.A feliratkozók számának megtekintése
Hibakereséskor hasznos tudni, hány kezelő van feliratkozva. A kibocsátó osztályon belül az esemény delegáltként is kezelhető, és a GetInvocationList() a feliratkozók tömbjét adja:
public static int FeliratkozokSzama
{
get
{
if (HirErkezett == null)
{
return 0;
}
return HirErkezett.GetInvocationList().Length;
}
}Jósolj!
Háromszor megnyitsz egy FigyeloAblak ablakot (mindhárom a Load-ban feliratkozik), majd az egyiket bezárod. Mennyi a FeliratkozokSzama, ha az ablak a FormClosed-ben leiratkozik? És ha nem iratkozik le?
Jóslat ellenőrzése
Leiratkozással: 2 (a bezárt ablak kikerül). Leiratkozás nélkül: 3 (a bezárt ablak kezelője továbbra is ott van, és a következő hír kiváltásakor hibát okoz). A FeliratkozokSzama ezért jó hibakereső eszköz: ha a nyitott ablakok száma és a feliratkozók száma eltér, valahol hiányzik egy leiratkozás.
Önleiratkozó kezelő: egyszer lefutó esemény
Néha egy kezelőnek csak egyszer kell lefutnia (például egy esemény első bekövetkezésére várunk). Ilyenkor a kezelő a futása végén leiratkozik magáról. Névvel ellátott metódussal ez egyszerű (Esemeny -= Kezelo;). Lambdával egy trükk kell: a lambdát előbb egy változóban deklarálni kell, hogy hivatkozhass rá önmagából:
// 1. névvel ellátott metódus: egyszerű
private void ElsoKattintas(object sender, EventArgs e)
{
MessageBox.Show("Ez csak egyszer jelenik meg.");
btnProba.Click -= ElsoKattintas;
}
// 2. lambda: előbb deklaráljuk (null értékkel), utána adjuk meg a törzsét
EventHandler egyszeri = null;
egyszeri = (s, ev) =>
{
MessageBox.Show("Egyszeri lambda.");
btnMasik.Click -= egyszeri; // önmagáról iratkozik le: a változó már létezik
};
btnMasik.Click += egyszeri;-= megtalálja.+= mellé írod, és később még egyszer leírod a -= mellé, a kettő két külön objektum (5.1 lecke). A változó oldja meg.Jósolj!
Egy gomb kattintásakor a kezelő feliratkozik egy másik gomb eseményére (btnB.Click += Kezelo;). Hányszor jelenik meg az üzenet a btnB egyszeri megnyomására, ha előtte háromszor megnyomtad a btnA gombot?
Jóslat ellenőrzése
Háromszor. Minden btnA kattintás újabb feliratkozást ad ugyanarra a metódusra, ezért a btnB egy kattintása háromszor hívja meg. Ez az egyik leggyakoribb feliratkozási hiba: a feliratkozás egy gyakran lefutó kezelőbe került. A javítás: a feliratkozás egyszer történjen (Load), vagy előbb leiratkozás, utána feliratkozás: btnB.Click -= Kezelo; btnB.Click += Kezelo; (a leiratkozás hatástalan, ha nincs feliratkozva, ezért a páros biztosan egyszeri feliratkozást ad).
Hibatűrő eseménykiváltás
Az 5.1 leckében láttad: ha egy eseményre több kezelő iratkozott fel, a kiváltás sorban hívja meg őket. De mi történik, ha az egyik kezelő kivételt dob? A kivétel megszakítja a kiváltást: a többi kezelő nem fut le. Egy hibás feliratkozó így megakadályozhatja a többit, hogy értesüljön:
// A SIMA kiváltás: ha az első kezelő hibát dob, a második nem kapja meg a hírt
public static void KuldSima(string szoveg)
{
HirErkezett?.Invoke(null, new HirEventArgs { Szoveg = szoveg });
}
// A HIBATŰRŐ kiváltás: minden kezelőt külön hívunk, a hibát elkapjuk
public static int Kuld(string szoveg)
{
EventHandler<HirEventArgs> kezelok = HirErkezett; // másolat: hívás közbeni változás ellen
int hibas = 0;
if (kezelok == null)
{
return 0;
}
foreach (Delegate d in kezelok.GetInvocationList())
{
try
{
((EventHandler<HirEventArgs>)d)(null, new HirEventArgs { Szoveg = szoveg });
}
catch (Exception)
{
hibas = hibas + 1; // egy hibás feliratkozó nem állíthatja meg a többit
}
}
return hibas; // hány kezelő dobott hibát
}Delegate típusú elemet vissza kell alakítani a pontos típusra, hogy paraméterekkel hívható legyen.Jósolj!
Három kezelő iratkozott fel a HirErkezett eseményre: az első rendben van, a második kivételt dob, a harmadik rendben van. A KuldSima hívására mely kezelők futnak le? És a Kuld hívására?
Jóslat ellenőrzése
A KuldSima esetén csak az első fut le rendben, a második kivételt dob, és a harmadik nem kap értesítést: a kivétel a hívó kódig emelkedik. A Kuld esetén mindhárom meghívódik (a második hibája el van kapva), és a visszatérési érték 1 (egy hibás kezelő). A hibákat természetesen nem szabad csendben elnyelni a gyakorlatban: érdemes naplózni (DB.Naploz), a példában a számlálás a rövidség miatt van.
Kódból létrehozott vezérlők: leiratkozás és felszabadítás
Ha a vezérlőket kódból hozod létre és törlöd (például egy dinamikus gomblista), a törlésnél két dolgot kell tenni: leiratkozni a kezelőkről (ha a kezelő hosszabb életű objektumhoz tartozik), és felszabadítani a vezérlőt (Dispose()). Egy gyakori csapda: a Controls.Clear() csak kiveszi a vezérlőket a gyűjteményből, de nem szabadítja fel őket:
private void btnLetrehoz_Click(object sender, EventArgs e)
{
for (int i = 1; i <= 5; i++)
{
Button gomb = new Button();
gomb.Text = "Gomb " + i;
gomb.Location = new Point(10, 10 + (i - 1) * 40);
gomb.Click += Dinamikus_Click; // a kezelő az ablak metódusa
pnlGombok.Controls.Add(gomb);
}
}
private void Dinamikus_Click(object sender, EventArgs e)
{
lblInfo.Text = "Megnyomtad: " + ((Button)sender).Text;
}
private void btnTorol_Click(object sender, EventArgs e)
{
// ROSSZ: pnlGombok.Controls.Clear(); // kiveszi, de nem szabadítja fel
// JÓ: visszafelé, leiratkozással és felszabadítással
for (int i = pnlGombok.Controls.Count - 1; i >= 0; i--)
{
Control vezerlo = pnlGombok.Controls[i];
if (vezerlo is Button gomb)
{
gomb.Click -= Dinamikus_Click;
}
pnlGombok.Controls.RemoveAt(i);
vezerlo.Dispose();
}
}Controls gyűjtemény eltávolítás közben változik, ezért a visszafelé haladó ciklus biztonságos (5.6 lecke).-= mégis jó szokás, ha a vezérlő hosszabb élettartamú objektumra (például egy statikus osztályra) is feliratkozott.Jósolj!
Százszor egymás után létrehozol és a Controls.Clear() metódussal törölsz ötven gombot. Mit tapasztalsz a Feladatkezelőben a program memóriahasználatánál és a GDI-objektumok számánál? (Segítség: a Details nézetben a GDI objects oszlop megjeleníthető.)
Jóslat ellenőrzése
A felszabadítatlan gombok erőforrásai (a vezérlők ablakkezelői és rajzolási objektumai) halmozódnak: a memóriahasználat és a GDI-/USER-objektumok száma folyamatosan nő, és ha eléri a Windows korlátját (folyamatonként általában 10 000 GDI objektum), hibákat (például Error creating window handle) kapsz. A Dispose() hívásával a szám visszaáll. A pontos számokat érdemes a saját gépeden kipróbálni.
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. Egyre többször fut le a kezelő
private void btnBeallit_Click(object sender, EventArgs e)
{
btnCel.Click += CelKezelo; // minden kattintás újabb feliratkozást ad
}Hiba és javítás
A feliratkozás egy gyakran lefutó kezelőbe került. Vidd a Load eseménybe (egyszer fusson), vagy iratkozz le feliratkozás előtt (btnCel.Click -= CelKezelo; btnCel.Click += CelKezelo;), így mindig pontosan egy feliratkozás marad.
2. ObjectDisposedException a küldő kódban
A hírközpont A bezárt ablak kezelője feliratkozva maradt a statikus eseményre. Iratkozz le az ablak Kuld metódusa hibát dob, amely egy már bezárt ablak vezérlőjére utal.Ok és javítás
FormClosed eseményében (Hirkozlo.HirErkezett -= HirKezelo;). A FeliratkozokSzama segít ellenőrizni, hogy a szám csökken-e bezáráskor.
3. A leiratkozás nem működik
btnProba.Click += (s, e) => MessageBox.Show("Szia");
btnProba.Click -= (s, e) => MessageBox.Show("Szia"); // két külön lambda!Hiba és javítás
A második lambda egy új, másik delegált, ezért a -= nem talál feliratkozást, és nem ad hibát. Tárold a lambdát változóban (EventHandler kezelo = (s, e) => ...;), vagy használj névvel ellátott metódust.
4. Egy hibás feliratkozó elnémít másokat
Az értesítést két ablak is figyeli, de ha az első hibát dob, a második soha nem kapja meg a hírt. A kivétel megszakítja a kiváltást, és a többi kezelő nem fut le. A kibocsátó oldalán a Ok és javítás
GetInvocationList() segítségével hívd meg a kezelőket egyenként, try–catch védelemmel, és naplózd a hibát. A feliratkozó oldalán a kezelő belül kezelje a hibát.
5. A memóriahasználat folyamatosan nő
A program ablakok nyitásával és zárásával egyre több memóriát használ. A bezárt ablakok feliratkozva maradtak egy hosszú életű kibocsátóra (statikus esemény, főablak, Ok és javítás
SystemEvents), ezért a szemétgyűjtő nem tudja őket felszabadítani. Iratkozz le a FormClosed eseményben, és ellenőrizd a feliratkozók számát.
6. A Controls.Clear nem szabadít fel
pnlGombok.Controls.Clear(); // a gombok eltűnnek, de nem szabadulnak felHiba és javítás
A Clear() csak kiveszi a vezérlőket. Járd be visszafelé a gyűjteményt, és minden vezérlőre hívd a Dispose() metódust (a kezelőkről előbb iratkozz le, ha szükséges).
Mini projekt: hírközpont figyelő ablakokkal
Készíts programot, amelyben a főablak híreket küld, és tetszőleges számú figyelő ablak fogadja őket:
- Egy statikus
Hirkozloosztály:HirErkezettesemény (EventHandler<HirEventArgs>),FeliratkozokSzamatulajdonság és a hibatűrőKuldmetódus (a hibás kezelők számát adja vissza). - A főablakon:
txtHir(a hír szövege),btnKuld(küldés),btnUjFigyelo(új figyelő ablak nyitása),lblAllapot(a feliratkozók és a nyitott figyelők száma, a hibás kezelők száma). - A
FigyeloAblakaLoad-ban feliratkozik, aFormClosed-ben leiratkozik, a hírt egylstHireklistába írja időponttal. EgychkHibajelölőnégyzet bekapcsolt állapotában a kezelő szándékosan kivételt dob (a hibatűrés kipróbálásához). - A főablak a figyelő ablakok
FormClosedeseményére iratkozik fel, és a bezáráskor frissíti alblAllapotfeliratot. - A hírküldés után a főablak kiírja: Elküldve 3 figyelőnek, 1 hibás.
- Teszteld: több ablak nyitása és zárása (a számláló nő és csökken), hiba bekapcsolása az egyik ablakban (a többi megkapja a hírt), hír küldése figyelők nélkül.
Megoldás megnyitása (próbáld ki előbb önállóan!)
A Hirkozlo a leckében látott Kuld és FeliratkozokSzama tagokat tartalmazza. A nyitott figyelők számát a főablak külön mezőben (nyitottFigyelok) tartja nyilván, így a két szám (feliratkozók és nyitott ablakok) összevethető: ha eltér, hiányzik egy leiratkozás.
// FigyeloAblak.cs
public partial class FigyeloAblak : Form
{
public FigyeloAblak()
{
InitializeComponent();
}
private void FigyeloAblak_Load(object sender, EventArgs e)
{
Hirkozlo.HirErkezett += HirKezelo;
}
private void FigyeloAblak_FormClosed(object sender, FormClosedEventArgs e)
{
Hirkozlo.HirErkezett -= HirKezelo;
}
private void HirKezelo(object sender, HirEventArgs e)
{
if (chkHiba.Checked)
{
throw new InvalidOperationException("Szándékos hiba a figyelőben.");
}
lstHirek.Items.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " – " + e.Szoveg);
}
}
// Form1.cs
private int nyitottFigyelok = 0;
private void AllapotFrissit()
{
lblAllapot.Text = $"Nyitott figyelők: {nyitottFigyelok} | Feliratkozók: {Hirkozlo.FeliratkozokSzama}";
}
private void btnUjFigyelo_Click(object sender, EventArgs e)
{
FigyeloAblak ablak = new FigyeloAblak();
ablak.FormClosed += (s, ev) =>
{
nyitottFigyelok = nyitottFigyelok - 1;
AllapotFrissit();
};
nyitottFigyelok = nyitottFigyelok + 1;
ablak.Show(this);
AllapotFrissit();
}
private void btnKuld_Click(object sender, EventArgs e)
{
string hir = txtHir.Text.Trim();
if (hir.Length == 0)
{
lblAllapot.Text = "Írj be egy hírt!";
return;
}
int felirat = Hirkozlo.FeliratkozokSzama;
int hibas = Hirkozlo.Kuld(hir);
lblAllapot.Text = $"Elküldve {felirat} figyelőnek, {hibas} hibás.";
}Mit tegyél ide: a főablak és három nyitott figyelő ablak, az egyikben a hibajelzővel; a főablakon a „Elküldve 3 figyelőnek, 1 hibás.” felirat.Javasolt fájlnév: winforms-5-7-kesz.webp · Alt szöveg: A hírközpont figyelő ablakokkal
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. Az első betöltés jelzése
A A névvel ellátott változatban a kezelő a Szamlalo osztály ElerteAHatart eseményére iratkozz fel úgy, hogy az üzenet csak az első alkalommal jelenjen meg, utána a kezelő magától leiratkozzon. Írd meg lambdával és névvel ellátott metódussal is.Egy lehetséges megoldás
-= operátorral iratkozik le a szamlalo eseményéről. A lambdánál a kezelőt előbb változóban deklaráljuk (null értékkel), hogy hivatkozhasson önmagára.// névvel ellátott változat
private void ElsoHatar(object sender, EventArgs e)
{
lblUzenet.Text = "Először értük el a határt!";
szamlalo.ElerteAHatart -= ElsoHatar;
}
// feliratkozás: szamlalo.ElerteAHatart += ElsoHatar;
// lambdás változat
EventHandler egyszeri = null;
egyszeri = (s, ev) =>
{
lblUzenet.Text = "Először értük el a határt!";
szamlalo.ElerteAHatart -= egyszeri;
};
szamlalo.ElerteAHatart += egyszeri;
2. Dinamikus gombok helyes törlése
Egy Az ellenőrzés egyik módja a numGombok mező megadja, hány gomb jöjjön létre egy panelen. A Frissít gomb előbb törölje az összes régi gombot (leiratkozással és felszabadítással), majd hozza létre az újakat. Hogyan ellenőrzöd, hogy nincs szivárgás?Egy lehetséges megoldás
pnlGombok.Controls.Count kiírása (a ténylegesen a panelen lévő vezérlők száma), a felszabadítás ellenőrzése pedig a Feladatkezelőben a GDI objektumok számának megfigyelése sokszori frissítés után (nem nő folyamatosan).private void Torol()
{
for (int i = pnlGombok.Controls.Count - 1; i >= 0; i--)
{
Control vezerlo = pnlGombok.Controls[i];
if (vezerlo is Button gomb)
{
gomb.Click -= Dinamikus_Click;
}
pnlGombok.Controls.RemoveAt(i);
vezerlo.Dispose();
}
}
private void btnFrissit_Click(object sender, EventArgs e)
{
Torol();
for (int i = 0; i < (int)numGombok.Value; i++)
{
Button gomb = new Button();
gomb.Text = (i + 1).ToString();
gomb.Location = new Point(10 + i * 60, 10);
gomb.Size = new Size(50, 30);
gomb.Click += Dinamikus_Click;
pnlGombok.Controls.Add(gomb);
}
lblDb.Text = "Vezérlők a panelen: " + pnlGombok.Controls.Count;
}
3. Pontosan egy feliratkozás
Egy A bekapcsoláskor előbb leiratkozunk (hatástalan, ha nincs feliratkozva), utána feliratkozunk, így a szám legfeljebb egy. A kikapcsoláskor leiratkozunk.chkFigyel jelölőnégyzet be- és kikapcsolja a feliratkozást a Hirkozlo eseményére. Hogyan biztosítod, hogy akárhányszor kapcsolgatod, mindig pontosan egy vagy nulla feliratkozás legyen? Ellenőrizd a FeliratkozokSzama értékével.Egy lehetséges megoldás
private void chkFigyel_CheckedChanged(object sender, EventArgs e)
{
Hirkozlo.HirErkezett -= HirKezelo; // biztosan nincs duplán
if (chkFigyel.Checked)
{
Hirkozlo.HirErkezett += HirKezelo;
}
lblSzam.Text = "Feliratkozók: " + Hirkozlo.FeliratkozokSzama;
}
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ért kell leiratkozni?
Két mondatban magyarázd el, miért kell egy gyermekablaknak leiratkoznia egy statikus eseményről, a saját gombjának eseményéről viszont nem. A statikus esemény az egész program életében létezik, és hivatkozik a feliratkozóra, ezért a bezárt ablak nem szabadulhat fel, és tovább kapja az eseményeket. A saját gombja az ablakkal együtt szűnik meg, így a köztük lévő kapcsolat nem tartja életben az ablakot.Megoldás megnyitása
2. Pontosan egyszer
Egy Előbb leiratkozás, utána feliratkozás. A leiratkozás hatástalan, ha nincs feliratkozva.btnFelvesz gomb kattintásakor a program feliratkozik a tmrOra.Tick eseményre. Írd át úgy, hogy többszöri kattintás után is egyetlen feliratkozás legyen.Megoldás megnyitása
private void btnFelvesz_Click(object sender, EventArgs e)
{
tmrOra.Tick -= OraKezelo;
tmrOra.Tick += OraKezelo;
tmrOra.Start();
}
Közepes szint
3. A dinamikus gomb törlése egyenként
Kódból létrehozott gombok mellett egy Törlés felirat minden gombra kattintva törölje az adott gombot (leiratkozással és felszabadítással). Mire kell vigyázni, hogy a gomb a saját kezelőjében törli magát? Az éppen futó kezelő a Megoldás megnyitása
Dispose() hívása után még lefut a végéig, ezért a Dispose() legyen az utolsó utasítás a kezelőben, és utána a gombra ne hivatkozz. A leiratkozás előtte történjen.private void Torlo_Click(object sender, EventArgs e)
{
Button gomb = (Button)sender;
gomb.Click -= Torlo_Click;
pnlGombok.Controls.Remove(gomb);
gomb.Dispose(); // az utolsó utasítás: ezután a gombot nem használjuk
}
4. Időzítő leállítása és felszabadítása
Egy kódból létrehozott A Timer a gyermekablakban fut. Mit kell tenni az ablak bezárásakor, hogy a Timer ne fusson tovább?Megoldás megnyitása
FormClosed eseményben le kell állítani (Stop()), le kell iratkozni (Tick -= ...) és fel kell szabadítani (Dispose()). A tervezőben elhelyezett Timer-t az ablak a saját components gyűjteményén keresztül szabadítja fel, a kódból létrehozottnál ezt neked kell megtenned.private Timer ido = null;
private void Form1_Load(object sender, EventArgs e)
{
ido = new Timer();
ido.Interval = 1000;
ido.Tick += IdoKezelo;
ido.Start();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
ido.Stop();
ido.Tick -= IdoKezelo;
ido.Dispose();
}
Kihívás szint
5. Szavazó esemény: bármelyik kezelő megtilthatja
Egy A kiváltás után a Dokumentum osztály Bezaras eseménye CancelEventArgs-ot kap. Ha bármelyik feliratkozó e.Cancel = true értékre állítja, a Bezar() metódus nem zárja be a dokumentumot. Hogyan váltod ki az eseményt?Megoldás megnyitása
Cancel értékét vizsgáljuk. Mivel az e egy objektum, minden feliratkozó ugyanazt módosítja: ha bármelyik true értékre állítja, az érték igaz marad. A CancelEventHandler és a CancelEventArgs a System.ComponentModel névtérben van.using System.ComponentModel;
public class Dokumentum
{
public bool Nyitva { get; private set; } = true;
public event CancelEventHandler Bezaras;
public bool Bezar()
{
CancelEventArgs e = new CancelEventArgs();
Bezaras?.Invoke(this, e);
if (e.Cancel)
{
return false; // valaki megtiltotta
}
Nyitva = false;
return true;
}
}
6. Szivárgás-kísérlet WeakReference-szel
Egy kísérlettel mutasd meg, hogy a bezárt, de feliratkozva maradt ablak nem szabadul fel, míg a leiratkozott igen. Mire kell vigyázni a kísérlet megírásánál? A Megoldás megnyitása
WeakReference egy objektumra mutat anélkül, hogy életben tartaná. Az IsAlive megmondja, felszabadult-e. A szemétgyűjtés időzítése nem garantált, ezért a kísérletet külön metódusban kell elvégezni (hogy a helyi változók ne tartsák életben az ablakot), és a hibakereső vagy a Debug módban az eredmény eltérhet (a helyi változók tovább élnek). Release módban futtasd. A kísérlet célja szemléltetés, nem pontos mérés. A FigyeloAblak kap egy public bool Leiratkozik { get; set; } = true; tulajdonságot, és a FormClosed eseményben csak akkor hajtja végre a -= műveletet, ha ez igaz. A két futtatás (false és true) összehasonlítása mutatja a különbséget.private WeakReference Letrehoz(bool leiratkozik)
{
FigyeloAblak ablak = new FigyeloAblak();
ablak.Leiratkozik = leiratkozik; // a FigyeloAblak.FormClosed csak ekkor iratkozik le
ablak.Show();
ablak.Close();
return new WeakReference(ablak);
}
private void btnProba_Click(object sender, EventArgs e)
{
WeakReference gyenge = Letrehoz(false); // próbáld ki true értékkel is, és hasonlítsd össze
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
lblEredmeny.Text = gyenge.IsAlive ? "Az ablak még él (szivárgás?)" : "Az ablak felszabadult";
}
Gyors önellenőrzés
Válaszolj fejben, és csak utána nyisd le a választ. Hivatkozást a kezelőre (delegáltat), ezzel a feliratkozó objektumra is. Ha a kibocsátó hosszabb életű, mint a feliratkozó (statikus esemény, főablak és gyermekablak): a feliratkozó nem szabadul fel, és bezárva is megkapja az eseményeket. A Előbb deklarálod a változót ( A kiváltás megszakad, a többi kezelő nem fut le. A Csak kiveszi a vezérlőket, de nem szabadítja fel őket: Feliratkozás előtt leiratkozol (1. Mit tárol a kibocsátó egy feliratkozásról?
2. Mikor okoz gondot a feliratkozás?
3. Hol iratkozz le egy ablakban?
FormClosed eseményben, a Load-ban történt feliratkozás párjaként.4. Hogyan írsz önleiratkozó lambdát?
EventHandler h = null;), utána megadod a törzsét, amely a -= h sorral iratkozik le.5. Mi történik, ha egy kezelő kivételt dob?
GetInvocationList() és a try–catch teszi hibatűrővé.6. Miért nem elég a Controls.Clear()?
Dispose() kell minden vezérlőre.7. Hogyan garantálod, hogy pontosan egy feliratkozás van?
-= majd +=): a leiratkozás hatástalan, ha nincs feliratkozva.
Mit tudsz már?
- Érted, hogy a feliratkozás kapcsolat, amelyet le kell bontani.
- Párban kezeled a fel- és leiratkozást (
Load/FormClosed). - Önleiratkozó kezelőt és hibatűrő eseménykiváltást írsz.
- Megszámolod a feliratkozókat, és kezeled a kódból létrehozott vezérlők életciklusát.
- Felismered a halmozódó feliratkozás, a bezárt ablak és a
Controls.Clearcsapdáit.
Következő lecke: egy eseménykezelő több vezérlőelemhez
Ha sok hasonló vezérlő (például egy számológép gombjai) ugyanazt csinálja, egyetlen kezelő is elég. Megnézzük, hogyan, és mire kell vigyázni.



