Grafikus C# – Windows Forms tanfolyam › 8.4 lecke

Windows Forms · 8.4 lecke · adatkötés alapjai

List, BindingList és BindingSource

Eddig a vezérlők maguk tárolták az adatot (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ő.

  • kb. 110 perc
  • közepes–haladó szint
  • adatkötés
Anime stílusú fiú és lány egy C# programozási feladatot beszél meg

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 a BindingList<T>;
  • megvalósítod az INotifyPropertyChanged felületet, hogy az objektum módosítása látsszon;
  • a BindingSource segí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ő, List frissí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.

8.3 DataGridView →

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:

Form1.cs (using System.ComponentModel; using System.Collections.Generic;)C#
// 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
};
Miért nem frissül a List?A 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 újrakötéseA 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.
BindingListA System.ComponentModel névtérben van (using!). A ListChanged esemény e.ListChangedType mezője megmondja, mi történt (ItemAdded, ItemDeleted, ItemChanged, Reset).
Adat és felület szétválasztásaAz adatot a gyűjtemény tartja: a számítások (összeg, darabszám) a gyűjteményen futnak, nem a tábla celláin. Így a program logikája a felület nélkül is érthető és tesztelhető.

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:

Termek.cs (using System.ComponentModel;)C#
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));
}
}
A set ág jelezMinden tulajdonság 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.
Számított tulajdonságAz Ertek az Ar és Keszlet szorzata. Ha azok változnak, az Ertek is: ezért jelezzük őt is a set ágakban.
A Jelez segédmetódusEgy privát metódus gyűjti a jelzést, így a tulajdonságok rövidek maradnak, és az ?.Invoke védi a feliratkozó nélküli esetet (5.1 lecke).
Csak olvasható oszlopA csak 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):

Form1.csC#
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éges
Current és PositionA Current 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.
Több vezérlő egy forráshozHa a táblához és több szövegmezőhöz is ugyanazt a BindingSource-t kötöd, mindegyik az aktuális elemet mutatja (8.6 lecke).
RemoveCurrentAz aktuális elemet törli a gyűjteményből, a tábla és a többi kötött vezérlő magától frissül. Ellenőrizd, hogy van-e aktuális elem (bs.Count > 0).
Mikor kell BindingSource?Egyszerű táblához elég a 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:

Termek.csC#
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; }
}
Automatikus tulajdonságA { get; set; } alak a legrövidebb tulajdonság. Ha nem kell jelzés (INotifyPropertyChanged) és ellenőrzés, ez elég.
Browsable(false)A [Browsable(false)] jellemző (attribútum) elrejti a tulajdonságot az automatikus oszlopok közül. Hasznos azonosítókhoz és belső adatokhoz.
DisplayNameA [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.
Mezők átalakításaEgy meglévő osztály mezőit tulajdonságra cseréled: 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.

📷 Képernyőkép helye

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 List<T> végére tettél egy elemet, de a tábla változatlan.

Ok és javítás

A 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 lista[0].Ar = 999; után a cella a régi értéket mutatja.

Ok és javítás

Az osztály nem jelez. Valósítsd meg az 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 Ar módosítása után az Ertek (számított) régi marad.

Ok és javítás

A számított tulajdonság változását is jelezni kell: az Ar és Keszlet set ágában Jelez("Ertek") is kell.

5. InvalidOperationException a RemoveCurrent-nél

A törlő gomb üres listán összeomlik.

Ok és javítás

Nincs aktuális elem. Ellenőrizd a 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.

Ok és javítás

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

  1. Termek osztály (INotifyPropertyChanged): Nev, Ar, Keszlet és a számított Ertek (Ar * Keszlet); [DisplayName] fejlécek; egy Azonosito [Browsable(false)].
  2. A BindingList<Termek> a BindingSource-on át a dgv-hez kötve; a tábla FullRowSelect, a Érték oszlop csak olvasható.
  3. Az Új termék gomb (txtNev, nudAr, nudKeszlet) hozzáad a BindingSource-hoz, és az új sor lesz kijelölve.
  4. A Törlés gomb a RemoveCurrent metódust használja megerősítéssel (üres listán tiltva).
  5. Az Árak +10% gomb a kódból módosítja az összes termék árát (az INotifyPropertyChanged miatt a tábla azonnal frissül, az Érték oszlop is).
  6. A ◀ és ▶ gombok a MovePrevious és MoveNext metódusokkal lépkednek; a lblPoz mutatja: 3 / 12 (PositionChanged).
  7. Az lblOssz a készlet összértékét mutatja a ListChanged eseményben (a gyűjteményből számolva, nem a celláiból).
  8. 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).

Form1.cs (using System.ComponentModel;)C#
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 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

A mezők automatikus tulajdonságra cserélődnek: a használó kód (sz.Cim = ...) változatlan marad.

Szam.csC#
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 BindingList-ben, hogy a tábla frissüljön?

Egy lehetséges megoldás

Az indexelő értékadása (lista[i] = új) ItemChanged eseményt vált ki, a tábla frissül.

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

Egy lehetséges megoldás

A PositionChanged és a ListChanged eseményben: a bs.Count és a Position alapján.

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

Megoldás megnyitása

Valószínűleg a mező nem tulajdonság (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, BindingSource, tábla.

Megoldás megnyitása

(1) 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?

Megoldás megnyitása

A 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 Osszertek(IEnumerable<Termek>) metódust, és teszteld három termékre. Miért könnyű tesztelni?

Megoldás megnyitása

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

Szamitas.csC#
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 Ar tulajdonság ne fogadjon negatív értéket. Hová teszed az ellenőrzést az osztályban?

Megoldás megnyitása

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

Termek.csC#
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 INotifyPropertyChanged felületet, számított tulajdonsággal is.
  • Használod a BindingSource aktuá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.