Bob Swart (aka Dr.Bob)
Delphi 6 WebSnap (Custom) Adapters

WebSnap is de nieuwe cross-platform architecture voor het bouwen van web server toepassingen in de Enterprise versies van Delphi 6 en Kylix 2. WebSnap bevat een flinke hoeveelheid wizards, componenten en design-time editors, en is daarnaast ook nog eens uitbreidbaar met eigen hulpmiddelen. In dit artikel laat ik zien hoe een custom Adapter component (voor de afhandeling van creditcard informatie) te bouwen is.

Adapters
Maar eerst wil ik de centrale rol van de TAdapters binnen WebSnap laten zien. Een Adapter kun je zien als de lijm tussen de data en de presentatie laag. De informatie die je wilt presenteren kan hierbij uit een aantal bronnen komen, waaronder database tables (uiteraard met de TDataSetAdapter component). De presentatie, oftewel de opbouw van de resulterende HTML pagina's gaat met behulp van een TAdapterPageProducer - een speciale PageProducer die met Adapters kan omgaan.
Ten einde de koppeling tussen informatie en presentatie mogelijk te maken bestaat een adapter component uit twee onderdelen: velden en akties. Als voorbeeld zal ik met behulp van een standaard TAdapter iets bouwen - een creditcard verificatie pagina - die ik later in dit artikel in één standard component TCreditCardAdapter zal inbouwen.
Start nu Delphi 6 Enterprise (of Kylix 2 Enterprise), en begin een nieuwe WebSnap toepassing (via File | New - Other, ga naar de WebSnap tab en kies voor WebSnap Appication). Kies een web server toepassing die je kunt testen op je machine - afhankelijk van de web server die erop staat. Bij twijfel kun je altijd een Web App Debugger toepassing maken, die je kunt testen zonder web server.
Kies voor een Page Module, en verander de Page Name in "Home" (of iets dergelijks). Omdat ik meteen in deze home pagina het gebruik van een Adapter wil laten zien, moeten we even op de "Page Options" knop drukken en in de Application Module Page Options dialoog aangeven dat ik een AdapterPageProducer wil gebruiken in plaats van - default - een normale PageProducer. De laatste is namelijk gewoon een "oude" WebBroker PageProducer, die niet met Adapters om kan gaan.

figuur 1. websnap page module

Figuur 1 laat het resultaat zien. Vijf componenten, waaronder de AdapterPageProducer. Ga nu naar de WebSnap tab en bekijk de componenten erop. Er zijn al een aantal Adapter componenten aanwezig, namelijk (links) de TAdapter, TPagedAdapter, TDataSetAdapter, TLoginFormAdapter, etc. Daarnaast zie een ApplicationAdapter component in Figuur 1 - een van de zgn. globale adapters, waar je er maar eentje per toepassing van nodig hebt. Een ander voorbeeld is de EndUserAdapter en EndUserSessionAdapter (nodig om in te loggen).

TAdapter
We beginnen met een normale TAdapter component. Zet die op de Page Module, en klik er met de rechtermuisknop op. Je kunt nu kiezen voor de Fields Editor (om nieuwe Adapter velden aan te maken) en de Actions Editor (om uiteraard nieuwe Adapter akties aan te maken).

Adapter Fields
De Adapter velden vormen de doorgeefluiken voor de informatie die je wilt presenteren. Ze kunnen of zelf direkt de gegevens opslaan, of die ergens anders vandaan halen. Voor de standaard TAdapter zijn standaard zes mogelijke soorten Adapter velden aanwezig: de AdapterBooleanField, het generieke AdapterField (een string of Variant waarde), de AdapterFileField, de AdapterImageField, de AdapterMemoField en tot slot de AdapterMultiValueField. Ieder veld soort is afgeleid van de TCustomAdapterField die geen gepubliceerde properties of events heeft, maar wel al intern de OnGetValue aanroept om de waarde van het Adapter veld op te halen, de valideren (met OnValidateValue), te wijzigen (OnUpdateValue) en te laten zien (met OnGetDisplayText). Zie de on-line help voor wat meer informatie over de verschillende adapterveld soorten.
Voor ons voorbeeld wil ik een drietal "normale" Adapter velden gebruiken, dus voeg er drie toe in de Fields Editor. Geef ze de namen AdaptCreditCardNumber, AdaptExpirationDate en AdaptUserName.

figuur 2. adapter fields editor

Selecteer nu elk van de drie Adapter velden en gebruik de Object Inspector om de DisplayLabel properties een fijne tekst te geven (die zien we straks nog terug). Je kan ook de Required property op True zetten voor alle velden, omdat ze allemaal verplicht zijn voor de credit card verificatie. De velden hebben een aantal event handlers, waaronder de OnGetValue die we kunnen gebruiken om de waarde van het veld op te halen (bijvoorbeeld uit een vorige bezoek of sessie). Dat zullen we zo zien; eerst een aktie toevoegen.

Adapter Actions
Nu we een drietal Adapter velden hebben aangemaakt wordt het tijd om na te denken over de aktie die we eraan toe kunnen voegen. Sluit de Adapter Fields Editor en klik weer met de rechtermuisknop op de Adapter component, maar kies nu de Adapter Actions Editor. Hier kunnen we maar één soort aktie toevoegen: de AdapterAction. Geef hem de naam UseCreditCard, en geef de DisplayLabel property weer een leuke tekst.

figuur 3. adapter actions editor

De UseCreditCard Adapter aktie heeft een vijftal event handlers, waaronder de OnExecute (de belangrijkste), maar ook OnGetEnabled, OnGetParams, OnBeforeGetResponse en OnAfterGetResponse.

TAdapterPageProducer
Voordat ik de andere adapters laat zien, lijkt het me eerst zinvol om de samenwerking tussen de adapters en de TAdapterPageProducer te demonstreren. Omdat ze allebei al op dezelfde Page Module staan, kun je direkt dubbelklikken op de AdapterPageProducer. Dit resulteert in de Web Page Editor, die sommige mensen wellicht nog kennen uit de Delphi 5 InternetExpress tijd. Hij werkt inderdaad nog precies hetzelfde, alleen nu met andere componenten (gekoppeld aan WebSnap adapter velden en akties).
Het scherm bestaat uit drie delen. Linksboven is de boom met web componenten. Daar meteen rechts van is het window waarin de kinderen van het huidig geselecteerde component in staan. Nodig, omdat links alleen maar componenten staan die kinderen hebben of kunnen krijgen (dus alle "bladeren" staan alleen maar rechts - is soms even zoeken). Onderaan staat de preview, zowel de browser preview als de HTML en het server-side script dat gegenereerd wordt.
Met de rechtermuisknop of de Insert knop kun je nieuwe componenten toevoegen als kind van het huidig geselecteerde component in de boom. Begin met een AdapterForm, en daaronder een AdapterFieldGroup, AdapterCommandGroup en AdapterErrorList. We krijgen nu meteen een paar waarschuwingen te zien:

figuur 4. web page editor en design-time warnings

Merk op dat de AdapterErrorList is wel zichtbaar in het rechter window, maar niet links (omdat er geen subcomponenten meer onder kunnen komen).
Om de warnings op te lossen moeten we de DisplayComponent property van de AdapterCommandGroup laten wijzen naar de AdapterFieldGroup, en de Adapter propert van zowel de AdapterFieldGroup als de AdapterErrorList laten wijzen naar de Adapter component die op de Page Module staat (dus Adapter1, en niet de ApplicationAdapter). Het resultaat ziet er dan al een stuk beter uit:

figuur 5. web page editor met preview in de browser

Nu nog optioneel de volgorde veranderen (zodat bijvoorbeeld de AdapterError list bovenaan de lijst staat, en de eventuele foutmeldingen dus bovenaan het scherm komen te staan), en dan kunnen we het geheel gaan testen.

Eerste Test
Bewaar alles (bijvoorbeeld de Page Module in wsWebMod.pas en het project in CGI.dpr), compileer de WebSnap toepassing, en gebruik hem op de geschikte wijze (een CGI executable of ISAPI DLL zal naar de scripts of cgi-bin directory moeten, een Web App Debugger executable kun je direct vanuit de Delphi 6 IDE uitvoeren). Vergeet bij een CGI of ISAPI toepassing niet om ook de wsWebMod.html mee te nemen. Je kunt de toepassing als CGI executable in de browser zien als http://localhost/scripts/CGI.exe

figuur 6. websnap toepassing in internet explorer

Als je de WebSnap toepassing op een machine zet waar Delphi 6 zelf niet ook staat, zul je een tweetal .tlb bestanden uit de Delphi6\bin directory naar die machine moeten meenemen, en aldaar moeten registreren met tregsvr. Dit kan met:
  tregsvr WebBrokerScript.tlb
  tregsvr stdvcl40.dll
Je hebt daarnaast ook de Microsoft Script Engine nodig, maar die staat al op elke machine waar Windows 2000 of Internet Explorer versie 5 of hoger staat. Indien nodig kun je de Microsoft Script Engine ook zelf ophalen van de Microsoft website te http://msdn.microsoft.com/scripting/

Als je de drie editvelden invult en dan op "Use Credit Card" drukt gebeurt er verder niks: je krijgt weer hetzelfde scherm met lege velden. Om iets zinvols te doen met de gegevens zullen we de OnExecute event handler van de UseCreditCard Adapter aktie moeten invullen, net als de OnGetValue event handlers van de drie Adapter velden.

OnExcecute
In de OnExecute kunnen we de credit card gegevens testen, maar ook opslaan om in een later scherm te gebruiken. Dit kan in een speciaal WebSnap sessie object, waarvoor we het TSessionServices component nodig hebben. Maar omdat die niet werkt in CGI toepassingen (het SessionService object wordt na ieder request weer uit het geheugen verwijderd), sla ik voor dit voorbeeld de gegevens maar even op in een test bestand, en haal ze er in de OnGetValue weer uit. Puur om te laten zien dat het werkt, meer niet.
Klik weer met de rechtermuisknop op de Adapter component en selecteer de Action Editor. Kies hierin voor de UseCreditCard action en ga naar de Object Inspector om zijn OnExecute event handler als volgt in te vullen:

  const
    sCreditCard     = 'CreditCard';
    sCardNumber     = 'CardNumber';
    sExpirationDate = 'ExpirationDate';
    sUserName       = 'UserName';
    IniFileName     = 'WebSnap.ini';

  procedure THome.UseCreditCardExecute(Sender: TObject; Params: TStrings);
  var
    Value: IActionFieldValue;
    Logfile: TIniFile;
  begin
    LogFile := TIniFile.Create(IniFileName);
    try
      Value := AdaptCreditCardNumber.ActionValue;
      if Value.ValueCount > 0 then
        Logfile.WriteString(sCreditCard,sCardNumber,Value.Values[0]);
      Value := AdaptExpirationDate.ActionValue;
      if Value.ValueCount > 0 then
        Logfile.WriteString(sCreditCard,sExpirationDate,Value.Values[0]);
      Value := AdaptUserName.ActionValue;
      if Value.ValueCount > 0 then
        Logfile.WriteString(sCreditCard,sUserName,Value.Values[0]);
    finally
      LogFile.UpdateFile;
      LogFile.Free
    end
  end;

OnGetValue
In de OnGetValue event handler van AdaptCreditCardNumber, AdaptExpirationDate en AdaptUserName kunnen we de adapter velden van een waarde voorzien (zonder dat zullen ze leeg zijn). De implementatie van eentje van hen is als volgt (de rest is hetzelfde). Merk op dat de sCreditCard en sCardNumber strings ook hier weer gebruikt worden - al is het maar om tikfouten te voorkomen.

  procedure THome.AdaptCreditCardNumberGetValue(Sender: TObject; var Value: Variant);
  var
    Logfile: TIniFile;
  begin
    LogFile := TIniFile.Create(IniFileName);
    try
      Value := Logfile.ReadString(sCreditCard,sCardNumber,'8888-8888-8888-8888')
    finally
      LogFile.Free
    end
  end;
Het resultaat hiervan is dat je de eerste keer de default waardes ziet (die je opgeeft bij het inlezen van de adapter velden uit de WebSnap.ini file), en daarna steeds de vorige waarden terugziet.

Meer Adapters
Naast de generieke TAdapter, die ik zojuist heb gedemonstreerd, bevat Delphi nog een aantal speciale Adapters. De TPagedAdapter heeft als extra de PageSize property, die aangeeft hoeveel elementen er per pagina moet worden weergegeven. Dit is bijvoorbeeld zinvol bij een zoekmachine, waarbij je maar tien resultaten per pagina wilt zien. Of een catalogus, waarbij je ook maar een beperkt aantal items per pagina wilt zien. Zodra de PageSize een getal groter dan 0, worden er boven en onder de output van de PagedAdapter drie links aangemaakt: Prev, Next en eentje met de nummers van de pagina's zelf (die kan nogal lang worden als je erg veel resultaten hebt en er maar een beperkt aantal op je pagina wilt laten zien, natuurlijk). De PageSize property is ook onderdeel van de TDataSetAdapter, dus is het makkelijker om het effekt te zien met deze laatste component. De TDataSetAdapter wordt gekoppeld aan een DataSet (table of query) en haalt zijn Adapter velden en akties direkt uit deze dataset. Erg fijn om op die manier snel van een tabel naar een interaktief HTML formulier te gaan.
Tot slot is de TLoginFormAdapter ook een echte speciale adapter die je kunt gebruiken om gebruikers in te laten loggen. Met UserName, Password en NextPage velden en de ActionLogin action is dit de meest specifieke Adapter tot nu toe.
En nu gaan we een TCreditCardAdapter schrijven, die lijkt op de TLoginFormAdapter, maar veel zal doen wat we hiervoor al hebben gedaan met de normale Adapter component.

TCreditCardAdapter
We beginnen met een TCustomCreditCardAdapter. Custom, omdat we in eerste instantie nog niet alle properties en events willen publiceren (dan kunnen andere ontwikkelaars er wellicht ook nog wat mee). De properties zijn nog nog public.
De declaratie van de TCustomCreditCardAdapter bestaat uit een aantal onderdelen. De drie Adapter Fields zelf zijn geen expliciete onderdelen van de class zelf (want de eindgebruiker kan ze toevoegen in de Fields Editor). Wel zullen we steeds drie methoden hebben voor het opvragen van de waarde, de validatie, en het eventueel geven van een foutmelding. Daarnaast zullen we ook een viertal interface methoden moeten implementeren om het toevoegen van adapter velden en akties toe te staan. Tot slot zal er voor de Adapter Action ook geen expliciete action in de class zijn opgenomen, maar moeten we wel al rekening houden met de event handler die komen gaat:

  type
    TCreditCardUseEvent = procedure(Sender: TObject;
      CardNumber, ExpirationDate, UserName: Variant) of object;
De definitie van de TCustomCreditCardAdapter zelf is als volgt (als voorouder gebruik ik de TDefaultFieldsAdapter omdat die default velden en akties kan toevoegen aan de adapter- iets wat ik inderdaad wil hebben in ons voorbeeld):
  type
    TCustomCreditCardAdapter = class(TDefaultFieldsAdapter)
    private // The event handler pointers
      FOnValidateCardNumber: TValidateAdapterFieldEvent;
      FOnValidateExpirationDate: TValidateAdapterFieldEvent;
      FOnValidateUserName: TValidateAdapterFieldEvent;
      FOnCreditCard: TCreditCardUseEvent;

    private // Get Adapter Field values
      function GetCardNumber: Variant;
      function GetExpirationDate: Variant;
      function GetUserName: Variant;

    protected // Validate Adapter values
      procedure ValidateCardNumber;
      procedure ValidateExpirationDate;
      procedure ValidateUserName;

    protected // Raise Adapter Field Errors
      procedure RaiseBlankCardNumber;
      procedure RaiseBlankExpirationDate;
      procedure RaiseBlankUserName;

    protected // Execute Adapter Action Event
      procedure ExecuteCreditCard(CardNumber, ExpirationDate, UserName: Variant);

    protected
      { IAdapterEditor }
      function ImplCanAddFieldClass(AParent: TComponent; AClass: TClass):
        Boolean; override;
      function ImplCanAddActionClass(AParent: TComponent; AClass: TClass):
        Boolean; override;
      { IWebDataFields }
      procedure ImplGetFieldsList(AList: TStrings); override;
      { IWebActionsList }
      procedure ImplGetActionsList(AList: TStrings); override;

    public
      property OnValidateCardNumber: TValidateAdapterFieldEvent
        read FOnValidateCardNumber write FOnValidateCardNumber;
      property OnValidateExpirationDate: TValidateAdapterFieldEvent
        read FOnValidateExpirationDate write FOnValidateExpirationDate;
      property OnValidateUserName: TValidateAdapterFieldEvent
        read FOnValidateUserName write FOnValidateUserName;
      property OnCreditCard: TCreditCardUseEvent
        read FOnCreditCard write FOnCreditCard;
      property CardNumber: Variant read GetCardNumber;
      property ExpirationDate: Variant read GetExpirationDate;
      property UserName: Variant read GetUserName;
    end;
Om maar met de vier interface methoden te beginnen: die bepalen welke Adapter velden en akties toegevoegd kunnen worden in de Fields Editor en Actions Editor van onze CustomCreditCardAdapter. Voor we aan de implementatie daarvan kunnen beginnen, zullen we eerst een definitie moeten geven van de generieke classes voor deze velden en akties. Die zijn als volgt:
  type
    TCreditCardAdapterField = class(TAdapterNamedDisplayField)
    protected
      function GetAdapter: TCustomCreditCardAdapter;
    public
      property Adapter: TCustomCreditCardAdapter read GetAdapter;
    end;

    TCreditCardAdapterAction = class(TCustomAdapterAction)
    private
      function GetAdapter: TCustomCreditCardAdapter;
    protected
      property Adapter: TCustomCreditCardAdapter read GetAdapter;
    end;
Dit is de meest generieke vorm van een Adapter Field en Action die we kunnen gebruiken in onze CustomCreditCardAdapter. Ze weten allebei bij welke Adapter ze horen (daar moeten we de GetAdapter funktie nog voor implementeren), en zijn de base classes voor de drie specifieke Adapter Fields en de specifieke Adapter Action die nog komen gaan.
Met de definitie voor TCreditCardAdapterField en TCreditCardAdapterFormAdapterAction hebben we in ieder geval vast voldoende voor de implementatie van ImplCanAddFieldClass voor het IAdapterEditor interface om te vragen of een class kan worden toegevoegd:
  function TCustomCreditCardAdapter.ImplCanAddFieldClass(AParent: TComponent;
    AClass: TClass): Boolean;
  begin
    Result := inherited ImplCanAddFieldClass(AParent, AClass) or
      AClass.InheritsFrom(TCreditCardAdapterField)
  end;

  function TCustomCreditCardAdapter.ImplCanAddActionClass(
    AParent: TComponent; AClass: TClass): Boolean;
  begin
    Result := inherited ImplCanAddActionClass(AParent, AClass) or
      AClass.InheritsFrom(TCreditCardAdapterAction)
  end;
Voor het daadwerkelijke toevoegen moeten we de ImplGetFieldsList van de IWebDataFields en de ImplGetACtionsList van de IWebActionsList interfaces implementeren. Beide methoden voegen de classtypes toe die mogelijk zijn. Maar die zullen we eerst zelf moeten definieren (en afleiden van resp. TCreditCardAdapterField en TCreditCardAdapterAction).
  type
    TCreditCardAdapterCardNumberField = class(TCreditCardAdapterField)
    protected
      function ImplGetValue: Variant; override;
    published
      property FieldName;
      property DisplayLabel;
      property DisplayWidth;
    end;
Voor TCreditCardAdapterExpirationDateField en TCreditCardAdapterUserNameField geldt een gelijke definitie (dus die schrijf ik niet op), en voor de TCreditCardAdapterCreditCardAction afgeleid van TCreditCardAdapterAction geldt de volgende definitie:
  type
    TCreditCardAdapterCreditCardAction = class(TCreditCardAdapterAction)
    protected
      procedure ImplExecuteActionRequest(AActionRequest: IActionRequest;
        AActionResponse: IActionResponse); override;
      function GetDefaultActionName: string; override;
    published
      property DisplayLabel;
      property OnBeforeExecute;
      property OnAfterExecute;
      property OnBeforeGetResponse;
      property OnAfterGetResponse;
    end;
Nu we deze class definities hebben gemaakt, kunnen we de TCustomCreditCardAdapter methoden afkomstig van de IWebDataFields en IWebACtionsList interfaces implementeren (om een totale lijst van alle mogelijke classes te maken):
  procedure TCustomCreditCardAdapter.ImplGetFieldsList(AList: TStrings);
  begin
    AList.Clear;
    AList.AddObject(sCreditCardNumber, TObject(TCreditCardAdapterCardNumberField));
    AList.AddObject(sCreditCardExpirationDate, TObject(TCreditCardAdapterExpirationDateField));
    AList.AddObject(sCreditCardUserName, TObject(TCreditCardAdapterUserNameField));
  end;

  procedure TCustomCreditCardAdapter.ImplGetActionsList(AList: TStrings);
  begin
    AList.Clear;
    AList.AddObject(sCreditCardAction, TObject(TCreditCardAdapterCreditCardAction))
  end;
In bovenstaande listing staan de namen van de drie Adapter velden en de Adapter aktie.

CreditCard Adapter Fields
Laten we even verder gaan met de implementatie van TCreditCardAdapterField en de drie afgeleide velden. Allereerst de GetAdapter method, die is geïmplementeerd als volgt:

  function TCreditCardAdapterField.GetAdapter: TCustomCreditCardAdapter;
  begin
    if (inherited Adapter <> nil) and
       (inherited Adapter is TCustomCreditCardAdapter) then
      Result := TCustomCreditCardAdapter(inherited Adapter)
    else
    begin
      Result := nil;
      Assert(False) // CreditCardAdapter not found
    end
  end;
De Assert(False) heb ik overgenomen van het voorbeeld van de TLoginFormAdapter. Dit is een manier om aan te geven (als je Assertions gebruikt bij het compileren) dat het veld bij een foute vader Adapter is aangemaakt. In praktijk zou dit niet eens voor mogen komen.
Naast de GetAdapter method, heeft ieder specifieke Adapter veld een eigen ImplGetValue methode, die in ons voorbeeld een lege waarde teruggeeft (maar waar je ook een default waarde voor bijvoorbeeld de expiration date of cardnumber in zou kunnen zetten):
  function TCreditCardAdapterCardNumberField.ImplGetValue: Variant;
  begin
    Result := ''
  end;
De implementatie voor de ImplGetValue van TCreditCardAdapterExpirationDateField en TCreditCardAdapterUserNameField is uiteraard gelijkvormig aan die voor de hier gegeven ImplGetValue van TCreditCardAdapterCardNumberField.

CreditCard Adapter Actions
Bij het kijken naar de CreditCard Adapter akties beginnen we ook hier met de GetAdapter method, die hetzelfde eruitziet als die van de CreditCard Adapter Field base class:

  function TCreditCardAdapterAction.GetAdapter: TCustomCreditCardAdapter;
  begin
    if (inherited Adapter <> nil) and
       (inherited Adapter is TCustomCreditCardAdapter) then
      Result := TCustomCreditCardAdapter(inherited Adapter)
    else
    begin
      Result := nil;
      Assert(False) // CreditCardAdapter not found
    end
  end;
Daarnaast heeft de echte TCreditCardAdapterCreditCardAction class twee methods, namelijk GetDefaultActionName (die de string sCreditCardAction teruggeeft - er is er bovendien maar één), en ImplExecuteActionRequest. De laatste voert de validatie van de drie Adapter velden uit en roept tevens de OnExecute event handler aan (als de programmeur daar nog een event handler aan heeft hangen).
  function TCreditCardAdapterCreditCardAction.GetDefaultActionName: string;
  begin
    Result := sCreditCardAction
  end;

  procedure TCreditCardAdapterCreditCardAction.ImplExecuteActionRequest(
    AActionRequest: IActionRequest; AActionResponse: IActionResponse);
  begin
    if Adapter <> nil then
    begin
      with Adapter do // TCustomCreditCardAdapter
      begin
        try
          UpdateRecords;
          ValidateCardNumber;
          ValidateExpirationDate;
          ValidateUserName;
          ExecuteCreditCard(CardNumber,ExpirationDate,UserName)
        except
          on E: Exception do
            Errors.AddError(E)
        end
      end
    end
  end;
Zoals je ziet wordt een mogelijke exception (die door de Validate routines moet worden gegenereerd) afgevangen en aan de Errors list toegevoegd. Helaas levert dit alleen maar de eerste fout op, en niet alle fouten. Om meteen alle mogelijke fouten op te leveren - een vraag van iemand tijdens de Conference to the Point van 13 december 2001 - zal ik het try-except blok in vieren moeten splitsen, als volgt:
  procedure TCreditCardAdapterCreditCardAction.ImplExecuteActionRequest(
    AActionRequest: IActionRequest; AActionResponse: IActionResponse);
  begin
    if Adapter <> nil then
    begin
      with Adapter do // TCustomCreditCardAdapter
      begin
        try
          UpdateRecords
        except
          on E: Exception do
            Errors.AddError(E)
        end;

        try
          ValidateCardNumber
        except
          on E: Exception do
            Errors.AddError(E)
        end;

        try
          ValidateExpirationDate
        except
          on E: Exception do
            Errors.AddError(E)
        end;

        try
          ValidateUserName
        except
          on E: Exception do
            Errors.AddError(E)
        end;

        if Errors.ErrorCount = 0 then
          ExecuteCreditCard(CardNumber,ExpirationDate,UserName)
      end
    end
  end;
De ExecuteCreditCard wordt nu alleen uitgevoerd als er geen errors zijn (als Errors.ErrorCount gelijk is aan nul). De Errors list kan tevens als input dienen voor een AdapterErrorList in een AdapterForm, zoals we eerder zagen.

TCustomCreditCardAdapter
Nu we alle velden en akties gehad hebben, kunnen we terugkeren naar de normale methoden van de TCustomCreditCardAdapter: voor het ophalen, valideren en melden van een fout. Het ophalen van een waarde, zoals die van de CardNumber adapter, gebeurt door het opzoeken van de waarde van het betreffende veld met FieldValues.ValueOfField.

  function TCustomCreditCardAdapter.GetCardNumber: Variant;
  var
    FieldValue: IActionFieldValue;
    FieldValues: IActionFieldValues;
  begin
    Result := Unassigned;
    if Supports(WebContext.AdapterRequest, IActionFieldValues, FieldValues) then
    begin
      FieldValue := FieldValues.ValueOfField(sCreditCardNumber);
      if FieldValue <> nil then
        Result := FieldValue.Values[0]
    end
  end;
Voor GetExpirationDate en GetUserName geldt uiteraard een vergelijkbare implementatie.

Validate
Het valideren van de waarde van een Adapter veld gebeurt op twee manieren. Allereerst wordt gekeken of de event handler (zoals OnValidateCreditCardNumber) is ingevuld door de WebSnap ontwikkelaar - de gebruiker van de TCustomCreditCardAdapter. Als dit het geval is, dan wordt deze event handler eerst aangeroepen. Daarbinnen kan de ontwikkelaar de parameter "Handled" op True zetten om aan te geven dat de verificatie afgelopen is - er zal dan geen verdere verificatie meer plaatsvinden.
Als Handled echter op False blijft staan, is het onze beurt, en volgt de code die verder nog in de ValidateCardNumber method staat (in dit geval niet zoveel zinvols, maar je zou je voor kunnen stellen dat je behalve de test voor een leeg nummer, ook test of het een geldig nummer is door daar wat van de bekende tests op uit te voeren).

  procedure TCustomCreditCardAdapter.ValidateCardNumber;
  var
    Handled: Boolean;
  begin
    Handled := False;
    if Assigned(OnValidateCardNumber) then
      OnValidateCardNumber(Self, CardNumber, Handled);
    if not Handled then
      if VarIsEmpty(CardNumber) or (Trim(string(CardNumber)) = '') then
        RaiseBlankCardNumber;
  end;
Als er een foutsituatie is opgetreden - de validate slaagt niet - dan kun wordt met de RaiseBlankCardNumber de fout doorgegeven. Deze laatste raised een exception, die in het try-except blok van de Adapter Action wordt afgevangen en toegevoegd aan de Error list (zoals we al eerder zagen).
  procedure TCustomCreditCardAdapter.RaiseBlankCardNumber;
  begin
    raise EAdapterFieldException.Create(sBlankCardNumber, sCreditCardNumber)
  end;

Execute CreditCard
Tot slot de methode van de TCustomCreditCard die de CreditCard dan daadwerkelijk gebruikt: de ExecuteCreditCard. Deze methode wordt aangeroepen door de Adapter Action, en zal eigenlijk alleen maar kijken of de event handler (de OnCreditCard) door de WebSnap ontwikkelaar is ingevuld, en indien aanwezig deze ook uitvoeren, waarbij alle drie de Adapter velden als argumenten worden meegegeven. Dit is de laatste kans - na de individuele verificatie - die de WebSnap ontwikkelaar heeft om nog een fout te melden. Maar meestal zal in de OnCreditCard de betaling zelf plaatsvinden, en hoef je alleen nog de terugkoppeling te doen.

  procedure TCustomCreditCardAdapter.ExecuteCreditCard(CardNumber,
    ExpirationDate, UserName: Variant);
  begin
    if Assigned(OnCreditCard) then
      OnCreditCard(Self, CardNumber, ExpirationDate, UserName)
  end;

TCreditCardAdapter
De TCreditCardAdapter component waar het om draait is eigenlijk vrij simpel. Hij is afgeleid van de TCustomCreditCardAdapter en publiceert een aantal properties. De Data property bevat de Adapter velden, de Actions property de Adapter akties. De rest hebben we al gezien.

  type
    TCreditCardAdapter = class(TCustomCreditCardAdapter)
    published
      property Data;
      property Actions;
      property OnValidateCardNumber;
      property OnValidateExpirationDate;
      property OnValidateUserName;
      property OnCreditCard;
      property OnBeforeExecuteAction;
      property OnAfterExecuteAction;
      property OnBeforeGetActionResponse;
      property OnAfterGetActionResponse;
    end;

Registratie
En last-but-not-least: de registratie van de TCreditCardAdapter met al zijn velden en akties. Omdat dit niet zomaar componenten zijn, kunnen we ook niet zomaar een Register routine met een aanroep naar RegisterComponents gebruiken. In plaats daarvan moeten we eerst RegisterWebComponents gebruiken voor alle Adapter velden en akties, en daarna pas de RegisterComponents voor de TCreditCardAdapter op de DrBob42 (of de WebSnap) tab van het Component Palette.

  procedure Register;
  begin
    RegisterWebComponents([TCreditCardAdapterField]);
    RegisterWebComponents([TCreditCardAdapterCardNumberField]);
    RegisterWebComponents([TCreditCardAdapterExpirationDateField]);
    RegisterWebComponents([TCreditCardAdapterUserNameField]);
    RegisterWebComponents([TCreditCardAdapterAction]);
    RegisterWebComponents([TCreditCardAdapterCreditCardAction]);
  //RegisterWebComponents(TCustomCreditCardAdapter);
    RegisterComponents('DrBob42',[TCreditCardAdapter]);
  end;
Een eigen icon maken laat ik over aan wie daar zin in heeft. De installatie gaat eenvoudig: doe Component | Install Component en instaleer de unit met de TCreditCardAdapter in de Delphi User Components package (tenzij je er een eigen package voor wilt maken natuurlijk).

Gebruik
Na installatie is de TCreditCardAdapter terug te vinden op het Component Palette van Delphi of Kylix, en kunnen we de component gebruiken op onze Page Module (zie Figuur 7):

figuur 7. creditcardadapter

Via de rechtermuisknop op de CreditCardAdapter component kunnen we de Fields Editor, en straks ook de Actions Editor selecteren. In beide hebben we de mogelijkheid tot "Add All Fields" (en Add All Actions). Dit levert de voorgedefinieerde velden en akties op, die we in de eerdergeschreven ImplGetFieldsList en ImplGetActionsList hebben aangegeven. Dat was de hoofdreden dat de TCustomCreditCardAdapter is afgeleid van de TDefaultFieldsAdapter. Verder hoeven we niks meer te doen. In tegenstelling tot de normale adapter waar we de drie velden stuk voor stuk moesten aanmaken en nog code moesten schrijven, kunnen we ons hier beperken tot het gebruiken van de voorgebakken funktionaliteit. Je kan natuurlijk wel weer de DisplayLabel properties aanpassen aan je wensen.
Na dubbelklikken op de AdapterPageProducer komen we weer in de Web Page Editor, alwaar we een AdapterForm kunnen toevoegen met een AdapterErrorList, een AdapterFieldGroup, en een AdapterCommandGroup. Los de drie warnings op zoals ik eerder deed (gebruik de CreditCardAdapter als Adapter property, en de AdapterFieldGroup als DisplayComponent).

figuur 8. creditcardadapter in web page editor

Je ziet, het ziet er hetzelfde uit. En als we dit in praktijk testen, dan zul je ook de drie exceptions zien in de errorlist (tussen de button en de fieldgroup), om aan te geven dat je bijvoorbeeld het CardNumber, de ExpirationDate of de UserName leef hebt gelaten.

Conclusie
WebSnap is een nieuwe architectuur voor Delphi, Kylix (en inmiddels ook C++Builder) waarin de Adapter componenten een verbinding zorgen tussen de data van de web server toepassing en de presentatie (vaak in HTML) naar de eindgebruiker. In dit artikel heb ik laten zien wat adapters zijn, hoe ze werken, en hoe je er zelf eentje kunt bouwen. Dit is maar het begin natuurlijk, want iedereen kan zelf nieuwe adapters bedenken en bouwen, alhoewel het wel echt nuttig en herbruikbaar moet zijn wil je daadwerkelijk een nieuwe adapter component gaan bouwen (anders is het waarschijnlijk toch sneller om gewoon met een standaard TAdapter te beginnen en zelf de velden en akties toe te voegen).
In dat kader is het aardig om te melden dat ik inmiddels een WebSnap Custom Adapter Wizard heb gemaakt die gebruikt kan worden om zelf snel Custom Adapters te genereren!
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


Dit artikel is eerder verschenen in SDGN Magazine #71 - april 2002

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