Grafikus C# – Windows Forms tanfolyam › 5.7 lecke

Windows Forms · 5.7 lecke · feliratkozás és leiratkozás

Események összekapcsolása, feliratkozás és leiratkozás

Az 5.1 leckében megtanultad a += é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.

  • kb. 110 perc
  • haladó szint
  • feliratkozások kezelése
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

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.Clear csapdá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.

5.6 Az ablak életciklusa →

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:

Hirkozlo.csC#
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; }
}
FigyeloAblak.cs (a hibás változat)C#
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):

FigyeloAblak.csC#
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);
}
Load és FormClosedA feliratkozás az ablak életének elején, a leiratkozás a végén történik. A FormClosed biztos bezárás (a FormClosing megszakítható, ott ne iratkozz le).
Metódus-hivatkozás és -=A -= 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.
Minden feliratkozónak saját párHa egy ablak több eseményre is feliratkozik, mindegyiket le kell bontani. Érdemes a leiratkozásokat egy helyre (egy Leiratkozas() metódusba) gyűjteni.
Azonos élettartam eseténHa az ablak a saját vezérlője eseményére iratkozik fel (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:

Hirkozlo.csC#
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:

Form1.csC#
// 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;
EventHandler egyszeri = null;A változót előbb létre kell hozni, különben a lambda törzsében nem hivatkozhatnál rá (a fordító azt mondja: „használat hozzárendelés előtt”). A második sorban kapja meg az értékét.
btnMasik.Click -= egyszeriA lambda a változón keresztül hivatkozik önmagára, és leiratkozik. A változó értéke a delegált, ugyanaz, mint amelyik feliratkozott, ezért a -= megtalálja.
Miért nem működik a névtelen lambda?Ha a lambdát közvetlenül a += 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.
Mikor jó?Egyszeri események (első kattintás, első betöltés, „kész” jelzés) és átmeneti állapotok esetén.

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:

Hirkozlo.csC#
// 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
}
GetInvocationList()Az esemény feliratkozóinak (delegáltjainak) tömbje, a feliratkozás sorrendjében. Mindegyiket külön meg lehet hívni.
Másolat (kezelok)Egy kezelő a hívás közben leiratkozhat (például az önleiratkozó). A másolat a hívás kezdetén lévő állapotot őrzi, ezért a bejárás biztonságos.
(EventHandler<HirEventArgs>)dA Delegate típusú elemet vissza kell alakítani a pontos típusra, hogy paraméterekkel hívható legyen.
Mikor kell?Ha a kibocsátó nem ismeri a feliratkozókat, és nem engedheti, hogy egy külső hiba megakassza a többit (naplózás, értesítések). A vezérlők saját eseményeinél a Windows Forms nem ezt a mintát használja.

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:

Form1.cs (pnlGombok: Panel)C#
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.Clear()Kiveszi a vezérlőket a gyűjteményből, így eltűnnek a felületről, de a vezérlők (és az operációs rendszer erőforrásaik) élnek tovább. Sok vezérlő ismételt létrehozásánál ez memória- és erőforrás-szivárgást okoz.
Dispose()Felszabadítja a vezérlő erőforrásait (ablakkezelő, rajzolási objektumok), és a vezérlő már nem használható.
Visszafelé haladásA Controls gyűjtemény eltávolítás közben változik, ezért a visszafelé haladó ciklus biztonságos (5.6 lecke).
LeiratkozásItt a kezelő az ablak metódusa, és a vezérlő a panelé: együtt élnek, de a -= 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 Kuld metódusa hibát dob, amely egy már bezárt ablak vezérlőjére utal.

Ok és javítás

A bezárt ablak kezelője feliratkozva maradt a statikus eseményre. Iratkozz le az ablak 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.

Ok és javítás

A kivétel megszakítja a kiváltást, és a többi kezelő nem fut le. A kibocsátó oldalán a 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.

Ok és javítás

A bezárt ablakok feliratkozva maradtak egy hosszú életű kibocsátóra (statikus esemény, főablak, 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 fel
Hiba é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:

  1. Egy statikus Hirkozlo osztály: HirErkezett esemény (EventHandler<HirEventArgs>), FeliratkozokSzama tulajdonság és a hibatűrő Kuld metódus (a hibás kezelők számát adja vissza).
  2. 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).
  3. A FigyeloAblak a Load-ban feliratkozik, a FormClosed-ben leiratkozik, a hírt egy lstHirek listába írja időponttal. Egy chkHiba jelölőnégyzet bekapcsolt állapotában a kezelő szándékosan kivételt dob (a hibatűrés kipróbálásához).
  4. A főablak a figyelő ablakok FormClosed eseményére iratkozik fel, és a bezáráskor frissíti a lblAllapot feliratot.
  5. A hírküldés után a főablak kiírja: Elküldve 3 figyelőnek, 1 hibás.
  6. 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 és Form1.csC#
// 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.";
}
📷 Képernyőkép helye

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 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

A névvel ellátott változatban a kezelő a -= 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.

Form1.csC#
// 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 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

Az ellenőrzés egyik módja a 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).

Form1.csC#
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 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

A bekapcsoláskor előbb leiratkozunk (hatástalan, ha nincs feliratkozva), utána feliratkozunk, így a szám legfeljebb egy. A kikapcsoláskor leiratkozunk.

Form1.csC#
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.

Megoldás megnyitása

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.

2. Pontosan egyszer

Egy 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

Előbb leiratkozás, utána feliratkozás. A leiratkozás hatástalan, ha nincs feliratkozva.

Form1.csC#
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?

Megoldás megnyitása

Az éppen futó kezelő a 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.

Form1.csC#
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 Timer a gyermekablakban fut. Mit kell tenni az ablak bezárásakor, hogy a Timer ne fusson tovább?

Megoldás megnyitása

A 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.

Form1.csC#
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 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

A kiváltás után a 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.

Dokumentum.csC#
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?

Megoldás megnyitása

A 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.

Form1.csC#
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.

1. Mit tárol a kibocsátó egy feliratkozásról?

Hivatkozást a kezelőre (delegáltat), ezzel a feliratkozó objektumra is.

2. Mikor okoz gondot a feliratkozás?

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.

3. Hol iratkozz le egy ablakban?

A FormClosed eseményben, a Load-ban történt feliratkozás párjaként.

4. Hogyan írsz önleiratkozó lambdát?

Előbb deklarálod a változó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?

A kiváltás megszakad, a többi kezelő nem fut le. A GetInvocationList() és a try–catch teszi hibatűrővé.

6. Miért nem elég a Controls.Clear()?

Csak kiveszi a vezérlőket, de nem szabadítja fel őket: Dispose() kell minden vezérlőre.

7. Hogyan garantálod, hogy pontosan egy feliratkozás van?

Feliratkozás előtt leiratkozol (-= 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.Clear csapdá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.