Bob Swart (aka Dr.Bob)
Delphi 8 for .NET en Databases met ADO.NET

Net als Kylix (Delphi for Linux) hebben we heel lang moeten wachten op Delphi for .NET. Maar vlak voor Kerst 2003 kwam het er eindelijk van: met de officiële naam Borland Delphi 8 for the Microsoft .NET Framework is Delphi - na de preview command-line compiler - nu ook een volwaardige ontwikkelomgeving onder .NET geworden. In dit artikel sta ik even stil bij het .NET Framework zelf, en laat vervolgens zien wat de mogelijkheden zijn om met Delphi 8 for .NET toepassingen te bouwen, waarbij ik zal uitgaan van de mogelijkheden van de Personal editie van Delphi 8 for .NET, en de in het .NET Framework ingebouwde ADO.NET mogelijkheden.

Het .NET Framework
Alhoewel het al een aantal jaren "in omloop" is, heeft nog niet iedereen er mee te maken: het .NET Framework. Toch is het nog slechts een kwestie van tijd voor het .NET Framework de "macht" heeft overgenomen. Het wordt nu al standaard meegeleverd met de nieuwste versies van Windows XP (met SP1), en is onderdeel van Windows 2003 (met name voor Servers), en wordt zelfs al automatisch toegevoegd als je bijvoorbeeld Office XP 2003 installeert. En over een aantal jaren zal de volgende versie van Windows uitkomen, nu nog onder de codenaam Longhorn, en daarin zal het .NET Framework zelfs de default Windows API zijn (en de Win32 API zoals we die vandaag kennen zal dan op het tweede plan staan). Dit is een beetje te vergelijken met de overstap van DOS naar Windows 3.x en later Win32. Borland is ruim twee jaar bezig geweest met de Delphi for .NET omgeving, en heeft vorig jaar eerst nog C#Builder uitgebracht. Deze gebruikt dezelfde IDE als Delphi for .NET, maar maakt daarbij gebruik van de Microsoft C# compiler die in het .NET Framework zit ingebouwd. Voor Delphi for .NET moest eerst de Delphi compiler voor .NET worden afgemaakt, maar dat is dan nu ook eindelijk het geval.

Het .NET Framework draait nu nog als een laag bovenop de Win32 API, en biedt operating system en services aan via een soort OO class hierarchy. In plaats van de platte APIs gebuik je in .NET - bij voorkeur - objecten en roep je methoden aan van deze objecten. De .NET classes zijn ingedeeld in namespaces en verzameld in zgn. assemblies (grofweg gezegd de volgende generatie DLL). Omdat nog niet alles uit de Win32 in een .NET assembly terug te vinden is, wordt er nog vaak toch gebruik gemaakt van unsafe, unmanaged code die via DllImport een Win32 API aanroept. Wat dat betreft voelen de huidige versies van het .NET Framework (en SDK) nog een beetje aan als Win32s, dat ook alleen maar de overgang naar puur Win32 aangaf. Het zou me niet verbazen als we over een aantal jaren een puur .NET besturingssysteem hebben, zonder Win32 API er meer onder. Voordeel is dan dat alle toepassingen in safe, managed mode zullen draaien, en er meteen wat minder kans is op beveiligingsproblemen die veroorzaakt worden door buffer overruns, etc. Dat is in mijn ogen trouwens ook een van de redenen om nu al naar .NET over te stappen - op de web server dan - en diep in ASP.NET te duiken.

Het is is in mijn ogen dan ook niet de vraag ".NET of niet?" die ik soms om me heen wel hoor, maar eerder de vraag ".NET nu, of nog niet?". Want dat .NET er zal komen, dat staat voor mij wel vast. Daarnaast zal Win32 niet verdwijnen - net zo min als DOS of 16-bits Windows toepassingen verdwenen zijn. Maar nieuwe toepassingen kunnen beter worden ontwikkeld met het .NET Framework in het achterhoofd. En nu Borland met Delphi 8 for .NET ook aan ons Delphi ontwikkelaars de mogelijkheid biedt om toepassingen voor .NET te bouwen, zie ik alleen maar positieve ontwikkelingen, en in ieder geval een migratiepad voor Delphi ontwikkelaars naar de wereld van .NET.

Delphi 8 for .NET
Er zullen ongetwijfeld trial versie van de Enterprise of Architect versie van Delphi 8 for .NET beschikbaar komen, maar die werken meestal maar korte tijd. Het is echter ook de verwachting dat er een soort Personal editie van Delphi 8 for .NET uit zal komen - net als nu al voor Delphi en C#Builder beschikbaar is. Deze editie zal naar verwachting ook gratis zijn, met de beperking dat je er geen commerciële software mee mag maken (je mag er geen geld aan verdienen), maar wel voor eigen gebruik programma's mee mag maken. Mijn verwachting is dat een heleboel mensen dat zullen aangrijpen om hun eerste (ontwikkel-) stappen op .NET te zetten. En daarbij zal er wel flink wat veranderd zijn, met name op het gebied van databases. Delphi onder Windows kent de BDE voor dBASE, Paradox en FoxPro, en SQL Links voor grote databases als Oracle, Microsoft SQL Server, IBM DB2, Informix, Sybase en InterBase. De laatste kun je ook met IBExpress benaderen, en er is sinds Delphi 6 ook een verzameling cross-platform componenten onder de naam dbExpress (om met InterBase, Oracle, SQL Server, DB2, Informix en MySQL te werken).

VCL for .NET of niet?
Wie met Delphi 8 for .NET gaat werken zal merken dat er twee mogelijkheden zijn (alhoewel ik niet zeker weet of die allebei in de Personal editie zullen zitten). Enerzijds kun je VCL for .NET toepassingen bouwen, en anderszijds niet-"VCL for .NET" toepassingen (en dat zijn dan WinForms, Web Forms of alles wat in ieder geval niks met de VCL for .NET te maken heeft). Dit is een grove tweesplitsing, die wel wat gevolgen heeft, en waarvoor je aan het begin van een nieuw project een bewuste keuze moet maken. Het voordeel van VCL for .NET is dat je zonder al te veel moeite deze toepassingen ook met Delphi 7 tot een Win32 toepassing kunt compileren. Daarnaast maakt de VCL for .NET gebruik van GDI voor het grafische werk, terwijl WinForms gebruik maakt van het langzamere GDI+. Het verschil is niet zo heel groot, maar is er wel. De VCL for .NET toepassingen hebben de beschikking over de database componenten die we kennen uit Delphi voor Windows, namelijk met een TDataSet en een TDataSource. Alle niet VCL for .NET toepassingen werken net even anders, en gebruiken een .NET DataSet - die nog het meest te vergelijken is met een TClientDataSet - en helemaal geen TDataSource. De data-aware componenten die we kennen uit Delphi voor Windows zijn onder .NET dan ook uitsluitend beschikbaar voor VCL for .NET toepassingen, net als de Data Module overigens, wat ook een specifieke Borland oplossing is, en niet standaard onder .NET beschikbaar (maar dus wel voor VCL for .NET).

.NET Data Access
Wat biedt het .NET Framework dan op database gebied? Welnu, Microsoft heeft pasgeleden een speciale "uitgekleedde" editie van de SQL Server beschikbaar gemaakt voor gratis gebruik. Het betreft hier MSDE, en deze is samen met C#Builder Personal te downloaden van de Borland website, dus zal ook wel bij Delphi 8 for .NET zitten denk ik. Met deze database kun je onder .NET je eigen database toepassingen bouwen - met Delphi for .NET - al moet je daarbij wel gebruik maken van de .NET specifieke database componenten genaamd ADO.NET. Delphi 8 for .NET zal naast ADO.NET ondersteuning ook een verzameling Borland Data Providers for .NET bevatten, maar deze zitten zeer waarschijnlijk niet in de Personal editie van Delphi 8 for .NET (net als Delphi 7 Personal ook al geen database componenten bevat). Omdat ADO.NET echter onderdeel is van het .NET Framework zelf, kun je toch database toepassingen maken met de Personal editie van Delphi 8 for .NET (alhoewel deze op het moment van schrijven nog niet beschikbaar is).

Na deze lange inleiding over het .NET Framework, Delphi 8 for .NET en database oplossingen onder .NET, wil ik de rest van het artikel besteden aan het introduceren van de ADO.NET manier om met databases te werken, en dan met name met de gratis MSDE. Hiermee kan iedereen dus zijn eigen database toepassingen bouwen zonder dure ontwikkelomgevingen te hoeven kopen, en nog steeds Delphi als taal gebruiken!

ADO.NET
De onderstaande tabel laat nog eens in het kort zien wat op dit moment de database ondersteuning is in Delphi 8 for .NET. Ten opzichte van Delphi 7 missen we SQL Links (maar die was deprecated) en dbGo for ADO (die zit inmiddels wel in Delphi 2005).

VCL for .NET.NET (WinForms & ASP.NET)
BDEdBase, Paradox, FoxProBDP for .NETInterBase, SQL server, Oracle, DB2, MS Access
IBExpressInterBaseADO.NETSQL Server, ODBC.NET, OLEDB, Oracle, ...
dbExpressInterBase, SQL Server, Oracle, DB2, Informix, MySQL, SQL Anywhere

Om gebruik te maken van ADO.NET met Delphi 8 for .NET moet je naast Delphi 8 for .NET (die zelf het .NET Framework 1.1, .NET SDK 1.1, en wellicht ook de Visual J# runtime nodig heeft), ook de MSDE sp3a installeren. Alle benodigdheden zijn te downloaden van de Borland website op dit moment (met uitzondering van Delphi 8 for .NET Personal zelf).

Zoals de tabel laat zien, kunnen we ADO.NET het beste gebruiken in een WinForms toepassing, dus we starten Delphi 8 for .NET en beginnen een nieuwe WinForms toepassing. Hier zal ik de ADO.NET componenten aan toevoegen, die overigens te vinden zijn in de Data Components categorie van het Tool Palette.

Maar voor ik daadwerkelijk componenten ga toevoegen, eerst nog een korte uitleg over hoe ADO.NET in elkaar zit.

Connected vs. Disconnected
Je kunt namelijk op twee manieren met ADO.NET werken: direct verbonden (connected) met de database, of niet verbonden (ook wel disconnected genoemd). Dit laatste heeft de grootste kracht, omdat je hiermee een dataset kunt vullen met het resultaat van een zoekopdracht, en deze data naar een heel andere plek kunt "sturen" waar hij vervolgens verwerkt kan worden. En dat heeft zelfs voordelen op je eigen lokale machine, want het alternatieve connected model heeft ook als eigenschap dat het resultaat niet gewijzigd mag worden, en alleen van begin (eerste record) tot eind (laatste record) gelezen mag worden. Wat we ook wel read-only uni-directional noemen, en eerder ook bij dbExpress in Delphi en Kylix zagen. De inhoud van een dbExpress read-only uni-directional dataset wordt in Delphi en Kylix door een TSQLDataSetProvider in een TClientDataSet gestopt, wat als resultaat het disconnected model heeft.

ADO.NET Voorbeeld
Laten we een voorbeeld bouwen, en dan kun je vanzelf zien wat het verschil is tussen de conencted en disconnected benaderingen (en leer je meteen een beetje hoe je ADO.NET met Delphi 8 for .NET kunt gebruiken, wat nooit weg is). In ga ervanuit dat je een WinForms project bent gestart, en zet nu een SqlConnection component op het WinForms - dit component kun je vinden in de Data Components categorie van het Tool Palette. Met dit component kunnen we de verbinding met een SQL Server of MSDE opzetten, en doen dat door een waarde in de ConnectionString te zetten. Voor iedere database is die weer anders, maar het daaropvolgende gebruik van de componenten is anders. Dus wie meer wil spelen zal even moeten kijken naar de juiste ConnectionString, maar als die eenmaal werkt dan is de rest niet zo'n probleem meer. Op mijn machine kan ik als ConnectionString naar MSDE het volgende gebruiken: "Data Source=.; Initial Catalog=master; Integrated security=SSPI". Als ik deze waarde toeken aan de ConnectionString property van de SqlConnection component, kan ik die vervolgens proberen te openen met Open. Als dat lukt, dan is het moeilijkste al achter de rug. Anders zul je even moeten "proberen" tot je de juiste ConnectionString te pakken hebt. Om de Delphi source code te kunnen schrijven zet ik een Button op het WinForm, en schrijf de volgende code in the Click event handler:

  procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
  begin
    try
      SqlConnection1.ConnectionString :=
       'Data Source=.; Initial Catalog=master; Integrated security=SSPI';
      try
        SqlConnection1.Open // connection openen
      finally
        SqlConnection1.Close
      end
    except
      on E: Exception do
        MessageBox.Show(E.Message)
    end
  end;
Let op dat je de SqlConnection ook weer afsluit met Close, anders blijft de verbinding open staan (en met MSDE mag en kun je maar een beperkt aantal open verbindingen hebben met je database).

ADO.NET Connected
Tijd om eens wat met de database te doen. Ik verstuur regelmatig mailtjes naar verschillende personen, en vanuit verschillende software pakketten - zelfs vanaf verschillende machines - en vind het daarbij handig als ik een centrale plek heb met daarin mijn adresboek. Dan hoef ik dit adresboek maar op één plek te onderhouden, maar wil ik er toch vanaf verschillende machines bij kunnen, als dat nodig is. Daarvoor gaan we nu een nieuwe tabel maken in een MSDE database, die we via ADO.NET kunnen aansturen. De tabel noem ik adresboek, en ieder record heeft twee velden: naam en email, waarbij naam de unieke key is voor de index genaamd "namen". Met behulp van een SqlCommand component kan ik SQL statements naar de SQL Server / MSDE database sturen. Dan moet ik wel de Connection property naar de SqlConnection laten wijzen, en in de CommandText property het SQL commando zetten dat ik wil uitvoeren. Zie de volgende listing voor een voorbeeld:

  procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
  var
    Command: SqlCommand;
  begin
    try
      SqlConnection1.ConnectionString :=
        'Data Source=.; Initial Catalog=master; Integrated security=SSPI';
      try
        SqlConnection1.Open // connection openen
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;

      Command := SqlCommand.Create;
      Command.Connection := SqlConnection1;

      Command.CommandText := 'create table adresboek ' +
                             '(naam nvarchar(42), email nvarchar(42))';
      try
        Command.ExecuteNonQuery; // create table
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;

      Command.CommandText := 'create unique index namen on adresboek (naam)';
      try
        Command.ExecuteNonQuery; // create table
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;

      Command.CommandText := 'insert into adresboek '+
                             'values(''Bob Swart'', ''b.swart@chello.nl'')';
      try
        Command.ExecuteNonQuery; // insert record
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end

    finally
      SqlConnection1.Close
    end
  end;
Behalve het create table commando, heb ik bovenstaande listing ook al een insert commando toegevoegd, om mijzelf met naam en e-mail adres al (als test) toe te voegen aan de adresboek tabel. Beide keren moet ik met ExecuteNonQuery de inhoud van het Command component uitvoeren. Als alternatief voor ExecuteNonQuery kun je ook ExecuteReader aanroepen, maar die verwacht dan dat het SQL commando een resultaat dataset oplevert, en dat is hier (nog) niet het geval, maar dat gaan we nu meteen doen. Als er namelijk data in de tabel zit, kun je die met een select commando eruit halen. Eventueel kun je aan de where clause bepalen welke records je wilt zien (kun je zoeken op achternaam bijvoorbeeld), maar in mijn volgende voorbeeld haal ik gewoon alle namen op en stop die in een lange string die ik vervolgens in een messagebox laat zien (dat moet je dus niet doen als de tabel meer dan een dozijn records bevat, maar het is slechts een voorbeeld).
  procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
  var
    Command: SqlCommand;
    DataReader: SqlDataReader;
    Str: String;
  begin
    try
      SqlConnection1.ConnectionString :=
        'Data Source=.; Initial Catalog=master; Integrated security=SSPI';
      try
        SqlConnection1.Open; // open connection
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;
      Command := SqlCommand.Create;
      Command.Connection := SqlConnection1;
      Command.CommandText := 'select * from adresboek';
      try
        DataReader := Command.ExecuteReader;
        Str := 'Lijst uit adresboek:';
        while DataReader.Read do
        begin
          Str := Str + #13#10 +
            DataReader.GetString(0) + ': ' + // 1e veld, type string
            DataReader.GetString(1);         // 2e veld, type string
        end;
        MessageBox.Show(Str)
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;
    finally
      SqlConnection1.Close
    end
  end;
Zoals je kunt zien roepen we nu de ExecuteReader methode aan, en die geeft een SqlDataReader terug als resultaat. Deze SqlDataReader kunnen we gebruiken om het resultaat van de SQL select query te bekijken. We kunnen het resultaat echter niet wijzigen; voor aanpassingen van de database moeten we iets anders bedenken (daar komen we zo op). Ook kunnen we alleen maar van boven naar beneden door de SqlDataReader heen lopen; het is niet mogelijk om terug te gaan. Dat is soms lastig, maar maakt niet zoveel uit als je alleen maar het resultaat van een query wilt hebben om er vervolgens zelf iets mee te doen (zoals het stoppen in een string). Als je het resultaat in een DataGrid op het scherm wilt laten zien dan moet je iets anders doen (en dat laat ik ook straks zien).

De SqlDataReader heeft een methode Read die we in een while loop kunnen aanroepen om naar het eerste en de daaropvolgende records te gaan. Na iedere aanroep verschuift het huidige record, en kunnen we met GetString, GetInt32 of andere Get- methoden de inhoud van de velden van het huidige record ophalen. Daarbij moet je goed weten van het type van het veld is. In ons geval is dat twee keer een string, dus ik kan twee keer GetString gebruiken, en hoef als argument alleen maar het nummer van het veld mee te geven - een numering die bij 0 begint, dus het eerste veld krijg je met GetString(0) en het tweede met GetString(1).

Het resultaat is op de volgende screenshot te zien: de lijst met daarin (alleen) mijn naam en e-mail adres.

Maar in plaats van alleen maar het resultaat op te halen, willen we dat natuurlijk ook wel eens kunnen veranderen. En daar hebben we de .NET DataSet voor nodig.

ADO.NET Disconnected
Je kan een SQL update commando in de CommandText property van een SqlCommand component stoppen, en dan weer met ExecuteNonQuery dit SQL commando naar de database sturen (vergelijkbaar met het SQL insert commando van twee listings geleden). Echter, meestal wil je de updates (automatisch) laten uitvoeren als resultaat van een bewerking door een eindgebruiker, bijvoorbeeld in een DataGrid. Ga naar de Data Controls category van de Tool Palette, en zet een DataGrid control op de form. In dit DataGrid gaan we de inhoud van mijn adresboek neerzetten, zodanig dat ik er ook nieuwe regels aan kan toevoegen, en updates kan maken en kan opsturen. Voor dat laatste heb ik wel een tweede button nodig. Ik heb er dan eentje om de data op te halen, en eentje om de updates terug te sturen. Tot slot hebben we een SqlDataAdapter nodig, alsmede een .NET DataSet. Dit zijn allebei non-visuele componenten, die dus onderaan de Designer komen te staan. Het WinForm ziet er nu als volgt uit:

In de eerste button moet ik de code schrijven om een select statement uit te voeren, maar deze keer niet om het resultaat niet in een read-only unidirectional SqlDataReader, maar om het resultaat in een .NET DataSet te stoppen, dat we vervolgens gebruiken om het DataGrid naar te laten wijzen. Voor het eerste deel hebben we de SqlDataAdapter nodig, en van dit component gebruiken we de ingebouwde SqlCommand property om daarvan de SQL query in de CommandText te zetten. Waar een "losse" SqlCommand via ofwel ExecuteNonQuery ofwel ExecureReader de query kan uitvoeren, daar kan een SqlDataAdapter de query uitvoeren en het resultaat automatisch in een .NET DataSet stoppen met behulp van de Fill methode. Fill krijgt als eerste argument de .NET DataSet mee, en als tweede argument de naam die we aan de resultaatset willen geven. In tegenstelling namelijk tot de dataset componenten die we in Delphi gewend zijn, kan een .NET DataSet meerdere tabellen (of resultaten van queries) bevatten, die allemaal DataTables zijn. In de aanroep die ik gebruik, geef ik de nieuwe DataTable de naam 'Adresboek'. Nadat we een DataTable in de .NET DataSet hebben gevuld, kunnen we de inhoud hiervan afbeelden in een DataGrid. Daarvoor moeten we eerst de DataSource property van de DataGrid laten wijzen naar de .NET DataSet, en daarna aangeven welke van de potentieel vele DataTables in de .NET DataSet we willen laten zien. Dat laatste kunnen we aangeven met de DataMember property, die ik de waarde 'Adresboek' geef (dezelfde waarde die ik bij de Fill methode meegaf dus).

  procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
  begin
    try
      SqlConnection1.ConnectionString :=
        'Data Source=.; Initial Catalog=master; Integrated security=SSPI';
      try
        SqlConnection1.Open // connection openen
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;
      SqlDataAdapter1.SelectCommand.Connection := SqlConnection1;
      SqlDataAdapter1.SelectCommand.CommandText := 'select * from adresboek';
      try
        SqlDataAdapter1.Fill(DataSet1, 'Adresboek');
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end;
      dataGrid1.DataSource := DataSet1;
      dataGrid1.DataMember := 'Adresboek'
    finally
      SqlConnection1.Close
    end
  end;
Als de deze toepassing compileren en runnen, dan kunnen we op de eerste button drukken om het DataGrid te vullen met de gegevens uit de adresboek tabel:

In het DataGrid kunnen we wijzigingen maken, bijvoorbeeld mijn e-mail adres wijzigen van b.swart@chello.nl naar drbob@chello.nl (die komen allebei aan), of een nieuwe naam met e-mail adres toevoegen aan het adresboek. In beide gevallen zal ik de inhoud van het DataGrid - die verbonden is met de .NET DataSet - naar de onderliggende database moeten sturen, en daarvoor kan ik de Update methode van de SqlDataAdapter aanroepen. Die gaat er wel vanuit dat er dan ook iets in de UpdateCommand van de SqlDataAdapter staat. Omdat we dat niet met de hand hebben gedaan - of willen doen - kunnen we een speciale SqlCommandBuilder gebruiken om de SQL update, delete en insert statements automatisch te laten genereren. Het was daarvoor wel noodzakelijk dat de tabel een unieke index heeft - en daarom hebben we destijds de "create unique index" aangeroepen bij het aanmaken van de tabel zelf. Zonder een unieke index is de SqlCommandBuilder niet in staat om de juiste where-clause bij de update en delete commando's te genereren.

  procedure TWinForm.Button2_Click(sender: System.Object; e: System.EventArgs);
  var
    CB: SqlCommandBuilder;
  begin
    try
      SqlConnection1.ConnectionString :=
        'Data Source=.; Initial Catalog=master; Integrated security=SSPI';
      try
        SqlConnection1.Open; // connection openen
        CB := SqlCommandBuilder.Create(SqlDataAdapter1);
        SqlDataAdapter1.UpdateCommand := CB.GetUpdateCommand;
        SqlDataAdapter1.InsertCommand := CB.GetInsertCommand;
        SqlDataAdapter1.DeleteCommand := CB.GetDeleteCommand;
        SqlDataAdapter1.Update(DataSet1,'Adresboek')
      except
        on E: Exception do
          MessageBox.Show(E.Message)
      end
    finally
      SqlConnection1.Close
    end
  end;
Hiermee kunnen we alle mogelijke aanpassingen in het DataGrid automatisch doorsturen naar de onderliggende database tabel voor mijn adresboek.

Natuurlijk konden een heleboel properties ook al met de hand in de Object Inspector ingesteld worden, maar door het met Delphi 8 for .NET source code te laten zien hoop ik de werking van ADO.NET een beetje duidelijker te hebben gemaakt.

Wie nog vragen of opmerkingen heeft, kan die altijd per e-mail aan mij kwijt. Wie op de hoogte wil blijven van de laatste ontwikkelingen zou daarnaast zeker eens moeten overwegen om mijn Delphi for .NET clinic bij te wonen.


This webpage © 2004-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.