Grafikus C# – Windows Forms tanfolyam › 8.4 lecke Windows Forms · 8.4 lecke · adatkötés alapjai Eddig a vezérlők maguk tárolták az adatot (List, BindingList és BindingSource
Items, Rows). Egy komolyabb programban az adat külön él (egy gyűjteményben), a vezérlő pedig csak megjeleníti. Ez az adatkötés (data binding) alapgondolata: a tábla vagy lista a gyűjteményhez kapcsolódik, és ha az adat változik, a megjelenítés magától frissül. Ebben a leckében megismered a List<T>, a BindingList<T> és a BindingSource szerepét, az INotifyPropertyChanged felületet, és a legfontosabb szabályt: a kötéshez tulajdonság kell, nem mező.
A lecke végére
- elkülönítöd az adatot (gyűjtemény) a megjelenítéstől (vezérlő);
- tudod, miért nem frissül a tábla
List<T>hozzáadásakor, és hogyan oldja meg aBindingList<T>; - megvalósítod az
INotifyPropertyChangedfelületet, hogy az objektum módosítása látsszon; - a
BindingSourcesegítségével kezeled az aktuális elemet (Current,Position,RemoveCurrent); - tudod, hogy a kötéshez nyilvános tulajdonság kell, és elrejtesz egy oszlopot (
Browsable); - felismered a gyakori hibákat (mező,
Listfrissítés, számított tulajdonság).
Mielőtt elkezded
Ehhez a leckéhez a 8.3 lecke (DataGridView), a 3.9 lecke (osztályok, tulajdonságok), az 5.1 és 5.2 lecke (események, EventArgs) és a 7.7 lecke (adat és felület szétválasztása) szükséges.
List<T> és BindingList<T>
A List<T> az adat tárolására kiváló, de nem értesít a változásról: ha egy kötött DataGridView forrása List<T>, és hozzáadsz egy elemet, a tábla nem frissül. A BindingList<T> ugyanaz, de értesítést küld (ListChanged esemény) minden hozzáadáskor, törléskor és módosításkor, ezért a tábla azonnal követi:
// 1. List<T>: a tábla nem frissül a hozzáadáskor
List<Termek> sima = new List<Termek>();
dgv.DataSource = sima;
sima.Add(new Termek { Nev = "Alma", Ar = 250 }); // a tábla ezt nem látja!
// a megoldás: a forrás újrakötése (működik, de a kijelölés és a görgetés elvész)
dgv.DataSource = null;
dgv.DataSource = sima;
// 2. BindingList<T>: a tábla magától frissül
BindingList<Termek> lista = new BindingList<Termek>();
dgv.DataSource = lista;
lista.Add(new Termek { Nev = "Körte", Ar = 320 }); // a tábla azonnal mutatja
lista.RemoveAt(0); // a törlés is látszik
lista.ListChanged += (s, e) =>
{
lblDarab.Text = lista.Count + " termék"; // a változásra reagálhatunk is
};List<T> nem valósítja meg az értesítő felületet (IBindingList), ezért a tábla nem tud a változásról.DataSource = null majd újra beállítás frissít, de elveszíti a kijelölést, a görgetést és az oszlopbeállításokat: csak szükségmegoldás.System.ComponentModel névtérben van (using!). A ListChanged esemény e.ListChangedType mezője megmondja, mi történt (ItemAdded, ItemDeleted, ItemChanged, Reset).Jósolj!
A dgv.DataSource egy List<Termek>, és a program a lista végére hozzáad egy elemet, de a DataSource újrabeállítása nélkül. Mit mutat a tábla?
Jóslat ellenőrzése
A tábla a régi állapotot mutatja (az új elem nem látszik, amíg valami újrarajzolásra vagy újrakötésre nem készteti, de akkor sem biztosan). A List<T> nem küld értesítést. Használj BindingList<T> gyűjteményt.
INotifyPropertyChanged: az objektum módosítása is látszik
A BindingList a lista változásairól értesít (hozzáadás, törlés). Ha viszont egy elem tulajdonságát módosítod kódból (lista[0].Ar = 300;), a cella nem frissül, hacsak az elem is jelez. Ehhez az osztálynak az INotifyPropertyChanged felületet kell megvalósítania: a set ágban eseményt vált ki, a BindingList pedig ezt figyeli:
public class Termek : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string nev = "";
private int ar = 0;
private int keszlet = 0;
public string Nev
{
get { return nev; }
set { if (nev != value) { nev = value; Jelez("Nev"); } }
}
public int Ar
{
get { return ar; }
set { if (ar != value) { ar = value; Jelez("Ar"); Jelez("Ertek"); } } // az Ertek is változik
}
public int Keszlet
{
get { return keszlet; }
set { if (keszlet != value) { keszlet = value; Jelez("Keszlet"); Jelez("Ertek"); } }
}
// számított, csak olvasható tulajdonság
public int Ertek
{
get { return Ar * Keszlet; }
}
private void Jelez(string tulajdonsag)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(tulajdonsag));
}
}set ága kiváltja a PropertyChanged eseményt a tulajdonság nevével. A feltétel (if (ar != value)) elkerüli a felesleges jelzést.Ertek az Ar és Keszlet szorzata. Ha azok változnak, az Ertek is: ezért jelezzük őt is a set ágakban.?.Invoke védi a feliratkozó nélküli esetet (5.1 lecke).get-es tulajdonság a táblában csak olvasható oszlop (a felhasználó nem szerkesztheti).Jósolj!
A Termek osztály nem valósítja meg az INotifyPropertyChanged felületet, a lista BindingList<Termek>. A kód: lista[0].Ar = 999;. Mit látsz a táblában?
Jóslat ellenőrzése
A cella a régi értéket mutatja, amíg valami újrarajzolásra nem készteti (például görgetés vagy kattintás): az objektum módosult, de nem jelzett. Az INotifyPropertyChanged megvalósítása azonnali frissítést ad. A tábla szerkesztéséből (a felhasználó beírásából) származó módosítás viszont mindig érvényesül az objektumban.
BindingSource: a közvetítő
A BindingSource egy komponens (a tervező alsó sávján), amely a gyűjtemény és a vezérlők közé áll. Előnyei: egyetlen helyen tárolja az aktuális elemet (Current, Position), több vezérlő is ugyanahhoz kapcsolódik, és kényelmes műveleteket ad (RemoveCurrent, MoveNext):
private BindingList<Termek> lista = new BindingList<Termek>();
private BindingSource bs = new BindingSource(); // vagy a tervezőben a komponens
private void Form1_Load(object sender, EventArgs e)
{
bs.DataSource = lista; // a forrás a gyűjtemény
dgv.DataSource = bs; // a tábla a BindingSource-hoz kötődik
bs.PositionChanged += bs_PositionChanged; // az aktuális elem változott
}
private void bs_PositionChanged(object sender, EventArgs e)
{
lblPoz.Text = (bs.Position + 1) + " / " + bs.Count;
}
// az aktuális elem
Termek aktualis = bs.Current as Termek; // null, ha nincs (üres lista)
if (aktualis != null)
{
txtNev.Text = aktualis.Nev;
}
bs.RemoveCurrent(); // az aktuális elem törlése (a gyűjteményből is)
bs.MoveNext(); // következő elemre lép
bs.MovePrevious();
bs.MoveFirst();
bs.Add(new Termek { Nev = "Szilva", Ar = 400 }); // hozzáadás a gyűjteményhez
bs.ResetBindings(false); // kényszerített frissítés, ha mégis szükségesCurrent az aktuális elem (object), a Position a sorszáma (-1, ha nincs). A tábla kijelölése és a Position összekapcsolódik: a sor kijelölése átállítja a Position értékét.BindingSource-t kötöd, mindegyik az aktuális elemet mutatja (8.6 lecke).bs.Count > 0).BindingList közvetlenül. A BindingSource akkor kell, ha az aktuális elemet több helyen használod, szűrni, rendezni vagy lapozni szeretnél (8.7 lecke).Jósolj!
A lista üres, és a gomb kezelője bs.RemoveCurrent();. Mi történik?
Jóslat ellenőrzése
Üres listán nincs aktuális elem: a RemoveCurrent InvalidOperationException kivételt dobhat. Előbb ellenőrizd: if (bs.Current != null) { bs.RemoveCurrent(); }.
Tulajdonság, nem mező
Az adatkötés nyilvános tulajdonságokat ({ get; set; }) használ. A nyilvános mező (public string Nev;) nem látszik az automatikusan képzett oszlopokban! Az előző leckékben egyszerűség kedvéért használtunk mezőket (Szam.Cim), ott nem volt adatkötés. Kötött adatnál az osztály mindig tulajdonságokat használ:
public class RosszTermek
{
public string Nev = ""; // MEZŐ: a tábla nem mutatja
}
public class JoTermek
{
public string Nev { get; set; } = ""; // tulajdonság: kötésre alkalmas
[Browsable(false)] // ezt az oszlopot a tábla ne mutassa
public int Azonosito { get; set; }
[DisplayName("Ár (Ft)")] // a fejléc szövege (using System.ComponentModel;)
public int Ar { get; set; }
}{ get; set; } alak a legrövidebb tulajdonság. Ha nem kell jelzés (INotifyPropertyChanged) és ellenőrzés, ez elég.[Browsable(false)] jellemző (attribútum) elrejti a tulajdonságot az automatikus oszlopok közül. Hasznos azonosítókhoz és belső adatokhoz.[DisplayName("...")] a fejléc szövegét adja. A tábla magyar, szép fejlécet mutat, a kódban pedig megmarad a rövid tulajdonságnév.public string Nev { get; set; }, és a használó kód nem változik (a obj.Nev hívás ugyanaz).Jósolj!
A RosszTermek osztály mezőt használ, és a dgv.DataSource = new BindingList<RosszTermek>(...), az AutoGenerateColumns igaz. Mit látsz?
Jóslat ellenőrzése
Egy üres tábla (nincs oszlop, vagy sorok üres cellákkal), mert a nyilvános mezők nem tulajdonságok, és az adatkötés nem látja őket. A javítás: a mező cseréje tulajdonságra.
Mit tegyél ide: a termékkészlet tábla az Érték oszloppal, a lapozó gombokkal és a „3 / 8” felirattal.Javasolt fájlnév: winforms-8-4-bindinglist.webp · Alt szöveg: Kötött DataGridView BindingList-tel
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 tábla nem mutatja az új elemet
A A List<T> végére tettél egy elemet, de a tábla változatlan.Ok és javítás
List<T> nem értesít. Használj BindingList<T> gyűjteményt.
2. Üres a tábla, pedig van adat
public class Termek
{
public string Nev; // mező
public int Ar;
}Hiba és javítás
A kötés tulajdonságot vár: public string Nev { get; set; }, public int Ar { get; set; }.
3. A kódból módosított érték nem látszik a cellában
A Az osztály nem jelez. Valósítsd meg az lista[0].Ar = 999; után a cella a régi értéket mutatja.Ok és javítás
INotifyPropertyChanged felületet, és hívd a Jelez("Ar") metódust a set ágban. Gyors megoldás: lista.ResetItem(0);.
4. Az Érték oszlop nem frissül
Az A számított tulajdonság változását is jelezni kell: az Ar módosítása után az Ertek (számított) régi marad.Ok és javítás
Ar és Keszlet set ágában Jelez("Ertek") is kell.
5. InvalidOperationException a RemoveCurrent-nél
A törlő gomb üres listán összeomlik. Nincs aktuális elem. Ellenőrizd a Ok és javítás
bs.Current != null feltételt, és tiltsd a gombot üres listán.
6. Az oszlopok duplán jelennek meg
Kézzel adtál oszlopokat, és a kötés is képzett. Az Ok és javítás
AutoGenerateColumns = false beállítás mellett az oszlopok DataPropertyName tulajdonsága kapcsolja a tulajdonsághoz (8.6 lecke).
Mini projekt: termékkészlet
Készíts kötött táblázatot termékekkel:
Termekosztály (INotifyPropertyChanged):Nev,Ar,Keszletés a számítottErtek(Ar * Keszlet);[DisplayName]fejlécek; egyAzonosito[Browsable(false)].- A
BindingList<Termek>aBindingSource-on át adgv-hez kötve; a táblaFullRowSelect, a Érték oszlop csak olvasható. - Az Új termék gomb (
txtNev,nudAr,nudKeszlet) hozzáad aBindingSource-hoz, és az új sor lesz kijelölve. - A Törlés gomb a
RemoveCurrentmetódust használja megerősítéssel (üres listán tiltva). - Az Árak +10% gomb a kódból módosítja az összes termék árát (az
INotifyPropertyChangedmiatt a tábla azonnal frissül, az Érték oszlop is). - A ◀ és ▶ gombok a
MovePreviousésMoveNextmetódusokkal lépkednek; alblPozmutatja: 3 / 12 (PositionChanged). - Az
lblOssza készlet összértékét mutatja aListChangedeseményben (a gyűjteményből számolva, nem a celláiból). - Teszteld: hozzáadás, törlés, üres lista, az árak emelése (az összérték is változik), lépkedés, a tábla szerkesztése (az összérték követi).
Megoldás megnyitása (próbáld ki előbb önállóan!)
Az összérték a lista elemeiből számolódik, és a ListChanged esemény frissíti (az ItemChanged a tulajdonságváltozásra is lefut, ha az elem INotifyPropertyChanged).
public class Termek : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string nev = "";
private int ar = 0;
private int keszlet = 0;
[Browsable(false)]
public int Azonosito { get; set; }
[DisplayName("Név")]
public string Nev { get { return nev; } set { if (nev != value) { nev = value; Jelez("Nev"); } } }
[DisplayName("Ár (Ft)")]
public int Ar { get { return ar; } set { if (ar != value) { ar = value; Jelez("Ar"); Jelez("Ertek"); } } }
[DisplayName("Készlet")]
public int Keszlet { get { return keszlet; } set { if (keszlet != value) { keszlet = value; Jelez("Keszlet"); Jelez("Ertek"); } } }
[DisplayName("Érték (Ft)")]
public int Ertek { get { return Ar * Keszlet; } }
private void Jelez(string tulajdonsag)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(tulajdonsag));
}
}
private BindingList<Termek> lista = new BindingList<Termek>();
private BindingSource bs = new BindingSource();
private int kovetkezoAzonosito = 1;
private void Form1_Load(object sender, EventArgs e)
{
bs.DataSource = lista;
dgv.DataSource = bs;
dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgv.AllowUserToAddRows = false;
dgv.Columns["Ertek"].ReadOnly = true;
dgv.Columns["Nev"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
bs.PositionChanged += (s, ev) => PozFrissit();
lista.ListChanged += (s, ev) => OsszFrissit();
OsszFrissit();
PozFrissit();
}
private void PozFrissit()
{
lblPoz.Text = bs.Count == 0 ? "–" : (bs.Position + 1) + " / " + bs.Count;
btnTorol.Enabled = bs.Current != null;
}
private void OsszFrissit()
{
long ossz = 0;
foreach (Termek t in lista)
{
ossz += t.Ertek;
}
lblOssz.Text = lista.Count + " termék, készlet értéke: " + ossz.ToString("N0") + " Ft";
PozFrissit();
}
private void btnUj_Click(object sender, EventArgs e)
{
if (txtNev.Text.Trim().Length == 0) { return; }
Termek t = new Termek { Azonosito = kovetkezoAzonosito++, Nev = txtNev.Text.Trim(), Ar = (int)nudAr.Value, Keszlet = (int)nudKeszlet.Value };
bs.Add(t);
bs.Position = bs.IndexOf(t); // az új elem lesz az aktuális
txtNev.Clear();
txtNev.Focus();
}
private void btnTorol_Click(object sender, EventArgs e)
{
Termek t = bs.Current as Termek;
if (t != null && MessageBox.Show(this, "Törlöd: " + t.Nev + "?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes)
{
bs.RemoveCurrent();
}
}
private void btnEmel_Click(object sender, EventArgs e)
{
foreach (Termek t in lista)
{
t.Ar = (int)Math.Round(t.Ar * 1.1); // az INotifyPropertyChanged miatt a tábla frissül
}
}
private void btnElozo_Click(object sender, EventArgs e) { bs.MovePrevious(); }
private void btnKovetkezo_Click(object sender, EventArgs e) { bs.MoveNext(); }Három önálló gyakorlófeladat
Egyszerre csak egyet csinálj, és csak utána nyisd ki a megoldást.
1. Mezők átalakítása
Alakítsd át a 8.2 lecke A mezők automatikus tulajdonságra cserélődnek: a használó kód (Szam osztályát (mezők) kötésre alkalmas osztállyá. Mi változik a használó kódban?Egy lehetséges megoldás
sz.Cim = ...) változatlan marad.public class Szam
{
public string Cim { get; set; } = "";
public int Masodperc { get; set; } = 0;
}
2. Egy listaelem lecserélése
A felhasználó átír egy termék minden adatát egy űrlapon. Hogyan cseréled az objektumot a Az indexelő értékadása (BindingList-ben, hogy a tábla frissüljön?Egy lehetséges megoldás
lista[i] = új) ItemChanged eseményt vált ki, a tábla frissül.int i = bs.Position;
if (i >= 0)
{
lista[i] = uj; // csere: a tábla frissül
}
3. Üres lista kezelése
Üres listán a Törlés, ◀ és ▶ gombok tiltottak. Melyik eseményben frissíted? A Egy lehetséges megoldás
PositionChanged és a ListChanged eseményben: a bs.Count és a Position alapján.private void GombokFrissit()
{
btnTorol.Enabled = bs.Count > 0;
btnElozo.Enabled = bs.Position > 0;
btnKovetkezo.Enabled = bs.Position < bs.Count - 1;
}
Gyakorlótár: 6 további feladat
Alap, közepes és kihívás szinten gyakorolhatsz. Csak a próbálkozás után nyisd le a megoldást.
Alap szint
1. List vagy BindingList?
Melyiket használod, ha a tábla magától kövesse a hozzáadást és törlést?Megoldás megnyitása
BindingList<T>: értesítést küld a változásról.
2. Mező vagy tulajdonság?
Miért hiányzik egy oszlop a táblából? Valószínűleg a mező nem tulajdonság (Megoldás megnyitása
public string Nev; helyett public string Nev { get; set; } kell), vagy [Browsable(false)] jellemzője van.
Közepes szint
3. Kötés kódból, tervező nélkül
Írd le a kötés három lépését: gyűjtemény, (1) BindingSource, tábla.Megoldás megnyitása
BindingList<T> lista = new BindingList<T>(); (2) bs.DataSource = lista; (3) dgv.DataSource = bs;
4. ResetItem és ResetBindings
Mikor használod őket, és miben különböznek? A Megoldás megnyitása
lista.ResetItem(i) egyetlen elem frissítését kéri (ha az objektum nem jelzett). A bs.ResetBindings(false) az egész kötés frissítését kéri (drágább). Az INotifyPropertyChanged jobb megoldás, mindkettő csak szükségmegoldás.
Kihívás szint
5. Az összeg tesztelése felület nélkül
Írj statikus Az adat a gyűjteményben van, a számítás nem függ a felülettől: ablak és tábla nélkül is lefuttatható.Osszertek(IEnumerable<Termek>) metódust, és teszteld három termékre. Miért könnyű tesztelni?Megoldás megnyitása
public static long Osszertek(IEnumerable<Termek> termekek)
{
long ossz = 0;
foreach (Termek t in termekek)
{
ossz += (long)t.Ar * t.Keszlet;
}
return ossz;
}
// teszt: 2 * 100 + 3 * 50 = 350
System.Diagnostics.Debug.Assert(Osszertek(new List<Termek> { new Termek { Ar = 100, Keszlet = 2 }, new Termek { Ar = 50, Keszlet = 3 } }) == 350);
6. Hiba az érvénytelen árra
Az A Ar tulajdonság ne fogadjon negatív értéket. Hová teszed az ellenőrzést az osztályban?Megoldás megnyitása
set ágba: kivételt dobhat (ArgumentOutOfRangeException) vagy a határra igazíthat. A felhasználói bevitel ellenőrzése a felületen történik (8.8 lecke), de az osztály is védje magát.public int Ar
{
get { return ar; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("Ar", "Az ár nem lehet negatív.");
}
if (ar != value) { ar = value; Jelez("Ar"); Jelez("Ertek"); }
}
}
Gyors önellenőrzés
1. Miért nem frissül a tábla List<T> esetén?
A List<T> nem küld értesítést a változásról.
2. Mit tud a BindingList<T>?
Értesít a hozzáadásról, törlésről és módosításról (ListChanged), a kötött vezérlők követik.
3. Mire való az INotifyPropertyChanged?
Az objektum tulajdonságának változását jelzi, ezért a megjelenítés frissül.
4. Mi a BindingSource szerepe?
Közvetítő a gyűjtemény és a vezérlők között: aktuális elem (Current, Position), műveletek (RemoveCurrent, MoveNext).
5. Miért kell tulajdonság a kötéshez?
Az adatkötés a nyilvános tulajdonságokat látja, a mezőket nem.
6. Mit csinál a [Browsable(false)]?
Elrejti a tulajdonságot az automatikusan képzett oszlopok közül.
7. Miért kell jelezni az Ertek tulajdonságot is?
Mert számított: az Ar és Keszlet változásakor ő is változik, és a megjelenítés ezt csak jelzésből tudja meg.
Mit tudsz már?
- Elválasztod az adatot a megjelenítéstől, és
BindingList<T>gyűjteményt kötsz. - Megvalósítod az
INotifyPropertyChangedfelületet, számított tulajdonsággal is. - Használod a
BindingSourceaktuális elemét és műveleteit. - Tudod, hogy a kötéshez tulajdonság kell, és formázod az oszlopokat (
Browsable,DisplayName).
Következő lecke: DataSource, DisplayMember és ValueMember
Listák és legördülő listák kötése adatobjektumokhoz: mit mutat a lista, és mit ad vissza a kiválasztás.



