Grafikus C# – Windows Forms tanfolyam › 3.9 lecke

Windows Forms · 3.9 lecke · rendezett szerkezet

Űrlap és adatbázis: CRUD műveletek rendezett szerkezetben

A 3.6 leckében egyetlen ablakba tettünk mindent: a táblázatot, a mezőket és az SQL hívásokat. Ez kis programnál működik, de ahogy a program nő, egyre nehezebb átlátni. Ebben a leckében ugyanazt a nyilvántartót építjük fel rendezetten: saját adat-osztály (Dolgozo), külön adatkezelő osztály, és egy önálló szerkesztő párbeszédablak, amely az Új és a Módosítás műveletet is kezeli.

  • kb. 120 perc
  • haladó szint
  • rétegek és párbeszédablak
Diák C# programozást gyakorol egy tanulósarokban

A lecke végére

  • érted, miért érdemes a programot rétegekre bontani (ablak, adat-osztály, adatkezelő);
  • saját osztályt (Dolgozo) írsz tulajdonságokkal és ellenőrző metódussal;
  • az adatkezelő osztály Betolt, Mentes és Torles metódusát megírod, az ExecuteReader segítségével;
  • külön szerkesztő párbeszédablakot készítesz (ShowDialog, DialogResult, AcceptButton, CancelButton);
  • megérted, miért nem szabad a párbeszédablaknak az eredeti objektumot módosítania;
  • kezeled a konkurens módosítás egyszerű esetét (közben törölték a sort).

Mielőtt elkezded

Ehhez a leckéhez kell a 3.6 lecke (INSERT, UPDATE, DELETE, a nyilvántartó), a 2.9 lecke (egy második ablak megnyitása) és a 3.7 lecke (hibanapló, DB.Naploz).

3.6 INSERT, UPDATE, DELETE →

Miért érdemes szétválasztani?

A 3.6 leckében az ablak kódja egyszerre kezelte a felületet (mezők, gombok), az üzleti szabályokat (kötelező név) és az adatbázist (SQL). Ha később például a név szabályát módosítani kell, vagy másik adatbázisra váltasz, a változtatásokat az ablak kódjában szétszórva kell megkeresni. Jobb, ha minden rész egyetlen feladatot végez:

Ablakok (Form1, DolgozoUrlap): megjelenítés, események→Dolgozo osztály: adat és ellenőrzés→DolgozoTar: SQL és adatbázis
RétegFeladataMit nem tehet
AblakMezők kitöltése, gombok, üzenetek.Nem tartalmaz SQL-t.
Dolgozo osztályEgy dolgozó adatai és az adatok ellenőrzése.Nem ismeri az ablakot és az adatbázist.
DolgozoTarAz SQL: betöltés, mentés, törlés.Nem mutat üzenetet a felhasználónak, nem ismer vezérlőt.

Jósolj!

Később az adatbázisról átváltanál egy másik adatbázis-kezelőre. Melyik réteg kódját kellene módosítanod, ha a program a leírt módon van szétosztva? Mennyi marad az ablakok kódjából?

Jóslat ellenőrzése

Csak a DolgozoTar (és a kapcsolatot létrehozó DB) osztályt. Az ablakok és a Dolgozo osztály kódja változatlan maradhat, mert azok nem tudnak az SQL-ről. Ez a szétválasztás legnagyobb haszna: a változtatás egy helyre koncentrálódik.

A Dolgozo osztály

Az osztály saját adattípus: összefogja egy dolgozó adatait. Az osztály tulajdonságokból (property) áll, amelyeket { get; set; } jelöléssel hozunk létre: a get kiolvassa, a set beállítja az értéket. Az osztály metódust is tartalmazhat: itt az adatok ellenőrzését (Hiba). A projekten jobb gomb: Add → Class, a fájl neve Dolgozo.cs:

Dolgozo.csC#
public class Dolgozo
{
public int Id { get; set; } // 0: még nincs az adatbázisban
public string Vnev { get; set; } = "";
public string Knev { get; set; } = "";
public int Fizetes { get; set; }
public string Cim { get; set; } = "";

public string TeljesNev
{
get { return Vnev + " " + Knev; }
}

// üres szöveget ad, ha az adatok rendben vannak, különben a hiba leírását
public string Hiba()
{
if (Vnev.Length == 0 || Knev.Length == 0)
{
return "A vezeték- és a keresztnév kötelező.";
}

if (Vnev.Length > 40 || Knev.Length > 40 || Cim.Length > 40)
{
return "A nevek és a város legfeljebb 40 karakteresek lehetnek.";
}

if (Fizetes < 0)
{
return "A fizetés nem lehet negatív.";
}

return "";
}
}
public class DolgozoÚj adattípus. A public azt jelenti, hogy a program bármely része használhatja.
public string Vnev { get; set; } = "";Tulajdonság kezdőértékkel. Az üres szöveg biztosítja, hogy a Vnev sosem null, és a Length hibát nem okoz.
TeljesNevCsak olvasható, számított tulajdonság (nincs set): a két nevet adja össze.
Hiba()Az üzleti szabályok egy helyen vannak. Az ablak és az adatkezelő is ezt hívja, ezért nem kell a szabályokat többször leírni.

Új dolgozót így hozhatsz létre és tölthetsz ki:

MintaC#
Dolgozo d = new Dolgozo();
d.Vnev = "Fekete";
d.Knev = "Dóra";
d.Fizetes = 390000;
d.Cim = "Győr";

string hiba = d.Hiba(); // "", mert minden rendben

// rövidebb írásmód (objektum-inicializáló):
Dolgozo e = new Dolgozo { Vnev = "Orbán", Knev = "Réka", Fizetes = 400000 };

Jósolj!

Két változó mutat ugyanarra a dolgozóra: Dolgozo a = new Dolgozo(); Dolgozo b = a; b.Vnev = "Kiss"; Mi a a.Vnev értéke?

Jóslat ellenőrzése

Kiss. Az osztály referencia típus: a b = a nem másolja az objektumot, csak egy második hivatkozást hoz létre ugyanarra az objektumra. Ez fontos lesz a párbeszédablaknál: ha az ablak az átadott objektumot módosítja, a hívó objektuma is megváltozik, akár Mégse esetén is. A szám és a szöveg típusok (int, string) ezzel szemben értéket másolnak.

Az adatkezelő osztály: DolgozoTar

A DolgozoTar az egyetlen hely, ahol a dolgozókhoz tartozó SQL él. Három fő metódusa van. A Betolt egy sort olvas ki az ExecuteReader segítségével: ez egy olvasó objektumot ad, amely a lekérdezés eredményét soronként járja be (a Read() a következő sorra lép, és hamis, ha nincs több sor):

DolgozoTar.cs (using System; using System.Data.SQLite;)C#
public static class DolgozoTar
{
// egy dolgozó betöltése azonosító alapján; null, ha nincs ilyen
public static Dolgozo Betolt(int id)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("SELECT id, vnev, knev, fizetes, cim FROM adatok WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);

using (SQLiteDataReader olvaso = parancs.ExecuteReader())
{
if (!olvaso.Read())
{
return null;
}

Dolgozo d = new Dolgozo();
d.Id = Convert.ToInt32(olvaso["id"]);
d.Vnev = Convert.ToString(olvaso["vnev"]);
d.Knev = Convert.ToString(olvaso["knev"]);
d.Fizetes = olvaso["fizetes"] == DBNull.Value ? 0 : Convert.ToInt32(olvaso["fizetes"]);
d.Cim = Convert.ToString(olvaso["cim"]);

return d;
}
}
}
}

// új sor (Id == 0) vagy módosítás (Id > 0); visszaadja a sor azonosítóját
public static int Mentes(Dolgozo d)
{
string hiba = d.Hiba();

if (hiba.Length > 0)
{
throw new ArgumentException(hiba);
}

object cim = d.Cim.Length == 0 ? (object)DBNull.Value : d.Cim;

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

if (d.Id == 0)
{
using (SQLiteCommand parancs = new SQLiteCommand("INSERT INTO adatok (vnev, knev, fizetes, cim) VALUES (@vnev, @knev, @fizetes, @cim)", kapcsolat))
{
parancs.Parameters.AddWithValue("@vnev", d.Vnev);
parancs.Parameters.AddWithValue("@knev", d.Knev);
parancs.Parameters.AddWithValue("@fizetes", d.Fizetes);
parancs.Parameters.AddWithValue("@cim", cim);
parancs.ExecuteNonQuery();
}

return (int)kapcsolat.LastInsertRowId;
}

using (SQLiteCommand parancs = new SQLiteCommand("UPDATE adatok SET vnev = @vnev, knev = @knev, fizetes = @fizetes, cim = @cim WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@vnev", d.Vnev);
parancs.Parameters.AddWithValue("@knev", d.Knev);
parancs.Parameters.AddWithValue("@fizetes", d.Fizetes);
parancs.Parameters.AddWithValue("@cim", cim);
parancs.Parameters.AddWithValue("@id", d.Id);

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("A dolgozó már nem létezik (közben törölték).");
}
}

return d.Id;
}
}

public static void Torles(int id)
{
using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

using (SQLiteCommand parancs = new SQLiteCommand("DELETE FROM adatok WHERE id = @id", kapcsolat))
{
parancs.Parameters.AddWithValue("@id", id);

if (parancs.ExecuteNonQuery() != 1)
{
throw new InvalidOperationException("A dolgozó már nem létezik (közben törölték).");
}
}
}
}
}
ExecuteReader() és Read()Az olvasó soronként halad. A Read() a következő sorra lép, és hamis, ha nincs több (itt legfeljebb egy sort várunk). Az oszlopot névvel érjük el: olvaso["vnev"].
return nullHa nincs ilyen azonosító, null a válasz. A hívónak ezt ellenőriznie kell.
Mentes: egy metódus, két esetAz Id dönt: 0 az INSERT, több az UPDATE. Az adatkezelő nem tud az ablakról: az eredményt visszaadja, a hibát kivételként dobja.
throw new ...Exception(szöveg)Szándékosan hibát jelez. A kivétel az ablakig emelkedik, ahol a try–catch elkapja, és érthető üzenetet mutat a felhasználónak.
Dupla védelemA Mentes maga is ellenőrzi a Hiba() metódussal az adatot. Így akkor sem kerülhet hibás adat az adatbázisba, ha egy ablak elfelejtené az ellenőrzést.
(int)kapcsolat.LastInsertRowIdA long típusú azonosító int típusra alakítása. Ebben a kis táblában az azonosítók belefér az int tartományába.

A szerkesztő párbeszédablak

Az új dolgozó felvételéhez és a módosításhoz ugyanazt a külön ablakot használjuk. Add hozzá a projekthez: Add → Form (Windows Forms), neve DolgozoUrlap. Elemei: txtVnev, txtKnev, txtCim (MaxLength: 40), nudFizetes, lblHiba, btnMent és btnMegse. Beállítások:

AcceptButton = btnMentAz Enter billentyű a Mentés gombot nyomja.
CancelButton = btnMegseAz Esc billentyű a Mégse gombot nyomja.
btnMegse.DialogResult = CancelA Mégse gomb megnyomása bezárja az ablakot, a hívó Cancel választ kap.
btnMent.DialogResult = NoneA Mentés gomb nem zárja be magától az ablakot: csak akkor zárjuk be, ha az adatok rendben vannak.
FormBorderStyle = FixedDialog, StartPosition = CenterParent, MaximizeBox/MinimizeBox = FalsePárbeszédablak jellegű megjelenés.
DolgozoUrlap.csC#
public partial class DolgozoUrlap : Form
{
private int id;

// az eredmény: egy ÚJ objektum, amely csak OK esetén áll rendelkezésre
public Dolgozo Eredmeny { get; private set; }

public DolgozoUrlap(Dolgozo d)
{
InitializeComponent();

id = d.Id;
this.Text = id == 0 ? "Új dolgozó" : "Dolgozó szerkesztése";

txtVnev.Text = d.Vnev;
txtKnev.Text = d.Knev;
nudFizetes.Value = d.Fizetes;
txtCim.Text = d.Cim;
}

private void btnMent_Click(object sender, EventArgs e)
{
Dolgozo uj = new Dolgozo();
uj.Id = id;
uj.Vnev = txtVnev.Text.Trim();
uj.Knev = txtKnev.Text.Trim();
uj.Fizetes = (int)nudFizetes.Value;
uj.Cim = txtCim.Text.Trim();

string hiba = uj.Hiba();

if (hiba.Length > 0)
{
lblHiba.Text = hiba;
return;
}

Eredmeny = uj;
this.DialogResult = DialogResult.OK; // ez zárja be az ablakot
}
}
A konstruktor paramétereAz ablak a megjelenítendő dolgozót megkapja, és kitölti a mezőket. Új dolgozónál ez egy üres Dolgozo (Id = 0).
Eredmeny { get; private set; }Kívülről csak olvasható tulajdonság. Az ablak belül állítja be.
Új objektum a mentéskorA btnMent_Click új Dolgozo objektumot hoz létre, nem az átadott objektumot módosítja. Így a hívó oldali adat Mégse esetén biztosan nem változik meg.
this.DialogResult = OKA DialogResult beállítása bezárja az ablakot, és ez az érték lesz a ShowDialog() visszatérési értéke.

Jósolj!

A felhasználó az X gombbal (az ablak jobb felső sarkában) zárja be az ablakot. Milyen DialogResult értéket kap a hívó? Mi lenne, ha a btnMent DialogResult tulajdonságát a tervezőben OK értékre állítanád?

Jóslat ellenőrzése

Az X gombbal bezárt ablak DialogResult.Cancel értéket ad. Ha a btnMent tervezői DialogResult tulajdonsága OK lenne, a gombnyomás azonnal bezárná az ablakot, még az ellenőrzés eredményétől függetlenül, és az Eredmeny null maradna. Ezért kell a tulajdonságnak None értéken maradnia, és az OK beállításának a kódban, az ellenőrzés után történnie.

📷 Képernyőkép helye

Mit tegyél ide: a szerkesztő párbeszédablak tervezőnézete (négy mező, Mentés és Mégse gomb, hibafelirat), mellette a Properties ablak az AcceptButton és CancelButton beállításával.Javasolt fájlnév: winforms-3-9-urlap.webp · Alt szöveg: A szerkesztő párbeszédablak tervezőnézete

A főablak: lista és három gomb

A főablakban csak egy táblázat (dgvDolgozok), három gomb (btnUj, btnSzerkeszt, btnTorol) és egy lblUzenet marad. Az SQL eltűnt az ablakból: a DolgozoTar végzi, a kivételeket pedig itt fogjuk el, és alakítjuk felhasználóbarát üzenetté. (A Uzenet, Frissit, SorKijelolese metódus a 3.6 leckéből ismerős.)

Form1.csC#
private int KijeloltId()
{
if (dgvDolgozok.CurrentRow == null)
{
return 0;
}

return Convert.ToInt32(dgvDolgozok.CurrentRow.Cells["id"].Value);
}

private void Szerkeszt(Dolgozo d)
{
using (DolgozoUrlap urlap = new DolgozoUrlap(d))
{
if (urlap.ShowDialog(this) != DialogResult.OK)
{
return; // Mégse vagy X: nem történik semmi
}

try
{
int id = DolgozoTar.Mentes(urlap.Eredmeny);

Frissit(id);
Uzenet("A dolgozó mentve.", false);
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
Frissit(0);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A mentés most nem sikerült.");
}
}
}

private void btnUj_Click(object sender, EventArgs e)
{
Szerkeszt(new Dolgozo());
}

private void SzerkesztKijelolt()
{
int id = KijeloltId();

if (id == 0)
{
Uzenet("Előbb válassz ki egy dolgozót!", true);
return;
}

Dolgozo d = DolgozoTar.Betolt(id);

if (d == null)
{
Uzenet("A dolgozó már nem létezik.", true);
Frissit(0);
return;
}

Szerkeszt(d);
}

private void btnSzerkeszt_Click(object sender, EventArgs e)
{
SzerkesztKijelolt();
}

private void dgvDolgozok_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
SzerkesztKijelolt();
}
}

private void btnTorol_Click(object sender, EventArgs e)
{
int id = KijeloltId();

if (id == 0)
{
Uzenet("Előbb válassz ki egy dolgozót!", true);
return;
}

DialogResult valasz = MessageBox.Show("Biztosan törlöd a kijelölt dolgozót?", "Törlés", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);

if (valasz != DialogResult.Yes)
{
return;
}

try
{
DolgozoTar.Torles(id);
Uzenet("A dolgozó törölve.", false);
}
catch (InvalidOperationException ex)
{
Uzenet(ex.Message, true);
}
catch (Exception ex)
{
DB.Naploz(ex);
MessageBox.Show("A törlés most nem sikerült.");
}

Frissit(0);
}
using (DolgozoUrlap urlap = ...)A párbeszédablakot using blokkban használjuk, hogy a bezárás után az erőforrásai felszabaduljanak. (A ShowDialog után az ablak nem szabadul fel magától, csak elrejtőzik.)
ShowDialog(this)Modális megnyitás: addig nem használható a főablak, amíg a párbeszédablak nyitva van. A this a szülő, így az ablak a főablak közepén jelenik meg.
catch (InvalidOperationException)Az adatkezelő által dobott, várt hibát (a sor közben törlődött) külön ágon, érthető üzenettel kezeljük. Az általános Exception ág a váratlan hibákat naplózza.
Dupla kattintás: e.RowIndex >= 0A táblázat fejlécén is lehet duplán kattintani. A fejléc sorszáma -1, ilyenkor nem szabad szerkeszteni.

Jósolj!

Megnyitsz egy dolgozót szerkesztésre. Amíg az ablak nyitva van, egy másik programban (például a DB Browserben) törlöd ezt a sort, majd a párbeszédablakban a Mentésre kattintasz. Mi történik?

Jóslat ellenőrzése

Az UPDATE nulla sort érint, ezért a DolgozoTar.Mentes InvalidOperationException kivételt dob. A főablak catch ága piros üzenetet jelenít meg (A dolgozó már nem létezik), és frissíti a listát. A program nem omlik össze, és a felhasználó érti, mi történt. A kérdés a konkurens módosítás: több program (vagy felhasználó) egyszerre változtatja ugyanazt az adatot. Ennek a legegyszerűbb esetét kezeltük, a bonyolultabbak (két módosítás ütközése) már haladó téma.

📷 Képernyőkép helye

Mit tegyél ide: a főablak a táblázattal, és előtte a szerkesztő párbeszédablak egy kijelölt dolgozóval, a hibafelirattal („A vezeték- és a keresztnév kötelező.”).Javasolt fájlnév: winforms-3-9-fo-es-urlap.webp · Alt szöveg: A főablak és a szerkesztő párbeszédablak

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 Mentés gomb hibás adatnál is bezárja az ablakot

Üres névvel is bezárul a párbeszédablak, és hiba keletkezik a mentésnél.

Ok és javítás

A btnMent tervezői DialogResult tulajdonsága OK értéken áll: a gombnyomás bezárja az ablakot az ellenőrzés előtt is. Állítsd None értékre, és a DialogResult.OK beállítása legyen a kódban, az ellenőrzés után.

2. A Mégse után is megváltozik az adat

private Dolgozo dolgozo;   // az átadott objektum

private void btnMent_Click(object sender, EventArgs e)
{
dolgozo.Vnev = txtVnev.Text; // az eredetit írja át!
// ...
}
Hiba és javítás

Az osztály referencia típus, ezért a párbeszédablak az eredeti objektumot módosítja, amelyet a főablak is használ. Ha az ellenőrzés hibát talál, vagy a felhasználó utána a Mégsét nyomja, a főablakban már a módosított (el nem mentett) adat látszik. A megoldás: a mentéskor új objektumot hozz létre (lásd a leckét), és csak OK esetén használd.

3. Minden mentés új sort hoz létre

A módosított dolgozó mentésekor a lista egy új sort mutat a régi mellett.

Ok és javítás

Az új objektum Id értéke 0 maradt, ezért a Mentes INSERT-et futtatott. A párbeszédablak a konstruktorban megjegyzi az azonosítót (id = d.Id), és a mentéskor átmásolja az új objektumba (uj.Id = id).

4. NullReferenceException a Betolt eredményén

Dolgozo d = DolgozoTar.Betolt(id);
txtVnev.Text = d.Vnev;
Hiba és javítás

A Betolt null értéket ad, ha nincs ilyen azonosító (például közben törölték). A használat előtt ellenőrizd: if (d == null) { ...üzenet... return; }.

5. Az üres cím szöveg lett az adatbázisban

Az IS NULL lekérdezés nem találja a cím nélküli dolgozókat, mert az üres cím üres szövegként került be.

Ok és javítás

Az üres szöveg és a NULL különbözik. A Mentes az üres címet DBNull.Value értékre alakítja (d.Cim.Length == 0 ? (object)DBNull.Value : d.Cim). Figyelj a (object) típusátalakításra: a feltételes kifejezés két ágának közös típusúnak kell lennie.

6. A dupla kattintás a fejlécen hibát okoz

private void dgvDolgozok_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
SzerkesztKijelolt();
}
Hiba és javítás

A fejlécen kattintva az e.RowIndex értéke -1, és előfordulhat, hogy nincs kijelölt sor, vagy az előző sor szerkesztődik. Ellenőrizd: if (e.RowIndex >= 0).

Mini projekt: eseménynyilvántartó ugyanezzel a szerkezettel

Alkalmazd a leckében látott szerkezetet az esemenyek táblára (id, nev, datum, ar a 3.7 leckéből):

  1. Esemeny osztály: Id, Nev, Datum (DateTime), Ar, és egy Hiba() metódus (a név kötelező, az ár nemnegatív, a dátum nem lehet 2000 előtti).
  2. EsemenyTar osztály: Betolt, Mentes (INSERT vagy UPDATE az Id alapján, a dátumot ISO szövegként tárolva) és Torles.
  3. EsemenyUrlap párbeszédablak: txtNev, dtpDatum, nudAr, lblHiba, Mentés (DialogResult: None) és Mégse gomb; új objektummal tér vissza.
  4. Főablak: táblázat az események listájával, Új, Szerkesztés (dupla kattintás is), Törlés megerősítéssel.
  5. Teszteld: üres név, Mégse, sor törlése közben szerkesztés (DB Browser), a dátum helyes tárolása és visszaolvasása.
Megoldás megnyitása (próbáld ki előbb önállóan!)

A szerkezet azonos a Dolgozo megoldással. A különbség a dátum kezelésében van: a mentéskor ISO szöveggé alakítjuk (3.7 lecke), visszaolvasáskor DateTime.Parse alakítja vissza.

Esemeny.cs és EsemenyTar.cs (részletek)C#
public class Esemeny
{
public int Id { get; set; }
public string Nev { get; set; } = "";
public DateTime Datum { get; set; } = DateTime.Today;
public int Ar { get; set; }

public string Hiba()
{
if (Nev.Length == 0) return "A név kötelező.";
if (Nev.Length > 60) return "A név legfeljebb 60 karakteres lehet.";
if (Ar < 0) return "Az ár nem lehet negatív.";
if (Datum.Year < 2000) return "A dátum nem lehet 2000 előtti.";

return "";
}
}

// EsemenyTar.Betolt: a dátum visszaolvasása
e.Datum = DateTime.Parse(Convert.ToString(olvaso["datum"]), System.Globalization.CultureInfo.InvariantCulture);

// EsemenyTar.Mentes: a dátum ISO szövegként
parancs.Parameters.AddWithValue("@datum", e.Datum.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
📷 Képernyőkép helye

Mit tegyél ide: a kész eseménynyilvántartó: a lista és a szerkesztő párbeszédablak a dátumválasztóval.Javasolt fájlnév: winforms-3-9-kesz.webp · Alt szöveg: Az eseménynyilvántartó rétegekre bontva

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. Figyelmeztetés a Mégse gombnál

Ha a felhasználó a párbeszédablakban módosított valamit, és a Mégse gombot nyomja (vagy az X-et), kérdezzen rá a program: Eldobod a változtatásokat?

Egy lehetséges megoldás

A FormClosing eseményben (0.8, 2.14 lecke) vizsgáljuk, hogy az ablak Cancel eredménnyel zárul-e, és történt-e módosítás. Az eredeti értékeket a konstruktorban mentjük el egy Dolgozo másolatban, és összehasonlítjuk a mezők aktuális tartalmával. Az OK esetén nincs kérdés.

DolgozoUrlap.csC#
private string eredetiAdatok = "";

private string MezokOsszefuzve()
{
return txtVnev.Text + "|" + txtKnev.Text + "|" + nudFizetes.Value + "|" + txtCim.Text;
}

// a konstruktor végén:
eredetiAdatok = MezokOsszefuzve();

private void DolgozoUrlap_FormClosing(object sender, FormClosingEventArgs e)
{
if (this.DialogResult == DialogResult.OK || MezokOsszefuzve() == eredetiAdatok)
{
return;
}

DialogResult valasz = MessageBox.Show("Eldobod a változtatásokat?", "Mégse", MessageBoxButtons.YesNo, MessageBoxIcon.Question);

if (valasz == DialogResult.No)
{
e.Cancel = true;
}
}

2. Törlés a Delete billentyűvel

A táblázatban a Delete billentyű ugyanazt tegye, mint a Törlés gomb (a megerősítő kérdéssel együtt).

Egy lehetséges megoldás

A DataGridView.KeyDown eseményben a Keys.Delete billentyűt vizsgáljuk. Az e.Handled = true megakadályozza, hogy a táblázat maga is kezelje a billentyűt (a DataGridView alapból törölne a DataTable-ből, és ez nem kívánt).

Form1.csC#
private void dgvDolgozok_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Delete)
{
btnTorol_Click(sender, e);
e.Handled = true;
}
}

3. Ki törölte? A konkurencia kipróbálása

Próbáld ki a konkurens módosítás két esetét: (a) szerkesztés megnyitása, majd a sor törlése a DB Browserben, utána Mentés; (b) a törlés gomb megnyomása olyan soron, amelyet közben törölt valaki. Mit látsz, és hol kezeli a program?

Egy lehetséges megoldás

(a) A Mentes UPDATE utasítása nulla sort érint, ezért az InvalidOperationException kivételt a főablak catch ága kezeli (piros üzenet, a lista frissül). (b) A Torles DELETE utasítása nulla sort érint, ugyanaz a kivétel keletkezik, és a törlés gomb catch ága ugyanígy kezeli. Fontos: a DB Browser Write Changes (Ctrl+S) nélkül a törlés nem kerül a fájlba, ezért a teszthez ezt is el kell végezni, és a DB Browser nyitott írása közben a program database is locked hibát is kaphat.

Gyakorlótár: 6 további feladat

Három szinten gyakorolhatsz: alap, közepes és kihívás. Az első négy feladat önálló, kis osztályokról szól, a másik kettő a nyilvántartó projektjéhez kapcsolódik. Csak a próbálkozás után nyisd le a megoldást.

Alap szint

1. Könyv osztály

Írj egy Konyv osztályt: Id (egész szám), Cim, Szerzo (szöveg), Ev (egész szám). A szövegek kezdőértéke üres szöveg legyen. Hozz létre egy példányt, és írd ki a címét egy MessageBox-ban.

Megoldás megnyitása

Az automatikus tulajdonságok ({ get; set; }) rövidek. A kezdőérték (= "") megakadályozza a null szövegeket.

Konyv.cs és Form1.csC#
public class Konyv
{
public int Id { get; set; }
public string Cim { get; set; } = "";
public string Szerzo { get; set; } = "";
public int Ev { get; set; }
}

// Form1.cs
Konyv k = new Konyv();
k.Cim = "Egri csillagok";
k.Szerzo = "Gárdonyi Géza";
k.Ev = 1901;

MessageBox.Show(k.Cim);

2. Ellenőrző metódus

Adj a Konyv osztályhoz egy Hiba() metódust: a cím kötelező, a szerző kötelező, az év 1000 és 2100 között legyen. Üres szöveget ad, ha minden rendben.

Megoldás megnyitása

A szabályokat sorban vizsgáljuk, az első hibánál visszatérünk a leírásával. Így az ablak egyetlen hívással megkapja a hibaüzenetet.

Konyv.csC#
public string Hiba()
{
if (Cim.Length == 0)
{
return "A cím kötelező.";
}

if (Szerzo.Length == 0)
{
return "A szerző kötelező.";
}

if (Ev < 1000 || Ev > 2100)
{
return "Az év 1000 és 2100 között legyen.";
}

return "";
}

Közepes szint

3. A ToString felülírása

A Konyv objektumok kerüljenek egy ListBox-ba úgy, hogy a lista Gárdonyi Géza: Egri csillagok (1901) alakban mutassa őket, és a kijelölt elemből visszakapd a Konyv objektumot.

Megoldás megnyitása

A ListBox az elemek szövegét a ToString() metódusból veszi. Az override felülírja az osztály alapértelmezett szövegét. A kijelölt elem (SelectedItem) az eredeti objektum, amelyet Konyv típusúra kell alakítani (as vagy típusátalakítás).

Konyv.cs és Form1.csC#
// Konyv.cs
public override string ToString()
{
return $"{Szerzo}: {Cim} ({Ev})";
}

// Form1.cs
lstKonyvek.Items.Add(k); // az objektum kerül a listába, a szöveg a ToString()-ből jön

private void lstKonyvek_SelectedIndexChanged(object sender, EventArgs e)
{
Konyv kijelolt = lstKonyvek.SelectedItem as Konyv;

if (kijelolt != null)
{
lblEv.Text = "Megjelenés éve: " + kijelolt.Ev;
}
}

4. Másolat a konstruktorral

Írj a Dolgozo osztályhoz másoló konstruktort, amely egy másik Dolgozo adataiból készít új, független objektumot. Miért hasznos ez a párbeszédablaknál?

Megoldás megnyitása

A másolat független az eredetitől: ha az ablak a másolatot módosítja, az eredeti nem változik. Ha a konstruktor paraméter nélkül is kell, külön meg kell írni (public Dolgozo() { }), mert az osztály a saját konstruktor írásakor az automatikusat már nem adja.

Dolgozo.csC#
public Dolgozo()
{
}

public Dolgozo(Dolgozo masik)
{
Id = masik.Id;
Vnev = masik.Vnev;
Knev = masik.Knev;
Fizetes = masik.Fizetes;
Cim = masik.Cim;
}

// használat:
Dolgozo masolat = new Dolgozo(eredeti);
masolat.Vnev = "Más"; // az eredeti Vnev nem változik

Kihívás szint

5. Keresés a tárban

Írj a DolgozoTar osztályba egy Keres(string nevresz) metódust, amely a névrészletre szűrt DataTable-t adja vissza (paraméterezetten, LIKE és ESCAPE segítségével, a 3.7 lecke szerint), és használd a főablak keresőmezőjéből.

Megoldás megnyitása

A keresés SQL-je a DolgozoTar-ban van, az ablak csak a szöveget adja át, és a táblázatba teszi az eredményt. A LikeVedelem segédmetódus a 3.7 leckéből jön (a DB osztályban public legyen, vagy másold ide).

DolgozoTar.csC#
public static DataTable Keres(string nevresz)
{
DataTable tabla = new DataTable();

using (SQLiteConnection kapcsolat = DB.UjKapcsolat())
{
kapcsolat.Open();

string sql = "SELECT id, vnev, knev, fizetes, cim FROM adatok WHERE vnev LIKE @minta ESCAPE '^' OR knev LIKE @minta ESCAPE '^' ORDER BY vnev, knev";

using (SQLiteCommand parancs = new SQLiteCommand(sql, kapcsolat))
{
parancs.Parameters.AddWithValue("@minta", "%" + DB.LikeVedelem(nevresz) + "%");

using (SQLiteDataAdapter adapter = new SQLiteDataAdapter(parancs))
{
adapter.Fill(tabla);
}
}
}

return tabla;
}

6. Melyik réteg?

Sorold be az alábbi nyolc feladatot a három réteg valamelyikébe (ablak, Dolgozo osztály, DolgozoTar), és indokold: (a) a név kötelező ellenőrzése, (b) INSERT futtatása, (c) üzenet pirosan, (d) a DBNull átalakítása, (e) MessageBox a törlés előtt, (f) a teljes név összefűzése, (g) DataGridView kijelölése, (h) az érintett sorok számának ellenőrzése.

Megoldás megnyitása
FeladatRétegIndok
(a) a név kötelező ellenőrzéseDolgozoÜzleti szabály, egy helyen legyen (Hiba()).
(b) INSERT futtatásaDolgozoTarSQL, csak az adatkezelőben.
(c) piros üzenetAblakMegjelenítés.
(d) DBNull átalakításaDolgozoTarAz adatbázis jellegzetessége, a többi réteg nem ismeri.
(e) MessageBox törlés előttAblakFelhasználói interakció.
(f) teljes név összefűzéseDolgozoAz adat származtatott értéke (TeljesNev).
(g) DataGridView kijelöléseAblakVezérlő kezelése.
(h) az érintett sorok számának ellenőrzéseDolgozoTarAz adatbázis művelet eredményének értelmezése; hibánál kivételt dob.

Gyors önellenőrzés

Válaszolj fejben, és csak utána nyisd le a választ.

1. Miért bontjuk a programot rétegekre?

Hogy minden rész egyetlen feladatot végezzen, és egy változtatás egy helyen legyen (például az SQL csak az adatkezelőben).

2. Mi a tulajdonság (property)?

Az osztály adata, amelyet get olvas, set állít. A { get; set; } az automatikus tulajdonság rövid írásmódja.

3. Miért referencia típus az osztály, és mi ennek a veszélye?

Az értékadás csak egy második hivatkozást hoz létre ugyanarra az objektumra. Ha egy ablak az átadott objektumot módosítja, a hívó objektuma is megváltozik, akár Mégse esetén is.

4. Mit csinál a ShowDialog(), és mit ad vissza?

Modálisan megnyitja az ablakot (addig nem használható a hívó), és a bezáráskor a DialogResult értékét adja vissza (OK, Cancel...).

5. Miért None a Mentés gomb DialogResult tulajdonsága?

Hogy a gomb ne zárja be magától az ablakot. A DialogResult.OK beállítása a kódban történik, az ellenőrzés sikere után.

6. Mire való az ExecuteReader és a Read()?

Lekérdezés eredményét soronként járja be: a Read() a következő sorra lép, és hamis, ha nincs több sor.

7. Mit jelent, ha az UPDATE nulla sort érint?

Nincs ilyen azonosítójú sor (például közben törölték). A program ezt kivétellel jelzi, a felhasználó pedig érthető üzenetet kap.

Mit tudsz már?

  • Saját osztályt írsz tulajdonságokkal és ellenőrző metódussal.
  • Az SQL-t külön adatkezelő osztályba szervezed.
  • Párbeszédablakot nyitsz (ShowDialog, DialogResult), és az eredményt új objektumban veszed át.
  • Érted a referencia típus veszélyét, és másolattal védekezel.
  • A várt és váratlan hibákat külön kezeled.

Következő lecke: kapcsolatok, kulcsok és tranzakciók

Eddig egyetlen táblával dolgoztunk. A következő leckében két táblát kapcsolunk össze idegen kulccsal, és megtanuljuk, hogyan lehet több műveletet egyetlen, oszthatatlan egységként végrehajtani.