Grafikus C# – Windows Forms tanfolyam › 8.5 lecke Windows Forms · 8.5 lecke · listák kötése Az adatbázisokban és a nyilvántartásokban az adatok gyakran azonosítókkal kapcsolódnak: egy feladathoz egy kategória-azonosító (DataSource, DisplayMember és ValueMember
3) tartozik, a felhasználónak viszont a kategória neve (Iskola) kell. A ListBox és a ComboBox kötése pontosan ezt oldja meg: a DisplayMember megmondja, melyik tulajdonságot mutassa, a ValueMember pedig, melyik tulajdonságot adja vissza (az azonosítót). Ebben a leckében a szöveg és az érték szétválasztását, a SelectedValue és SelectedItem különbségét, a Format eseményt és a táblázat ComboBox oszlopát tanulod meg.
A lecke végére
- listát és legördülő listát kötsz egy gyűjteményhez (
DataSource); - beállítod, mit mutasson (
DisplayMember) és mit adjon vissza (ValueMember); - megkülönbözteted a
SelectedItem(a teljes objektum) és aSelectedValue(az azonosító) használatát; - a
Formateseménnyel összetett szöveget mutatsz; - a
DataGridViewComboBox oszlopát kötöd (DataPropertyName), és az azonosítót névként jeleníted meg; - kerülöd a tipikus hibákat (
Itemsmódosítása kötött listán, kötéskor lefutó események).
Mielőtt elkezded
Ehhez a leckéhez a 8.1 és 8.2 lecke (listák, kijelölés), a 8.3 lecke (DataGridView oszlopok) és a 8.4 lecke (BindingList, BindingSource, tulajdonságok) szükséges.
Lista kötése: DataSource, DisplayMember, ValueMember
public class Kategoria
{
public int Id { get; set; }
public string Nev { get; set; } = "";
}
private BindingList<Kategoria> kategoriak = new BindingList<Kategoria>();
private void Form1_Load(object sender, EventArgs e)
{
kategoriak.Add(new Kategoria { Id = 1, Nev = "Iskola" });
kategoriak.Add(new Kategoria { Id = 2, Nev = "Otthon" });
kategoriak.Add(new Kategoria { Id = 3, Nev = "Hobbi" });
cmbKategoria.DropDownStyle = ComboBoxStyle.DropDownList;
cmbKategoria.DisplayMember = "Nev"; // ezt látja a felhasználó
cmbKategoria.ValueMember = "Id"; // ezt adja vissza a SelectedValue
cmbKategoria.DataSource = kategoriak; // a forrás: a tulajdonságnevek a DataSource beállítása után is érvényesek
}
private void btnMutat_Click(object sender, EventArgs e)
{
int id = (int)cmbKategoria.SelectedValue; // az azonosító: 1, 2 vagy 3
Kategoria k = (Kategoria)cmbKategoria.SelectedItem; // a teljes objektum
lblInfo.Text = "Id: " + id + ", név: " + k.Nev + ", felirat: " + cmbKategoria.Text;
}ToString() eredménye jelenik meg (8.2 lecke).SelectedValue. Egyedi azonosító legyen (Id), ne a szöveg.Items.Add kivételt dob (InvalidOperationException: a lista adatforrásból töltődik). Az adatot a forrásban módosítod (kategoriak.Add(...)), a lista követi (BindingList esetén).DisplayMember és ValueMember beállítását általában a DataSource előtt érdemes megtenni: így a kötés már ezekkel épül fel, és kevesebb felesleges esemény fut.Jósolj!
A ComboBox kötött (DataSource beállítva), és a program a cmbKategoria.Items.Add("Új"); sort hajtja végre. Mi történik?
Jóslat ellenőrzése
InvalidOperationException: adatforrással kötött lista elemeit közvetlenül nem lehet módosítani. A forrásgyűjteményhez adj hozzá (kategoriak.Add(új)).
SelectedValue és SelectedItem
| Tulajdonság | Mit ad? | Mikor használd? |
|---|---|---|
SelectedItem | A teljes objektum (Kategoria). | Ha több tulajdonságra van szükség. |
SelectedValue | A ValueMember tulajdonság értéke (Id), object típusú. | Ha az azonosítót kell tárolni (például Feladat.KategoriaId). |
Text | A megjelenített felirat. | Csak kiírásra. |
SelectedIndex | A sorszám (-1, ha nincs). | Lépegetéshez, az első elem beállításához. |
// beállítás azonosító szerint: a lista a megfelelő elemre áll
cmbKategoria.SelectedValue = 2; // az "Otthon"
// kiolvasás védetten: a SelectedValue null lehet (nincs kijelölés), és kötés közben más típusú is
if (cmbKategoria.SelectedValue is int)
{
int id = (int)cmbKategoria.SelectedValue;
// ... használat ...
}
// a kötés közbeni események elkerülése: jelző (5.5 lecke)
private bool kotes = false;
private void KategoriakKot()
{
kotes = true;
cmbKategoria.DataSource = kategoriak;
kotes = false;
}
private void cmbKategoria_SelectedValueChanged(object sender, EventArgs e)
{
if (kotes || !(cmbKategoria.SelectedValue is int))
{
return;
}
// a felhasználó választására reagálunk
}SelectedValue object: a (int) átalakítás kell, és null vagy más típus esetén kivételt dob. Az is int ellenőrzés védi.SelectedValue = 2 az azonos típusú azonosítót keresi: a "2" szöveg nem találna int azonosítót. Ha nincs ilyen elem, a kijelölés megszűnik.DataSource beállításakor a SelectedIndexChanged és SelectedValueChanged események lefuthatnak, amikor a SelectedValue még nem érvényes. A jelző vagy az is int ellenőrzés kell.SelectedItem típusos (as Kategoria) és kényelmesebb, ha több mezőt használsz. Az azonosító tárolásához a SelectedValue a megszokott.Jósolj!
A kategóriák azonosítója int. A kód: cmb.SelectedValue = "2"; (szöveg). Mit látsz a ComboBoxban?
Jóslat ellenőrzése
A lista nem választ ki semmit (az üres állapot marad): a "2" szöveg nem egyezik az int típusú 2 azonosítóval, a kereső nem talál elemet. Az érték típusa egyezzen az azonosítóéval.
Összetett szöveg: a Format esemény
A DisplayMember egyetlen tulajdonságot mutat. Ha összetett szöveg kell (Iskola (3 feladat)), a Format eseményt használod, amely minden elem megjelenítése előtt lefut, és átírhatja a szöveget (FormattingEnabled = true szükséges):
lstKategoria.FormattingEnabled = true;
lstKategoria.Format += lstKategoria_Format;
private void lstKategoria_Format(object sender, ListControlConvertEventArgs e)
{
Kategoria k = e.ListItem as Kategoria; // az e.ListItem az eredeti objektum
if (k != null)
{
int db = 0;
foreach (Feladat f in feladatok)
{
if (f.KategoriaId == k.Id) { db++; }
}
e.Value = k.Nev + " (" + db + " feladat)"; // a megjelenített szöveg
}
}
// a Format kimenete akkor frissül, ha a kötés újraolvas:
bsKategoria.ResetBindings(false);e.ListItem a listaelem (az objektum), az e.Value a megjelenítendő szöveg: ezt írod át. A DisplayMember értéke kezdetben az e.Value.Format minden megjelenítéskor lefut, ezért gyors legyen. A darabszám számolása itt nagy listán lassú: érdemes előre kiszámolni (szótárban).Format csak újrarajzoláskor fut. Ha az adat változik (új feladat), kérd az újrarajzolást (ResetBindings, lst.Refresh()).Felirat tulajdonsága (Nev + " (" + Db + ")"), és azt kötöd DisplayMember-ként. A Format akkor jó, ha a szöveg a felülettől (nyelvtől, nézettől) függ.Jósolj!
A Format kezelő kiszámolja a feladatok számát, de egy új feladat hozzáadása után a lista felirata nem változik. Miért?
Jóslat ellenőrzése
A lista nem tudja, hogy a megjelenített szöveg függ a feladatok számától: nem rajzolja újra magát. Újrarajzolásra kell kérni (bsKategoria.ResetBindings(false)), a feladat hozzáadása után.
ComboBox oszlop a táblázatban
A leggyakoribb alkalmazás: a táblázat sorai azonosítót tárolnak (Feladat.KategoriaId), de a felhasználó a nevet látja, és legördülő listából választ. A DataGridViewComboBoxColumn pont ezt tudja: a DataPropertyName kapcsolja a sor adatának tulajdonságához, a DataSource, DisplayMember, ValueMember pedig a kategóriák listájához:
public class Feladat
{
public string Cim { get; set; } = "";
public int KategoriaId { get; set; } = 1;
public bool Kesz { get; set; }
}
private BindingList<Feladat> feladatok = new BindingList<Feladat>();
private void TablaBeallit()
{
dgv.AutoGenerateColumns = false;
dgv.Columns.Add(new DataGridViewTextBoxColumn { Name = "colCim", HeaderText = "Feladat", DataPropertyName = "Cim", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill });
DataGridViewComboBoxColumn kat = new DataGridViewComboBoxColumn();
kat.Name = "colKat";
kat.HeaderText = "Kategória";
kat.DataPropertyName = "KategoriaId"; // a sor adatának tulajdonsága (az azonosító)
kat.DataSource = kategoriak; // a választék
kat.DisplayMember = "Nev"; // amit a felhasználó lát
kat.ValueMember = "Id"; // amit a cella tárol
kat.Width = 140;
dgv.Columns.Add(kat);
dgv.Columns.Add(new DataGridViewCheckBoxColumn { Name = "colKesz", HeaderText = "Kész", DataPropertyName = "Kesz", Width = 60 });
dgv.DataSource = feladatok; // a sorok adata
}AutoGenerateColumns = false mellett minden oszlopnak meg kell adni, különben üres marad.3), a megjelenítés a névre fordítja (Hobbi). A felhasználó választása visszaírja az azonosítót a Feladat.KategoriaId tulajdonságba.KategoriaId értéke nincs a kategóriák között (törölt kategória), a DataGridView DataError eseményt vált ki (8.3 lecke). Ezért az érték törlése előtt ellenőrizni kell a hivatkozásokat.BindingList a ComboBox-ban, a táblában és a ListBox-ban: egy módosítás mindenhol látszik.Jósolj!
Egy feladat KategoriaId értéke 9, de a kategóriák azonosítói 1, 2, 3. Mit tapasztalsz a tábla megjelenítésekor?
Jóslat ellenőrzése
A cella hibát jelez (alapból felugró „a legördülő cella értéke érvénytelen” üzenet, DataError esemény). A javítás: DataError kezelő, és az érvényes hivatkozások biztosítása (alapértelmezett kategória, törlés előtti ellenőrzés).
Mit tegyél ide: a feladattábla Kategória legördülő oszloppal (kinyitva), mellette a kategóriák listája a darabszámokkal.Javasolt fájlnév: winforms-8-5-combooszlop.webp · Alt szöveg: Táblázat ComboBox oszloppal
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 lista a típus nevét mutatja
A ComboBox elemei Kategoria-ként jelennek meg. A Ok és javítás
DisplayMember nincs beállítva (vagy elírt), és a ToString() az osztály nevét adja. Állítsd DisplayMember = "Nev"-re, vagy írd felül a ToString() metódust.
2. InvalidOperationException az Items.Add-nál
cmb.DataSource = kategoriak;
cmb.Items.Add("Új"); // kötött listához nem lehetHiba és javítás
Az elemet a forrásba add: kategoriak.Add(new Kategoria { ... }).
3. InvalidCastException a SelectedValue-nál
A A (int)cmb.SelectedValue a kötés közben hibázik.Ok és javítás
DataSource beállításakor az események lefutnak, amikor a SelectedValue még nem int. Védd is int ellenőrzéssel vagy jelzővel.
4. A SelectedValue beállítása nem választ ki semmit
cmb.SelectedValue = "2"; // az azonosító intHiba és javítás
Az azonosító típusa nem egyezik: használd 2-t (int), vagy alakítsd át az értéket.
5. A tábla ComboBox oszlopa hibát jelez
A cellák az „érvénytelen érték” üzenetet adják. A sor azonosítója nincs a választékban, vagy a Ok és javítás
DataPropertyName/ValueMember típusa eltér. Ellenőrizd az értékeket, és kezeld a DataError eseményt.
6. A lista felirata nem frissül új adatnál
A A Format kezelő számolja a darabszámot, de az nem változik.Ok és javítás
Format csak újrarajzoláskor fut. Kérd a frissítést: bs.ResetBindings(false) a feladat hozzáadása után.
Mini projekt: feladatkezelő kategóriákkal
Készíts programot, amely feladatokat kategóriákhoz rendel:
- Az osztályok:
Kategoria(Id,Nev),Feladat(Cim,KategoriaId,Kesz), mindkettő tulajdonságokkal; kétBindingList. - A
cmbUjKat(DropDownList,DisplayMemberNev,ValueMemberId) az új feladat kategóriája; atxtCimés az Új feladat gomb (vagy Enter) új feladatot ad a kiválasztott kategória azonosítójával. - A
dgvoszlopai: Feladat (szöveg), Kategória (ComboBox oszlopDataPropertyName = "KategoriaId"), Kész (jelölőnégyzet). A kategória a táblában is átállítható. - A
lstKategoria(ListBox) a kategóriák nevét mutatja a feladatok számával (Iskola (3 feladat),Formatesemény), és a kijelölt kategória feladatainak számát frissen tartja (ResetBindings). - Az Új kategória mező és gomb új kategóriát ad a forrásgyűjteményhez (ismétlés nélkül,
Id= legnagyobb + 1), amely a három helyen azonnal megjelenik. - A kategória törlése csak akkor lehet, ha nincs hozzá feladat (különben üzenet, 7.4 lecke).
- Teszteld: új feladat, kategóriaváltás a táblában, új kategória (a ComboBox oszlopban is), törlés használt és használatlan kategóriára, a darabszámok frissülése, a
DataErrorkezelése.
Megoldás megnyitása (próbáld ki előbb önállóan!)
A kategóriák BindingSource-on át kötődnek a ListBox-hoz (a ResetBindings miatt), a ComboBox oszlop és a ComboBox ugyanazt a BindingList-et használja.
public class Kategoria { public int Id { get; set; } public string Nev { get; set; } = ""; }
public class Feladat { public string Cim { get; set; } = ""; public int KategoriaId { get; set; } = 1; public bool Kesz { get; set; } }
private BindingList<Kategoria> kategoriak = new BindingList<Kategoria>();
private BindingList<Feladat> feladatok = new BindingList<Feladat>();
private BindingSource bsKat = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
kategoriak.Add(new Kategoria { Id = 1, Nev = "Iskola" });
kategoriak.Add(new Kategoria { Id = 2, Nev = "Otthon" });
kategoriak.Add(new Kategoria { Id = 3, Nev = "Hobbi" });
cmbUjKat.DropDownStyle = ComboBoxStyle.DropDownList;
cmbUjKat.DisplayMember = "Nev";
cmbUjKat.ValueMember = "Id";
cmbUjKat.DataSource = kategoriak;
bsKat.DataSource = kategoriak;
lstKategoria.DisplayMember = "Nev";
lstKategoria.ValueMember = "Id";
lstKategoria.FormattingEnabled = true;
lstKategoria.Format += lstKategoria_Format;
lstKategoria.DataSource = bsKat;
TablaBeallit();
dgv.DataError += dgv_DataError;
}
private void TablaBeallit()
{
dgv.AutoGenerateColumns = false;
dgv.Columns.Add(new DataGridViewTextBoxColumn { Name = "colCim", HeaderText = "Feladat", DataPropertyName = "Cim", AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill });
DataGridViewComboBoxColumn kat = new DataGridViewComboBoxColumn { Name = "colKat", HeaderText = "Kategória", DataPropertyName = "KategoriaId", DataSource = kategoriak, DisplayMember = "Nev", ValueMember = "Id", Width = 140 };
dgv.Columns.Add(kat);
dgv.Columns.Add(new DataGridViewCheckBoxColumn { Name = "colKesz", HeaderText = "Kész", DataPropertyName = "Kesz", Width = 60 });
dgv.AllowUserToAddRows = false;
dgv.DataSource = feladatok;
dgv.CellValueChanged += (s, ev) => bsKat.ResetBindings(false); // a kategóriaváltás a számokat is érinti
}
private int Darab(int kategoriaId)
{
int db = 0;
foreach (Feladat f in feladatok)
{
if (f.KategoriaId == kategoriaId) { db++; }
}
return db;
}
private void lstKategoria_Format(object sender, ListControlConvertEventArgs e)
{
Kategoria k = e.ListItem as Kategoria;
if (k != null)
{
e.Value = k.Nev + " (" + Darab(k.Id) + " feladat)";
}
}
private void UjFeladat()
{
if (txtCim.Text.Trim().Length == 0 || !(cmbUjKat.SelectedValue is int)) { return; }
feladatok.Add(new Feladat { Cim = txtCim.Text.Trim(), KategoriaId = (int)cmbUjKat.SelectedValue });
txtCim.Clear();
txtCim.Focus();
bsKat.ResetBindings(false);
}
private void btnUjFeladat_Click(object sender, EventArgs e) { UjFeladat(); }
private void txtCim_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; UjFeladat(); }
}
private void btnUjKat_Click(object sender, EventArgs e)
{
string nev = txtUjKat.Text.Trim();
if (nev.Length == 0) { return; }
foreach (Kategoria k in kategoriak)
{
if (k.Nev.Equals(nev, StringComparison.CurrentCultureIgnoreCase))
{
lblInfo.Text = "Ilyen kategória már van.";
return;
}
}
int max = 0;
foreach (Kategoria k in kategoriak) { max = Math.Max(max, k.Id); }
kategoriak.Add(new Kategoria { Id = max + 1, Nev = nev });
txtUjKat.Clear();
}
private void btnTorolKat_Click(object sender, EventArgs e)
{
Kategoria k = bsKat.Current as Kategoria;
if (k == null) { return; }
if (Darab(k.Id) > 0)
{
MessageBox.Show(this, "A kategóriához feladatok tartoznak, ezért nem törölhető.", "Törlés", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
bsKat.RemoveCurrent(); // a ComboBoxok és az oszlop is követi
}
private void dgv_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
e.ThrowException = false;
lblInfo.Text = "Érvénytelen kategória a(z) " + (e.RowIndex + 1) + ". sorban.";
}Három önálló gyakorlófeladat
Egyszerre csak egyet csinálj, és csak utána nyisd ki a megoldást.
1. Név és azonosító kiírása
Egy kötött A ListBox kijelölt elemének nevét és azonosítóját írd ki egy címkébe. Melyik tulajdonságokat használod?Egy lehetséges megoldás
SelectedItem típusos olvasásával (as Kategoria) mindkettő elérhető.Kategoria k = lst.SelectedItem as Kategoria;
lblInfo.Text = k == null ? "Nincs kijelölés." : k.Nev + " (azonosító: " + k.Id + ")";
2. A felirat tulajdonságként
A Egyszerűbb (nincs eseménykezelő), és a rendezéshez, kereséshez is használható. A Kategoria osztály kapjon Felirat tulajdonságot (Id. Nev), amelyet a lista DisplayMember-ként mutat. Mi az előnye a Format eseménnyel szemben?Egy lehetséges megoldás
Format akkor jobb, ha a szöveg a felülettől függ.public string Felirat { get { return Id + ". " + Nev; } }
// lst.DisplayMember = "Felirat";
3. Kezdőérték a kategóriához
A program indításkor az utolsó használt kategóriát jelöli ki a ComboBoxban (a A Settings-ből, azonosító szerint). Hogyan állítod be?Egy lehetséges megoldás
SelectedValue = azonosító beállítással; ha nincs ilyen elem (törölt kategória), az első elemet jelölöd ki.cmbUjKat.SelectedValue = Properties.Settings.Default.UtolsoKat;
if (cmbUjKat.SelectedIndex < 0 && cmbUjKat.Items.Count > 0)
{
cmbUjKat.SelectedIndex = 0;
}
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. Mit tesz a DisplayMember?
Mit mutat a lista, ha a Az objektum DisplayMember üres?Megoldás megnyitása
ToString() eredményét (alapból az osztály nevét).
2. SelectedItem vagy SelectedValue?
Melyiket használod, ha az azonosítót kell eltárolni? A Megoldás megnyitása
SelectedValue-t (a ValueMember értékét), is int védelemmel.
Közepes szint
3. Kötés ismétlődés nélkül
Hogyan akadályozod meg, hogy kötéskor a Jelző (SelectedIndexChanged kezelő összeomoljon?Megoldás megnyitása
kotes = true a kötés alatt), vagy ellenőrzés a kezelő elején: if (!(cmb.SelectedValue is int)) return;.
4. Szótár a darabszámokhoz
A Minden feladatváltozás után újraszámolod (egy menet), a Format eseményben gyorsítsd a darabszámlálást: előre számold ki egy Dictionary<int,int> szótárba. Mikor frissíted?Megoldás megnyitása
Format csak kiolvassa.private Dictionary<int, int> darabok = new Dictionary<int, int>();
private void Szamol()
{
darabok.Clear();
foreach (Feladat f in feladatok)
{
int db;
darabok.TryGetValue(f.KategoriaId, out db);
darabok[f.KategoriaId] = db + 1;
}
bsKat.ResetBindings(false);
}
// a Format kezelőben: int db; darabok.TryGetValue(k.Id, out db);
Kihívás szint
5. Két összefüggő ComboBox
Az Ország kiválasztása után a Város listája az adott ország városait mutatja. Hogyan kötöd? Az első lista Megoldás megnyitása
SelectedValueChanged eseményében a második DataSource-át a szűrt gyűjteményre cseréled (új List a megfelelő városokkal).private void cmbOrszag_SelectedValueChanged(object sender, EventArgs e)
{
if (!(cmbOrszag.SelectedValue is int)) { return; }
int orszagId = (int)cmbOrszag.SelectedValue;
List<Varos> szurt = varosok.FindAll(v => v.OrszagId == orszagId);
cmbVaros.DisplayMember = "Nev";
cmbVaros.ValueMember = "Id";
cmbVaros.DataSource = szurt;
}
6. Törlés védelme
Hogyan akadályozod meg, hogy egy használt kategória törlése Törlés előtt ellenőrzöd, hogy tartozik-e hozzá feladat (ahogy a mini projektben): ha igen, üzenet és nincs törlés. A másik lehetőség az áthelyezés egy alapértelmezett kategóriába.DataError-t okozzon?Megoldás megnyitása
Gyors önellenőrzés
1. Mi a DisplayMember?
A tulajdonság neve, amelynek értékét a lista megjeleníti.
2. Mi a ValueMember?
A tulajdonság neve, amelynek értéke a SelectedValue (általában az azonosító).
3. Mi a különbség a SelectedItem és a SelectedValue között?
A SelectedItem a teljes objektum, a SelectedValue a ValueMember értéke.
4. Miért nem működik az Items.Add kötött listán?
Az elemek az adatforrásból jönnek: a forrást módosítsd.
5. Mire való a Format esemény?
Összetett megjelenített szöveg előállítása az objektumból, a FormattingEnabled = true mellett.
6. Mit köt a DataPropertyName?
A táblázat oszlopát a sorobjektum tulajdonságához.
7. Mit ad a SelectedValue kötés közben?
Lehet null vagy nem a várt típus: ellenőrizd (is int) vagy jelzővel védd.
Mit tudsz már?
- Listát és legördülő listát kötsz: mit mutasson, és mit adjon vissza.
- Megkülönbözteted és védetten használod a
SelectedItemésSelectedValuetulajdonságot. - Összetett szöveget mutatsz a
Formateseménnyel. - A táblázatban az azonosítót névként jeleníted meg ComboBox oszloppal.
Következő lecke: Adatkötés alapjai
Szövegmezők és más vezérlők kötése az aktuális elemhez: két irányú kötés, formázás, frissítés.



