[c#] DataAdapter <-> DataSet binden aan gebruikersnterface

Pagina: 1
Acties:

  • reyan
  • Registratie: November 2003
  • Laatst online: 17-12-2021
Ik ben aan het experimenteren met DataSets en het koppelen hiervan aan de gebruikersinterface. De eenvoudige voorbeeldjes met een DataGrid control die je met honderden vindt op Internet krijg ik makkelijk werkende, maar vanzodra ik ietwat complexere zaken probeer kom ik vast te zitten.

Ik ga proberen mijn vragen te schetsen aan de hand van een eenvoudig voorbeeldprogramma.

Ik heb in een database een tabel "clients" welke een aantal kolommen heeft zoals "id" (primary key), "name", "street", "zip",... Nu werk ik aan een applicatie waarlangs ik de data in die tabel kan benadere, aanpassen, verwijderen en toevoegen.

Hiervoor heb ik een form (FormClient) welke een aantal invoercontrols zoals TextBoxes en CheckBoxes bevat. Op de form staan ook een Save, Reset en Cancel knop. De Save knop moet de eventueel veranderde rijgegevens opslaan in de database, de Reset knop herstelt de inhoud van de form naar de beginsituatie en Cancel negeert gewoon de wijzigingen.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public partial class FormClient : Form
{
    DataSet ds = new DataSet();

    public FormClient()
    {
        InitializeComponent();
    }

    public FetchClient(int index)
    {
        NpqslConnection connection = new NpqslConnection("......");
        connection.Open();

        // Hier halen we de gewenste rij op. Omdat we altijd slechts één rij nodig hebben
        // gebruiken we een WHERE in de select in plaats van een RowFilter op het
        // DataSet object. Het is dan immers absurd om alle rijen op te halen als we er
        // toch maar één van gebruiken.
        string sqlstring = "SELECT * FROM clients WHERE id = " + index.ToString();
        NpqslDataAdapter da = new NpqslDataAdapter(sqlstring, connection);

        // Vullen DataSet
        da.Fill(ds, "clients");

        SetDataBindings();
    }

    private SetDataBindings()
    {
        // DataBindings
        this.textBoxName.DataBindings.Add("Text", ds.Tables["clients"], "name");
        this.textBoxStreet.DataBindings.Add("Text", ds.Tables["clients"], "street");
        this.checkBoxVisible.DataBindings.Add("Checked", ds.Tables["clients"], "visible");
        ..
    }
}
Wanneer we vanuit een andere class de form aanroepen als volgt:
C#:
1
2
FormClient form = new FormClient();
form.FetchClient(15);
dan wordt automatisch de rij met 'id' 15 in de form geladen. Dat werkt perfect.

Vraag 1.

Nu moeten we echter gaan herkennen of er gegevens veranderd zijn voor de Save knop.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class FormClient : Form
{
    ..

    private void buttonSave_Click(object sender, EventArgs e)
    {
        // We kunnen steeds uitgaan van Rows[0] omdat er toch maar één rij in de DataTable zit.
        if (ds.Tables["clients"].Rows[0].RowState == DataRowState.Modified)
        {
            // Komt hier nooit?
        }
    }
}
Bij die if in buttonSave_Click gaat het echter al mis... RowState is altijd Unchanged, zelfs wanneer ik de data op de form compleet verander. Ik snap echter niet waarom... Moet ik eerst nog een bepaalde opdracht uitvoeren eer de RowState gezet wordt?

Wanneer ik het iets anders aanpak werkt het wel min of meer:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public partial class FormClient : Form
{
    ..

    public FetchClient(int index)
    {
        ..
        ds.Tables["clients"].RowChanged += new DataRowChangeEventHandler(Clients_RowChanged);
    }

    ..

    private void buttonSave_Click(object sender, EventArgs e)
    {
        ds.Tables["clients"].Rows[0].AcceptChanges();
    }

    void Clients_RowChanged(object sender, DataRowChangeEventArgs e)
    {
        if (ds.Tables["clients"].Rows[0].RowState == DataRowState.Modified)
        {
            // Nu werkt het wel, maar is dit de juiste manier?
        }
    }
}
Nu wordt de code in het if blok wel uitgevoerd wanneer ik op de Save knop druk en de formdata gewijzigd is. De functie ClientSupplier_RowChanged wordt wel tweemaal aangeroepen bij gewijzigde data (eerste keer e.Action = Change / .RowState = Modified wat normaal is, meteen gevolgd door een ietwat onverwachte e.Action = Commit / .RowState = Unchanged). Dat heeft geen invloed op de werking, maar ik vind het wel raar.

Ik vind dit trouwens maar een rare werkwijze. M'n eerste (niet-werkende) poging vond ik veel logischer van opzet. Ik vraag me dan ook af of ik die niet werkende kan krijgen op een of andere manier. Ik ben vast iets stoms vergeten.

Vraag 2.

En eens we dit stukje werkende krijgen, moet de veranderde data nog in de database opgeslagen worden. Hier vraag ik me af of ik dit best manueel kan doen (dus controleren op Modified en dan zelf een "UPDATE clients SET ..." doen, of dit door de DataAdapter te laten gebeuren? De eerste methode ken ik, de tweede krijg ik niet werkende. Als iemand me dat kan uitleggen... En misschien ook beargumenteren waarom je voor de manuele methode zou kiezen of de DataAdapter methode? Mij lijkt de automatische methode logischer, minder code, minder kans op fouten,... Ik krijg het alleen nog niet werkende :)

Vraag 3.

Dan de Reset knop. Wanneer we daar op drukken moet de formdata terug naar de originele toestand gebracht worden (in de veronderstelling dat ondertussen al niet op Save geklikt is).
C#:
1
2
3
4
5
6
7
8
9
public partial class FormClient : Form
{
    ..

    private void buttonReset_Click(object sender, EventArgs e)
    {
        ds.Tables["clients"].Rows[0].RejectChanges();
    }
}
Dit doet schijnbaar niets... Ik vermoed wel dat het DataSet object wel terug gereset wordt, maar de controls blijven hun (veranderde) waarden behouden. Hoe forceer ik dat alle gebonden controls mee geupdate worden en terug de begintoestand tonen?

Vraag 4.

En als laatste belangrijke punt: Een nieuwe rij... Ik wil de form natuurlijk ook gebruiken voor het aanmaken van een nieuwe rij. Ik vraag me echter af hoe ik hierbij tewerk ga. Hoe krijg ik een nieuwe rij? Ik dacht zelf aan een "SELECT * FROM clients WHERE id = 0" waarbij ik weet dat id = 0 niet bestaat. Dit geeft dus geen rijen terug, maar als ik me niet vergis is het DataSet/DataTable object dan wel gezet. Middels een NewRow() ofzo zou ik dan een nieuwe lege rij kunnen aanmaken en die binden aan m'n controls. Maar dat lijkt me nogal een gekke werkwijze. Hoe doe je dit normaal?

Verwijderd

reyan schreef op dinsdag 20 juni 2006 @ 16:59:
Ik ben aan het experimenteren met DataSets en het koppelen hiervan aan de gebruikersinterface.
Ik ben zelf een beetje huiverig voor bv. de DataBinding oplossing van .NET of de "data aware" controls van Delphi, omdat ze a) vaak net niet doen wat ik wil, en b) de boel danig kunnen vertragen. In Delphi scheelt een TADOCommand.DisableControls al gauw 10-tallen procenten in performance, ook al is er geen enkel control gekoppeld aan die dataset...
Ik ben dan ook gewend om 't zelf te schrijven, en koppel ook niet de datarow aan de controls, maar de controls aan de veld-objecten in die row. Voordeel is dat het control niet bij iedere wijziging iets door moet geven aan de datarow (via reflection, ook niet de snelste manier), maar dat de datarow pas iets met die controls hoeft te doen wanneer 'ie zelf iets moet doen (select, update, insert, delete).
De functie ClientSupplier_RowChanged wordt wel tweemaal aangeroepen bij gewijzigde data (eerste keer e.Action = Change / .RowState = Modified wat normaal is, meteen gevolgd door een ietwat onverwachte e.Action = Commit / .RowState = Unchanged).
Vreemd, en niet conform de documentatie van MS: "Unchanged - The row has not changed since AcceptChanges was last called".
Misschien een bijeffect van die NpqslDataAdapter (ken ik niet, en Google blijkbaar ook niet, al kwam "Npsql" en "Npqsql" uit op een russische pagina waar 't blijkbaar PostgreSQL betrof). Dat zou ook verklaren waarom je in je eerste situatie altijd DataRowState.Unchanged te zien krijgt.
Hier vraag ik me af of ik dit best manueel kan doen (dus controleren op Modified en dan zelf een "UPDATE clients SET ..." doen, of dit door de DataAdapter te laten gebeuren?
Ik ga voor het zelf maken van de update/insert/delete queries, maar dat had je ws al begrepen... :)
Hoe krijg ik een nieuwe rij? Ik dacht zelf aan een "SELECT * FROM clients WHERE id = 0" waarbij ik weet dat id = 0 niet bestaat. Dit geeft dus geen rijen terug, maar als ik me niet vergis is het DataSet/DataTable object dan wel gezet. Middels een NewRow() ofzo zou ik dan een nieuwe lege rij kunnen aanmaken en die binden aan m'n controls. Maar dat lijkt me nogal een gekke werkwijze. Hoe doe je dit normaal?
De MicroSoft way (MSSQL) zou dan "SELECT TOP 0 * FROM clients" zijn: je krijgt dan wel 't schema, maar geen data. Komt dus overeen met jouw manier, dan NewRow(), databinden, en uiteindelijk AcceptChanges() of RejectChanges().
Niet mijn manier: ik clear de inhoud van de controls wel, en bepaal op het moment van posten wel of er een insert of een update moet gebeuren (PK-veld(en) leeg? dan insert).

  • PhysicsRules
  • Registratie: Februari 2002
  • Laatst online: 22-12-2025

PhysicsRules

Dux: Linux voor Eenden

reyan schreef op dinsdag 20 juni 2006 @ 16:59:
Vraag 1.
Ik vind dit trouwens maar een rare werkwijze. M'n eerste (niet-werkende) poging vond ik veel logischer van opzet. Ik vraag me dan ook af of ik die niet werkende kan krijgen op een of andere manier. Ik ben vast iets stoms vergeten.
Je verwijst nu rechtstreeks naar je tabel. Gebruik eens de Row property die met de DataRowChangeEventArgs is meegestuurd.
[quote]

Vraag 2.
Om de ingebouwde updatemogelijkheden te gebruiken moet je eerst met een CommandBuilder de SQL commando's voor de DataAdapter genereren. Deze kun je daarna aanroepen.

Vraag 3
Om goed van databindings gebruik te kunnen maken moet je je eens verdiepen in BindingContext en CurrencyManagers. Dit zijn classes die manipulatie van DataBindings verzorgen. Als je een wijziging in een DataSet aanbrengt wordt dat niet automatisch overgebracht op je databinding. Daar zorgen deze classes voor.

Vraag 4
Ove het algemeen is het sowieso verstandig om gebruik te maken van Stored Procedures. Schrijf een spGetClients, spAddClients, spDeleteClients e.d. ipv SQL code in je C# te hebben staan. Dit heeft een aantal voordelen.
  • .Net controleert automatisch op code-insteken bij parameters. Je programma is dus veiliger.
  • Je code is overzichtelijker.
Deze kun je dan ook gebruiken voor het toevoegen van een nieuwe regel. Maak anders gebruik van de BindingContext


Om een ni

  • riezebosch
  • Registratie: Oktober 2001
  • Laatst online: 14-02 12:54
In de laatste .NET Magazine staat een artikel Data binding revival, waarvan de voorbeeldcode beschikbaar is. Helaas het artikel zelf dus niet...

Vreemd, voorgaande nummers staan wel volledig online :?

[ Voor 9% gewijzigd door riezebosch op 21-06-2006 11:06 ]

Canon EOS 400D + 18-55mm F3.5-5.6 + 50mm F1.8 II + 24-105 F4L + 430EX Speedlite + Crumpler Pretty Boy Back Pack


  • whoami
  • Registratie: December 2000
  • Laatst online: 19:13
Vraag 4
Ove het algemeen is het sowieso verstandig om gebruik te maken van Stored Procedures. Schrijf een spGetClients, spAddClients, spDeleteClients e.d. ipv SQL code in je C# te hebben staan. Dit heeft een aantal voordelen.
Da's helemaal niet waar. Stored Procedures zijn handig voor data intensieve taken. In de meeste gevallen is het makkelijker, en beter om een parametrized query te gebruiken. Daarin ben je nl. flexibeler als je bv geen vaste where clausule hebt. Zet die parametrized queries in een DAL , en je bent even veilig en flexibel als je met SP's zou werken.

https://fgheysels.github.io/


  • reyan
  • Registratie: November 2003
  • Laatst online: 17-12-2021
Dankjewel voor jullie antwoorden!

Ik had ook al opgevangen dat de .NET DataBinding nogal traag kon uitvallen, maar op het eerste zicht leek DataBinding eenvoudiger en dus veiliger. Op het tweede zicht blijkt er toch nog heel wat code bij te komen zien, dus ik vraag me af of ik er wel voordeel uit zou halen.

Ik denk dus dat ik DataBinding maar links ga laten liggen en weer terugkeer naar m'n vertrouwde techniek - of iets dat er op lijkt.

De "Npqsl" uit m'n voorbeeld is een typo. Dat moet natuurlijk Npgsql zijn wat wel een bekend ding is en de communicatie met PostgreSQL voorziet.

Vraag 5.

Dus terug naar de oude werkwijze...

Momenteel gebruik ik geen DataSet objecten (DataBinding is een van de redenen dat ik ermee ben gaan spelen) en ik vraag me af of ik die wel moet blijven gebruiken. Ik werk nu als volgt (pseudocode):
C#:
1
2
3
4
5
6
7
8
9
10
sqlSelect("SELECT * FROM clients WHERE id = " + index.ToString());
if (sqlResult)
{
    this.textBoxName.Text = (string)sqlResult["name"];
    this.textBoxStreet.Text = (string)sqlResult["street"];
    this.checkBoxVisible.Checked = (bool)sqlResult["visible"];
    ..
}
else
    showError();
Daarmee vul ik de form. Wanneer een control z'n data wijzigt wordt er een variabele true. Wanneer ik dan op de Save knop druk zie ik meteen of er geupdate moet worden:
C#:
1
2
3
4
5
6
if (this.formDataChanged)
{
    sqlUpdate("UPDATE clients SET name = '" + this.textBoxName.Text + "', street = '" + 
        this.textBoxStreet.Text + "', visible = '" + this.checkBoxVisible.Checked +
        "' WHERE id = " + index.ToString());
}
In werkelijkheid gebeurt dit natuurlijk stored of parametrized (afhankelijk van de situatie) en wordt er ook gekeken of de ingevoerde data wel geldig is, maar ik probeer de voorbeelden zo eenvoudig mogelijk te houden.

Ook een nieuwe rij is zo eenvoudig. Ik heb als conventie dat index = 0 een nieuwe rij betekent. Dan hoeft er geen SELECT te gebeuren om de form te preloaden en de UPDATE wordt een INSERT.

Zoals je ziet heb ik hier geen DataSet voor nodig, maar zou een DataSet hier voordelen kunnen bieden? Ik begrijp dat jullie net als ik zelf de UPDATE/INSERT queries willen opstellen, maar houden jullie de data in een DataSet?

In het bovenstaande voorbeeld is een DataSet misschien wat overbodig, maar ik heb ook complexere gevallen waarbij één form tal van tabellen beïnvloedt en het soms ingewikkeld kan worden om te volgen welke geupdate, geinsert of genegeerd moeten worden. Het mechanisme van de DataSet dat dit bijhoudt vond ik wel interessant.

[ Voor 5% gewijzigd door reyan op 21-06-2006 11:29 ]


Verwijderd

Wanneer in de toekomst je applicatie bv. ook offline moet kunnen worden gebruikt ("verplicht" bij SmartClient apps), of wanneer je i.p.v. puur client-server ook een webservice als data provider wilt gaan gebruiken, heeft een DataSet wel voordelen. Het synchroniseert een stuk gemakkelijker met de "thuis"-database omdat 'ie ook op een offline DataSet bijhoudt wat er gewijzigd is, en hij is ontworpen om goed geserialiseerd te kunnen worden (prettig bij webservices :)).

Maar dat levert ook een hoop overhead en performance penalties op, en in jouw (en mijn) geval zou ik gewoon gaan voor een SqlDataReader en een SqlCommand (maar dan de PostgreSQL varianten daarop ;)). Veel lichter dan een DataSet, 't kan alles wat je hierboven schetst (alleen moet je zelf bijhouden of een row dirty of new is), en vooral veel sneller!
Vooral wanneer je bij een SqlCommand.ExecuteReader meegeeft dat 'ie CommandBehavior.SequentialAccess moet gebruiken (dan gebruikt 'ie een forward readonly cursor) kan bv. het vullen van een DataViewGrid wel 's 10 tot 100x sneller gaan dan met een DataSet en DataBinding.

En als je er ook nog een leuk stel wrapper classes omheenbouwt zodat je bv. design time je edit controls aan de velden in je recordset kunt koppelen i.p.v. steeds
"this.textBoxName.Text = (string)sqlResult["name"];" en zo met 't handje te moeten definieren, wordt 't echt leuk.

Ik ben daar dik een maand geleden mee begonnen, met nauwelijks C# ervaring op wat kleine ASP.NET dingetjes na, en was eerst zwaar gefrustreerd dat ik niet voor elkaar kreeg wat ik wilde (zie [rml][ C#] Rant: non-visual componenten zijn een crime[/rml], wist ik veel wat een DesignerSerialisationVisibleAttribute was :)), maar 't is nu een framework dat al voor 'proof of concept' dingen gebruikt wordt, en dat doet wat ik wil en qua performance rondjes draait om een DataSet oplossing. :)
Pagina: 1