Page 1 of 2 1 2 LastLast
Results 1 to 15 of 16

Thread: Communicatie tussen forms

  1. #1

    Communicatie tussen forms

    Communicatie tussen forms
    Door Sam Witse
    • De user drukt op een knop "Logging Aan" in een form waar hij/zij allerlei instellingen kan veranderen, en de Mainform wenst hiermee iets te doen.
    • Ik open een tweede form waarin de user zijn/haar naam invult, en na het sluiten wil ik met die naam verder werken in mijn Mainform
    • Ik wens in een dialoogform te vragen of de user zeker is van zijn/haar actie, in de stijl "Are you sure to blow up your computer?". Afhankelijk of de user Yes of No klikt, gebeurt er iets in de Mainform.
    • De waarde van het veld "Achtergrondkleur" verandert op een form met instellingen, waardoor alle andere forms hun achtergrondkleur verandert.


    Hoe maken we dit?
    Zulke problemen vormen dikwijls een onderdeel van je applicatie. Hier volgt een mooi overzicht van de technieken die je kunt gebruiken om te communiceren tussen verschillende forms. Van heel eenvoudige oplossingen tot de meer geavanceerde oplossingen, maar allemaal goed begrijpbaar voor elke Delphi-liefhebber.

    Inleiding

    Een veel voorkomend probleem: ik wens informatie uit een ander form halen. Hoe doe ik dat het best? Ik ga een aantal technieken uitleggen aan de hand van het volgend eenvoudig programmaatje, dat een kaartspelletje bevat. Het spelletje op zich is niet belangrijk en ook niet uitgewerkt, het belangrijkste is de communicatie tussen de verschillende forms. Concrete voorbeelden:

    1. Ik open een form waarin de user de vraag gesteld wordt of hij/zij zeker is om het spelletje te stoppen, waarop hij/zij ja of neen moet antwoorden.
    2. Ik open een form waarin de user een keuze uit 4 ‘kaartkleuren’ maakt, die verder in de Mainform gebruikt wordt.
    3. Ik open een form waarin de user een keuze uit 4 ‘kaartkleuren’ maakt, en zijn/haar naam ingeeft; ik sluit de form met een OK of Cancel button.
    4. Ik open een form waarin de user een keuze uit 4 ‘kaartkleuren’ maakt, en zijn/haar naam ingeeft, die onmiddellijk in de Mainform gebruikt worden, zonder mijn form te moeten sluiten.
    5. Ik open een form waarin de user een keuze uit 4 ‘kaartkleuren’ maakt, die onmiddellijk in verschillende forms gebruikt worden, zonder mijn form te moeten sluiten.


    Inhoud
    1. MessageDialog
    2. Eigen waarden teruggeven.
    3. Form met resulaten als var-parameter.
    4. Form met events naar Mainform.
    5. Observer pattern: Form met events naar Observers.

    1. MessageDialog
    De eenvoudigste oplossing is gebruik te maken van de standaardfunctie MessageDlg. Deze toont je een boodschap, een icoontje, en een aantal buttons.
    In je MainForm maak je een button “Stop”, met in het OnClick-event de volgende code:

    Delphi Code:
    1. procedure TMainForm.StopButtonClick(Sender: TObject);
    2. begin
    3.   if MessageDlg ('Ben je zeker om het spelletje te stoppen?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    4.     Close {De Mainform, en dus ook de applicatie sluiten}
    5. end;
    Voer het programma uit. Klik op de Stop-button. Er wordt een standaard-dialoog getoond met de boodschap “Ben je zeker om het spelletje te stoppen?” en een icoontje met een vraagteken. Verder zijn er twee buttons, één met Yes, en één met No.

    Name:  Oplossing1-1.jpg
Views: 1341
Size:  10.6 KB
    Klik je op Yes, dan wordt de dialoog gesloten, en krijgt je Mainform de waarde mrYes terug als resultaat van de functie-oproep MessageDlg. Je komt dus in het IF-gedeelte van de test, en de Mainform wordt gesloten. Had je op No geklikt, dan kwam je niet in het IF-gedeelte, en vermits er geen ELSE-gedeelte is, gebeurt er niets.

    Je kunt ook je een eigen form maken, deze Modal oproepen, en een result teruggeven. Maak hiervoor een Form2 zet er een label op met de volgende caption: "Ben je zeker om het spelletje te stoppen?" Plaats er ook twee buttons op. De eerste geef je als caption "Heel zeker", en de tweede "Hmm, toch niet". In de OnClick-event van de eerste button schrijf je:

    Delphi Code:
    1. procedure TForm2.Button1Click(Sender: TObject);
    2. begin
    3.   ModalResult := mrYes
    4. end;
    In de Onclick-event van de tweede button schrijf je:
    Delphi Code:
    1. procedure TForm2.Button2Click(Sender: TObject);
    2. begin
    3.   ModalResult := mrNo
    4. end;
    Deze tweede form bewaar je in het bestand Form2Unit.pas.
    Deze tweede form roep je nu als volgt op in de OnClick-event van je Stop-button:
    Delphi Code:
    1. procedure TMainForm.StopButtonClick(Sender: TObject);
    2. begin
    3.  if Form2.ShowModal = mrYes then
    4.    Close {De Mainform, en dus ook de applicatie sluiten}
    5. end;

    Je moet nu nog enkel aangeven dat je MainForm deze Form2 'kent'. Dit doe je door bovenaan in je implementation
    Delphi Code:
    1. uses Form2Unit ;
    te schrijven.
    Test dit even uit, en je merkt dat het werkt zoals je zou verwachten.
    Click image for larger version. 

Name:	Oplossing1-2.jpg 
Views:	299 
Size:	15.9 KB 
ID:	4809
    Wat gebeurt er nu? Je mainform roept een functie op: ShowModal. Deze toont form2, en wacht tot iets of iemand hem sluit. In Form2 geef je ModalResult een waarde (mrYes of mrNo). Hiermee sluit je ook de form, en geef je deze ModalResult terug als 'resultaat' van ShowModal.
    Terug in je Mainform, is ShowModal dus gedaan, en heeft die een waarde teruggegeven. Afhankelijk of de waarde die je terugkrijgt mrYes is of iets anders, kom je in het IF-gedeelte, of niet.
    Je eigen dialoogform kun je aanpassen naar je eigen wensen. Geef het een kleur, zet er een image op enz. Ju kunt ook vanuit je Mainform eerst de tekst aanpassen alvorens je dialoog op te roepen. Bijvoorbeeld:
    Delphi Code:
    1. procedure TMainForm.StopButtonClick(Sender: TObject);
    2. begin
    3.    Form2.Label1.Caption := 'Wil je nu al stoppen?' ;
    4.    if Form2.ShowModal = mrYes then
    5.      Close; {De Mainform, en dus ook de applicatie sluiten}
    6. end;
    Dan krijg je:
    Click image for larger version. 

Name:	Oplossing1-3.jpg 
Views:	300 
Size:	14.6 KB 
ID:	4810
    Last edited by GolezTrol; 30-Jan-10 at 10:53.

  2. #2
    2. Eigen waarden teruggeven.

    Met een Modal form kun je een integer terug krijgen. In principe zijn dit waarden als mrYes, mrNo, en zo meer. Zie MessageDlg in de help voor alle standaardwaarden die je zo terug kunt krijgen.
    Stel nu dat je geen mrYes, mrNo, etc wil terugkrijgen, maar een andere waarde. Bijvoorbeeld een button 'Wijzig kleur' moet een Form2 oproepen met daarop 4 buttons: Harten, Ruiten, Klaveren, Schoppen. Deze form moet 'modal' opgeroepen worden (wat wil zeggen dat je niet op je mainform verder kunt vooraleer je Form2 hebt afgesloten). We willen nu dat het resultaat van de oproep van ShowModal van Form2 één van die vier kleuren teruggeeft.
    We kunnen de gewone ShowModal hiervoor niet gebruiken, maar moeten hiervoor een 'eigen' versie van ShowModal maken. Laten we onze eigen versie 'VraagKleur' noemen. We definiëren het type TKaartKleur in het interface-gedeelte van de unit:
    Delphi Code:
    1. type
    2.   TKaartKleur = (kkHarten, kkRuiten, kkKlaveren, kkSchoppen) ;
    3. const
    4.   KaartKleurString: array[TKaartKleur] of string =
    5.     ('Rode Harten','Rode Ruiten','Zwarte Klaveren','Zwarte Schoppen') ;
    We definiëren dit type in Form2Unit omdat dit type zowel in onze MainForm als in Form2 gekend moet zijn. Met de constante array KaartKleurString kunnen we een enumerated type eenvoudig omvomen naar een string.
    In Form2 definiëren we nu de public function VraagKleur. Deze functie moet public zijn omdat men ze moet kunnen oproepen van buiten de unit.
    Om de geslecteerde kleur die de user kiest, even bij te houden, declareren we ook een private variabele 'GeselecteerdeKleur'.
    Delphi Code:
    1. private
    2.     { Private declarations }
    3.     GeselecteerdeKleur : TKaartKleur ;
    4.   public
    5.     { Public declarations }
    6.     function VraagKleur: TKaartKleur ;
    De implementatie van de functie VraagKleur is misschien een beetje onverwacht. We gaan de form zichzelf modal tonen. Daar waar je ShowModal tot nogtoe altijd opriep om een ander form te tonen, gebruiken we het hier om zichzelf te tonen.
    Delphi Code:
    1. function TForm2.VraagKleur: TKaartKleur;
    2. begin
    3.   ShowModal; {zichzelf modal tonen, en blijven tonen totdat het form gesloten wordt}
    4.   Result := GeselecteerdeKleur {Dit statement wordt pas uitgevoerd als de form gesloten wordt}
    5. end;
    GeselecteerdeKleur is een private variabele van Form2, en krijgt een waarde als je op een button klikt. De buttons sluiten ook Form2, zodat de functie VraagKleur verder afgewerkt kan worden:
    Delphi Code:
    1. procedure TForm2.HartenButtonClick(Sender: TObject);
    2. begin
    3.   GeselecteerdeKleur := kkHarten ;
    4.   Close {ÔǪen vervolg in VraagKleur}
    5. end;
    Dit doe je eveneens voor de drie andere buttons op Form2.
    Delphi Code:
    1. procedure TForm2.RuitenButtonClick(Sender: TObject);
    2. begin
    3.   GeselecteerdeKleur := kkRuiten ;
    4.   Close {ÔǪen vervolg in VraagKleur}
    5. end;
    6.  
    7. procedure TForm2.SchoppenButtonClick(Sender: TObject);
    8. begin
    9.   GeselecteerdeKleur := kkSchoppen ;
    10.   Close {ÔǪen vervolg in VraagKleur}
    11. end;
    12.  
    13. procedure TForm2.KlaverenButtonClick(Sender: TObject);
    14. begin
    15.   GeselecteerdeKleur := kkKlaveren ;
    16.   Close {ÔǪen vervolg in VraagKleur}
    17. end;
    Het oproepen van VraagKleur zetten we gewoon in de OnClick van de WijzigKleurButton op je Mainform.
    Delphi Code:
    1. procedure TMainform.WijzigButtonClick(Sender: TObject);
    2. var
    3.   GekozenKleur: TKaartKleur;
    4. begin
    5.   GekozenKleur := Form2.VraagKleur ; {in een dialoog de kaartkleur vragen}
    6.   Label2.Caption := KaartKleurString [GekozenKleur] ; {Als string tonen in een label}
    7. end;
    Compileren en testen geeft het volgende resultaat:
    Click image for larger version. 

Name:	Oplossing2-1.jpg 
Views:	283 
Size:	16.5 KB 
ID:	4813
    De button 'Wijzig Kleur' laat Form 2 tonen. Op één van de 4 buttons klikken, geeft een waarde terug naar de Mainform, die waarde wordt omgezet in een string, en getoond als label2.
    Last edited by GolezTrol; 30-Jan-10 at 10:59.

  3. #3
    3. Form met resulaten als var-parameter.

    Een veel voorkomende situatie is deze waarbij je meerdere instellingen in een modal form wil aanpassen. Bijvoorbeeld, je wil in een dialoog de kaartkleur en de spelersnaam vragen. Wel, we maken gewoon een ShowModal-variant, die naast een result mrOK of mrCancel, ook een record teruggeeft met alle instellingen die de user gekozen heeft.
    Allereerst definiëren we het recordtype voor de instellingen:
    Delphi Code:
    1. type
    2.   Tinstellingen = Record
    3.     KaartKleur: TKaartKleur ;
    4.     SpelersNaam: string
    5.   end;
    Dit type definiëren we in het implementatie-geldeelte van de Form2Unit.
    Vergeet natuurlijk niet de definitie van TKaartKleur en de declaratie van de constante KaartKleurstring zoals in het vorig hoofdstuk.
    Nu definiëren we onze variante op ShowMessage in het public-gedeelte van TForm2:
    Delphi Code:
    1. function VraagInstellingen ( var Instellingen: Tinstellingen ): integer ;
    Op het Form2 zetten we de nodige controls opdat de user de instellingen kan aanpassen. Een groep radiobuttons en een editveld zijn prima geschikt hiervoor.
    De implementatie van VraagInstellingen houden we heel simpel: we vullen de controls op met de waarden uit de parameter en openen de form modal. Let op de volgorde: eerst de waarden op het scherm zetten, en dan pas modal openen, want eenmaal showmodal wordt opgeroepen, wordt de functie niet verder uitgevoerd totdat het form gesloten wordt.
    Delphi Code:
    1. function TForm2.VraagInstellingen(var Instellingen: Tinstellingen): integer;
    2. begin
    3.   KaartKleurRadio.ItemIndex := ord (Instellingen.KaartKleur) ; {De juiste radiobutton checked zetten}
    4.   SpelersNaamEdit.Text := Instellingen.SpelersNaam ;
    5.   ShowModal ;
    6.   if ModalResult = mrOK then
    7.     begin
    8.       Instellingen.KaartKleur := TKaartKleur (KaartKleurRadio.ItemIndex) ;
    9.       Instellingen.SpelersNaam := SpelersNaamEdit.Text
    10.     end ;
    11.   Result := ModalResult ; {De ModalResult moet als resultaat van deze functie (VraagInstellingen) teruggeven worden}
    12. end;
    De laatste statements zou je gemakkelijk zou vergeten. Als je op Form2 ModalResult een waarde geeft, dan wordt de form gesloten, en eindigt dus je dialoog. Je MainForm zal echter niet weten waarom de form gesloten is, want die verwacht het antwoord als resultaat van de oproep van VraagInstellingen (). Mocht je de form gewoon sluiten, dan heb je geen informatie teruggestuurd naar je Mainform. Dus, na de ShowModal, nog even snel het resultaat teruggeven aan je Mainform, door de var-parameter op te vullen, en het resultaat van de functie VraagInstellingen op te vullen met ModalResult.

    Plaats nog twee buttons voor Ok en Cancel op het form. De OKButton en de CancelButton moeten enkel de form sluiten, en mrOk of mrCancel teruggeven. Hiervoor kun je zorgen door de property ModalResult van beide juist te zetten.
    Click image for larger version. 

Name:	Oplossing3-1.jpg 
Views:	268 
Size:	36.3 KB 
ID:	4814
    De oproep van je instellingen-dialoog gebeurt in je Mainform op de volgende manier:
    Delphi Code:
    1. procedure TMainform.InstellingenButtonClick(Sender: TObject);
    2. begin
    3.   if Form2.VraagInstellingen ( MijnInstellingen )=mrOk then
    4.   begin
    5.     Label2.Caption := KaartKleurString [MijnInstellingen .KaartKleur] ;
    6.     Label4.Caption := MijnInstellingen.SpelersNaam
    7.   end
    8. end;
    Merk op dat je natuurlijk MijnInstellingen in je mainform moet definiëren, en liefst opvullen bij de creatie van de Mainform:
    Delphi Code:
    1. procedure TMainform.FormCreate(Sender: TObject);
    2. begin
    3.    MijnInstellingen.KaartKleur := kkRuiten ;
    4.    MijnInstellingen.SpelersNaam := ''
    5. end;
    Het resultaat is een Form2 waarin de vragen die door de user beantwoord worden, 'doorgestuurd' worden naar het Mainform. Deze maakt er verder gebruik van om ermee te doen wat je je maar kunt verbeelden.
    Click image for larger version. 

Name:	Oplossing3-2.jpg 
Views:	263 
Size:	23.4 KB 
ID:	4815
    Last edited by GolezTrol; 30-Jan-10 at 11:00.

  4. #4
    4. Form met events naar Mainform.

    Als je je Mainform wil aanpassen met gegevens die je in een ander form wijzigt, dan moet je je Mainform hiervan verwittigen. Herlees de vorige zin, het klinkt héél logisch: "Als je je Mainform wil aanpassen met gegevens die je in een ander form wijzigt, dan moet je je Mainform hiervan verwittigen". Toch laat iedereen zich verrassen, en duurt het dikwijls een tijdje vooraleer je je probleem kunt omvormen naar een dergelijke zin.
    Ik heb bijvoorbeeld een InstellingenForm waarop enkel radiobuttons staan die mij de keuze laten maken uit de verschillende kaartkleuren. Van het ogenblik dat ik een andere kaartkleur selecteer, moet dit onmiddellijk getoond worden op mijn Mainform. De Mainform moet dus verwittigd worden van de selectie van één van de radiobuttons.
    Dit is heel eenvoudig te verwezenlijken als die radiobuttons op je Mainform staan, maar dikwijls is het handig als dergelijke instellingen op een ander form staan.

    Ik herhaal even de eerste zin van dit hoofdstuk: "Als je je Mainform wil aanpassen met gegevens die je in een ander form wijzigt, dan moet je je Mainform hiervan verwittigen." Hiervoor gebruik je een 'event'. InstellingenForm verwittigt dat er iets gewijzigd is, en dat mogelijks voor de owner die InstellingenForm gebruikt, van nut is.
    De eenvoudigste vorm van een event is het TNotifyEvent. Hiermee verwittig je dat er iets gebeurd is. Bijvoorbeeld, "DeUserHeeftOpDeKlaverenButtonGedrukt" zou een NotifyEvent kunnen zijn. Het feit dat dit event gebeurt, zegt al voldoende: "de user heeft op de 'Klaveren' button gedrukt". Soms wil je echter meer: "DeUserHeeftDeKaartKleurGewijzigd" is ook een event, maar hier wil je ook graag weten wélke kaartkleur de user dan wel gekozen heeft. Hier maken we een voorbeeld van zo'n eigen event:
    Voor een nieuw soort event (lees: een event met andere parameters) moet je een event definiëren als type:
    Delphi Code:
    1. type
    2.   TKaartKleur = (kkHarten, kkRuiten, kkKlaveren, kkSchoppen) ;
    3.   TKaartKleurEvent = procedure ( NieuweKleur: TKaartKleur ) of object ;
    Let op de speciale syntax. Je zegt dat TKaartKleurEvent een procedure is.
    Nu kun je zeggen dat InstellingenForm een dergelijk event weet uit te voeren. Hiervoor maak je een 'gewone' property in je InstellingenForm klasse:
    Delphi Code:
    1. type
    2.   TInstellingenForm = class(TForm)
    3.   private
    4.     { Private declarations }
    5.     DoKaartKleurGewijzigd : TKaartKleurEvent ;
    6.   public
    7.     { Public declarations }
    8.     property OnKaartKleurGewijzigd: TKaartKleurEvent read DoKaartKleurGewijzigd write DoKaartKleurGewijzigd ;
    9.   end;
    Als je het gebruik van properties kent, dan is hier in in wezen niets moeilijks mee: een private variabele en een public property met read- en write-mogelijkheden. Om duidelijk te maken dat het om een event gaat, laten we de variabele beginnen met 'Do' in plaats van 'F' (Dit is geen verplichting, maar maakt het soms gemakkelijker leesbaar). Maar verder is er niets speciaals.

    Telkens je vanuit de InstellingenForm aan de buitenwereld willen laten weten dat er een kleur gewijzigd is (= 'verwittig Mainform' uit de eerste zin van dit hoofdstuk) voer je DoKaartKleurGewijzigd uit, op voorwaarde dat daar iets zinnigs in staat. De controle of er iets zinnigs in de property staat, doe je met Assigned().
    In ons voorbeeld gaan we dus telkens een radiobutton gechecked wordt, de DoKaartKleurGewijzigd uitvoeren:
    Delphi Code:
    1. procedure TInstellingenForm.KaartKleurRadioClick(Sender: TObject);
    2. begin
    3.   if assigned (DoKaartKleurGewijzigd) then {indien er iets in het event staat}
    4.     DoKaartKleurGewijzigd ( TKaartKleur(KaartKleurRadio.ItemIndex) ) {voer dit event uit}
    5. end;
    Merk op dat InstellingenForm niet weet wat er in het DoKaartKleurGewijzigd staat. Als er iets in staat, voer het uit; dat is alles wat we in InstellingenForm doen.

    Je Mainform wil reageren als InstellingenForm een event signaleert. De reactie van de Mainform zetten we in een private procedure, die net dezelfde vorm heeft als het event van InstellingenForm. We declareren dus
    Delphi Code:
    1. TMainform = class(TForm)
    2.   private
    3.       { Private declarations }
    4.     procedure ToonNieuweKleur ( NieuweKleur: TKaartKleur ) ;
    5.      ...enz.
    En in het implementatiegedeelte zeggen we wat er zoal gebeurt in de Mainform. Hier in ons voorbeeld tonen we de nieuwe kaartkleur gewoon in een label:
    Delphi Code:
    1. procedure TMainform.ToonNieuweKleur(NieuweKleur: TKaartKleur);
    2. begin
    3.   Label2.Caption := KaartKleurString[NieuweKleur]
    4. end;
    Nu moeten we nog onze procedure ToonNieuweKleur toekennen aan het event OnKaartKleurGewijzigd van de InstellingenForm. Vermits OnKaartKleurGewijzigd een property is, kun je je procedure daar gewoon aan toekennen met ons aller gekend :=
    Delphi Code:
    1. InstellingenForm.OnKaartKleurGewijzigd := ToonNieuweKleur;
    Nog één probleem: w?á?ár zet je die toekenningsregel?
    Je moet deze toekenning éénmaal uitvoeren, en v????r je de InstellingenForm oproept. Wel, wij doen het hier in de button die de InstellingenForm toont.
    Delphi Code:
    1. procedure TMainform.InstellingenButtonClick(Sender: TObject);
    2. begin
    3.   InstellingenForm.OnKaartKleurGewijzigd := ToonNieuweKleur;
    4.   InstellingenForm.Show
    5. end;
    Merk op dat je InstellingenForm.Show uitvoert, en niet ShowModal. Je wenst namelijk onmiddellijk verder te werken in je Mainform, zonder eerst InstellingenForm te moeten sluiten.

    Nu nog even compileren en testen: de kaartkleur die je in InstellingenForm kiest, wordt onmiddellijkgetoond in je Mainform!
    Click image for larger version. 

Name:	Oplossing4-1.jpg 
Views:	265 
Size:	18.8 KB 
ID:	4816
    Last edited by GolezTrol; 30-Jan-10 at 11:00.

  5. #5
    5. Form met events naar Observers.

    In hoofdstuk 4 hebben we gezien hoe je met een event een andere form kunt verwittigen dat er instellingen gewijzigd worden. Maar wat als er twee of meer forms 'interesse hebben' in het feit dat een instelling gewijzigd wordt? Bijvoorbeeld, je Mainform wil graag de label aanpassen, en een form die spelsituaties logt in een memo, wil dat ook graag weten! Je kunt maar één procedure aan het OnKaartKleurGewijzigd-event koppelen. Dus als twee forms elk een procedure aan dit event willen koppelen, dan heb je een probleem!
    De oplossing ligt voor de hand: zorg dat je een lijst van procedures kunt koppelen aan één event, en voer die procedures één na één uit, telkens het event getriggerd wordt. Deze oplossing heeft ook een naam: het observer-patroon. De uitwerking is wat complexer dan 'een eenvoudig lijstje van procedures', maar als je het stap voor stap uitvoert, geraak je er zeker wel! Dit zijn de 6 stappen:
    1. Definieer een event-type.
    2. Maak een observerklasse, die gebruikt gaat worden in elke form die geïnformeerd wil worden.
    3. Maak een subjectklasse, die de rol van event-dispatcher op zich neemt.
    4. Pas InstellingenForm aan door een fSubject te gebruiken.
    5. In elke form die geïnformeerd wil worden voeg je een fObserver toe.
    6. In de Mainform link je de observers aan het subject van de InstellingenForm.


    Om het effect duidelijk te zien in een voorbeeld, maken we eerst een nieuwe form, LogForm genaamd, met een Tmemo, die elke kleurwijziging gaat registreren. Zo zijn dus de Mainform en de LogForm geïnteresseerd in kleurwijzigingen.
    Name:  Oplossing5-1.jpg
Views: 1271
Size:  15.1 KB
    Stap 1: Definieer een event-type.
    De definitie van een event die aanduidt dat er een kaartkleur veranderd is, hebben we reeds in hoofdstuk 4 gezien. Deze definitie kunnen we hier gewoon overnemen:
    Delphi Code:
    1. type
    2.   TKaartKleur = (kkHarten, kkRuiten, kkKlaveren, kkSchoppen) ;
    3.   TKaartKleurEvent = procedure ( NieuweKleur: TKaartKleur ) of object ;
    Dit was het voor stap 1. Tot hier niks nieuws, niks moeilijks.

    Stap 2: De observerklasse.
    De observerklasse is een klasse die 'observeert'.Hij vormt een onderdeel van de Form waaraan hij toegevoegd is, en voert de code uit die hem toegekend is. Eigenlijk speelt de observer de rol van de code in het notify-event, zoals we in voorbeeld 4 gezien hebben.
    Hieronder volgt de volledige code van de observer. Merk op dt we er alle definities van Kaartkleuren aan hebben toegevoegd. Verder bevat een TKaartKleurObserver welgeteld één event (fOnUpdate), en één public procedure: Update. Zoals te verwachten, dient de property als plek om de event-code in op te slaan, en wordt het event uitgevoerd met de procedure Update.
    Niet moeilijk, maar op dit ogenblik wellicht een beetje eigenaardig. Hoe deze observer percies in werking treedt, zie je later.
    Delphi Code:
    1. unit ObserverUnit;
    2.  
    3. interface
    4. type
    5.   TKaartKleur = (kkHarten, kkRuiten, kkKlaveren, kkSchoppen) ;
    6. const
    7.   KaartKleurString: array[TKaartKleur] of string =
    8.     ('Rode Harten','Rode Ruiten','Zwarte Klaveren','Zwarte Schoppen') ;
    9. type
    10.   TKaartKleurEvent = procedure (NieuweKleur: TKaartKleur) of object ;
    11.   TKaartKleurObserver = class
    12.   private
    13.     fOnUpdate: TKaartKleurEvent ;
    14.   public
    15.     property OnUpdate: TKaartKleurEvent read fOnUpdate write fOnUpdate;
    16.     procedure Update (NieuweKleur: TKaartKleur);
    17.   end;
    18. implementation
    19.  
    20. { TKaartKleurObserver }
    21.  
    22. procedure TKaartKleurObserver.Update(NieuweKleur: TKaartKleur);
    23. begin
    24.   if Assigned(FOnUpdate) then
    25.     FOnUpdate(NieuweKleur)
    26. end;
    27.  
    28. end.

    Stap 3: De subjectklasse.
    De subjectklasse zorgt ervoor dat we de events in een lijstje bijhouden, en telkens het event plaatsvindt, er niet één maar een hele reeks procedures uitgevoerd worden.
    Niet te verwonderen dus dat de kern van een subjectklasse een TObjectList is.
    Eigenlijk is het nog iets leuker: er staan niet gewoon procedures in het lijstje van een subjectklasse, maar observers!
    Hier zien we de code voor de subjectklasse:
    Delphi Code:
    1. unit SubjectUnit;
    2.  
    3. interface
    4. uses Contnrs {voor TObjectList}, ObserverUnit;
    5.  
    6. type
    7.   TKaartKleurSubject = class
    8.   private
    9.     fObservers: TObjectList;
    10.   public
    11.     constructor Create ;
    12.     destructor Destroy; override;
    13.     procedure Attach (Observer: TKaartKleurObserver);
    14.     procedure Detach (Observer: TKaartKleurObserver);
    15.     procedure Notify (KaartKleur: TKaartKleur);
    16.   end;
    17.  
    18. implementation
    19.  
    20. { TSubject }
    21.  
    22. procedure TKaartKleurSubject.Attach(Observer: TKaartKleurObserver);
    23. begin
    24.   fObservers.Add (Observer)
    25. end;
    26.  
    27. constructor TKaartKleurSubject.Create;
    28. begin
    29.   fObservers := TObjectList.Create (false)
    30. end;
    31.  
    32. destructor TKaartKleurSubject.Destroy;
    33. begin
    34.   fObservers.Free;
    35.   inherited;
    36. end;
    37.  
    38. procedure TKaartKleurSubject.Detach(Observer: TKaartKleurObserver);
    39. begin
    40.   fObservers.Remove (Observer)
    41. end;
    42.  
    43. procedure TKaartKleurSubject.Notify (KaartKleur: TKaartKleur);
    44. var
    45.   i: integer ;
    46. begin
    47.   for i:=0 to fObservers.Count - 1 do
    48.     TKaartKleurObserver(fObservers[i]).Update(KaartKleur)
    49. end;
    50.  
    51. end.
    We hebben hierin allereerst fObservers, het lijstje met observers. Deze moet in een constructor gecreate worden, en in een destructor vernietigd worden.
    De Attach en de Detach zorgen ervoor dat een observer zich kan melden als 'geïnteresseerd in het event'. Meer nog, de obeserver zal uitgevoerd worden als het event plaatsvindt. Merk op dat de Detach in ons voorbeeld niet gebruikt wordt, en er enkel bij staat ter volledigheid: hiermee zou je in de loop van je programma observers kunnen weghalen uit de lijst met uit te voeren observers, als je dat wil.
    Dan blijft er enkel de procedure Notify uit te leggen. Nu, dat is, zoals ook te verwachten was, in een lusje alle 'aangesloten' observers één na één uitvoeren.

    Nu moeten we de observers en de subject nog juist weten te gebruiken. Dat doen we in stappen 4, 5 en 6.

    Stap 4: Subject inbouwen.
    In de Instellingenform bouwen de een subject in. En daar waar je normaal een event oproept (zie voorbeeld 4), roep je nu 'fSubject.Notify' op.
    Concreet: declareer een private fSubject, en maak er een public property van.
    Delphi Code:
    1. private
    2.     { Private declarations }
    3.     fSubject: TKaartKleurSubject ;
    4.   public
    5.     { Public declarations }
    6.     property Subject: TKaartKleurSubject read fSubject;
    Deze fSubject moet je nog creëren bij de creatie van de form, en vernietigen bij de destroy. En zoals reeds gemeld, roep je de fSubject.Notify op daar waar je het event wil uitvoeren. Bij ons is dit in de OnClick van de RadioGroup.
    In code ziet dat er als volgt uit:
    Delphi Code:
    1. procedure TInstellingenForm.KaartKleurRadioClick(Sender: TObject);
    2. begin
    3.   fSubject.Notify (TKaartKleur(KaartKleurRadio.ItemIndex))
    4. end;
    5.  
    6. procedure TInstellingenForm.FormCreate(Sender: TObject);
    7. begin
    8.   FSubject := TKaartKleurSubject.Create ;
    9. end;
    10.  
    11. procedure TInstellingenForm.FormDestroy(Sender: TObject);
    12. begin
    13.   fSubject.Free
    14. end;

    Stap 5: De observers inbouwen.
    In elke form dat we willen laten reageren op een event van onze Instellingen, bouwen we een observer in. Een fObserver wordt private gedeclareerd, en een public property verwijst naar deze fObserver. Verder maken we een private procedure ToonKaartKleur, waarin de code staat die we wensen uit te voeren als het event in de InstellingenForm gemeld wordt.
    Samen geeft dit:
    Delphi Code:
    1. private
    2.     { Private declarations }
    3.     fObserver: TKaartKleurObserver ;
    4.     procedure ToonKaartKleur (NieuweKleur: TKaartKleur);
    5.   public
    6.     { Public declarations }
    7.     property Observer: TKaartKleurObserver read fObserver ;
    In de FormCreate creëren we de fObserver, en de ToonKaart koppelen we er aan de fObserver.OnUpdate. In de FormDestroy vernietigen we de fObserver.
    Delphi Code:
    1. procedure TMainform.FormCreate(Sender: TObject);
    2. begin
    3.   fObserver := TKaartKleurObserver.Create ;
    4.   fObserver.OnUpdate := ToonKaartKleur
    5. end;
    6.  
    7. procedure TMainform.FormDestroy(Sender: TObject);
    8. begin
    9.   fObserver.OnUpdate := nil;
    10.   fObserver.Free
    11. end;
    De implementatie van ToonKaartKleur hebben we hier in het voorbeeld, net als in alle voorgaande voorbeelden, eenvoudig gehouden. De ToonKaartKleur zet gewoon de overeenkomstige string in Label2.
    Delphi Code:
    1. procedure TMainform.ToonKaartKleur(NieuweKleur: TKaartKleur);
    2. begin
    3.   Label2.Caption := KaartKleurString[NieuweKleur]
    4. end;
    De observer moeten we inbouwen in ELKE form die wil reageren op het event. Dus doen we hetzelfde in de onze nieuwe form LogForm.
    Delphi Code:
    1. procedure TLogForm.FormCrHij voert de eate(Sender: TObject);
    2. begin
    3.   fObserver := TKaartKleurObserver.Create ;
    4.   fObserver.OnUpdate := ToonKaartKleur
    5. end;
    6.  
    7. procedure TLogForm.FormDestroy(Sender: TObject);
    8. begin
    9.   fObserver.OnUpdate := nil;
    10.   fObserver.Free
    11. end;
    12.  
    13. procedure TLogForm.ToonKaartKleur(NieuweKleur: TKaartKleur);
    14. begin
    15.   Memo1.Lines.Add(DateTimeTostr(Now)+ ' - Nieuwe kleur: ' + KaartKleurString[NieuweKleur])
    16. end;
    Als reactie op een verandering van kleur in onze InstellingenForm, willen we in de LogForm dit melden in Memo1, samen met het tijdstip. Dit is wat we in de ToonKaartKleur van TLogForm doen.

    We hebben nog één ding te doen:

    Stap 6: Op de observers aan de subject koppelen.
    We moeten nog enkel zeggen welke observers geïnteresseerd zijn in welke subject. In ons voorbeeld is dat eenvoudig: er zijn maar twee observers, en zelfs maar één subject! De koppeling gebeurt door de oproep van de Attach-method van de subject.
    Waar gaan we dit doen? Wel v????r we onze InstellingenForm voor de eerste maal openen in MainForm.
    Delphi Code:
    1. procedure TMainform.InstellingenButtonClick(Sender: TObject);
    2. begin
    3.   InstellingenForm.Subject.Attach (fObserver) ; {{Eigen observer toevoegen}
    4.   InstellingenForm.Subject.Attach (LogForm.Observer); {{De observer van de de Logform toevoegen}
    5.   LogForm.Show;
    6.   InstellingenForm.Show
    7. end;

    We zijn er!

    Toegegeven, wat meer werk dan een gewoon event, maar het effect is spectaculair!
    Je klikt op de InstellingenButton, en de Logform en de InstellingenForm openen zich.
    Telkens je een andere kaartkleur kiest, wordt dat onmiddellijk gemeld aan de Mainform, die de Label2 aanpast, én aan de Logform die het in de Memo logt.
    Click image for larger version. 

Name:	Oplossing5-2.jpg 
Views:	288 
Size:	31.8 KB 
ID:	4818
    Werking.
    Nog even kort de werking van het geheel uitleggen in 4 stappen:
    1. In de InstellingenForm wordt van kaartkleur veranderd. De InstellingenForm wil deze blijde gebeurtenis aan de 'Gehele Wereld' verkondigen. Alleen, weet de instellingenform niet in welke applicatie hij zit, laat staan wie er nu geïnsteresseerd is in dit heuglijke nieuws. Daarom stuurt hij maar een een Notify aan zijn Subject, die zich wel over de blijde boodschap zal kommeren.
      Delphi Code:
      1. fSubject.Notify (TKaartKleur(KaartKleurRadio.ItemIndex))
    2. De subject gaat alle ingeschreven observers verwittigen. Da's gemakkelijk, al deze observers behoren tot dezelfde klasse, en hebben de Update method. Dus moet de subject gewoon in een lusje de Update methods oproepen.
      Delphi Code:
      1. for i:=0 to fObservers.Count - 1 do
      2.   TKaartKleurObserver(fObservers[i]).Update(KaartKleur)
    3. De Update van een observer doet niets anders dan zijn eigen event fOnUpdate starten.
      Delphi Code:
      1. if Assigned(FOnUpdate) then
      2.   FOnUpdate(NieuweKleur)
    4. Vermits een procedure ToonKaartKleur (had evengoed helemaal anders kunnen heten) aan de OnUpdate gekoppeld is, wordt deze ToonKaartKleur uitgevoerd.
      Delphi Code:
      1. Memo1.Lines.Add(DateTimeTostr(Now)+ ' - Nieuwe kleur: ' + KaartKleurString[NieuweKleur])


    Besluit.

    Communicatie tussen twee forms is heel eenvoudig als het van de MainForm naar een Subform gaat:
    Delphi Code:
    1. SubForm.DoeIets( MetDezeParameter )
    Wil de Mainform speciaal een SubForm openen, om van die Subform iets te weten te komen, dan gebruiken we een dialog:
    Delphi Code:
    1. NieuweWaarde := SubForm.GeefMijEenWaarde ( MetDezeParameter )
    Wil de SubForm de Mainform verwittigen, telkens Subform iets interessants te melden heeft, dan gebruiken we een event:
    Delphi Code:
    1. if assigned(fNotify) then
    2.   fNotify ( MetDezeNieuweWaarde )
    Wil de SubForm de hele wereld verwittigen van elke interessante gebeurtenis, dan gebruiken we een subject uit een Observer-Pattern:
    Delphi Code:
    1. fSubject.Notify ( MetDezeNieuweWaarde )
    Deze technieken kun je niet alleen tussen forms gebruiken, maar zo kun je ook allerlei componenten en klassen met mekaar laten communiceren. Laat je fantasie maar de vrije loop.

    Artikel uitknippen en boven je bed hangen.

    Sam Witse
    Attached Files Attached Files
    Last edited by GolezTrol; 30-Jan-10 at 11:00.

  6. #6
    Een mooi artikel, Sam! Het observer pattern is volgens mij best een krachtig pattern, maar het is vrij bewerkelijk om te implementeren. Natuurlijk krijg je er wel handigheid in en kun je delen kopiëren, maar er wordt toch al snel gegrepen naar 'gewone' events, en niet voor niets.

    Het is in ieder geval prettig om te zien hoe je van een heel simpele oplossing steeds verder bouwt en ontwikkelt tot de implementatie van het pattern. Ik denk dat mensen met dit artikel veel inzicht kunnen krijgen in de verschillende mogelijkheden die er zijn, en welke mate van abstractie je daarmee kunt krijgen. Op die manier kun je een oplossing kiezen die op dat moment een goede afweging is tussen benodigde flexibelheid en tijd/kosten, in de wetenschap dat je, met dit artikel bij de hand, altijd nog eens een stukje kunt refactoren naar een hoger niveau.

    Om de overhead wat in te dammen heb ik wel een keer geprobeerd om een generieke interface te maken voor een observer. Mijn hersenspinsels heb ik destijds al eens gepost:
    Delphi Code:
    1. type
    2. // ---------------------------------------------------------------------------
    3. // Observer.
    4. // ---------------------------------------------------------------------------
    5.   IBigObserverSubject = interface;
    6.  
    7.   IBigObserver = interface
    8.   ['{78590AD3-F432-41EA-9511-C6639F97BC33}']
    9.     procedure SubjectNotification(Subject: IBigObserverSubject);
    10.   end;
    11.  
    12.   IBigObserverSubject = interface
    13.   ['{746CD21C-04B3-4BB5-9944-2CA561D15BF3}']
    14.     procedure Attach(Observer: IBigObserver);
    15.     procedure Detach(Observer: IBigObserver);
    16.     procedure NotifyObservers;
    17.   end;
    Ik heb toen ook een generieke implementatie gemaakt die als aggregate object te gebruiken is.

    Meer code en uitleg hierover zijn te vinden in deze post. Ik zal niet alle inhoud hier overnemen.
    1+1=b

  7. #7
    Mooi artikel, dankjewel Sam! Ik hoop dat het mensen helpt om weer een stapje te zetten in éénrichtingsverkeercode. Ik zie nog steeds in veel projecten dat er keihard wordt geroepen naar MainForm, of andere public forms of datamodules. Dat leidt dan altijd tot units of classes die niet voor hergebruik in aanmerking komen. Jouw oplossing is één van de manieren om dat te voorkomen.
    Marcel

  8. #8
    Delphi & OO in Vlaanderen SamWitse's Avatar
    Join Date
    Sep 2007
    Location
    Brussel
    Posts
    833
    Ook voor mij is het soms van heel ingewikkelde structuren terugvallen op forms met events, en de hele structuur valt mooi in de plooi. Met dit artikel kan ik gemakkelijk terugvinden wat ik nodig heb.
    Het artikel was dus in de eerste plaats voor mezelf bedoeld

    Hopelijk is dit de aanzet voor anderen om ook artikels te zichzelf te schrijven, en ze dan hier te publiceren.
    Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration.

    Sam Witse.
    Delphi & OO in Vlaanderen

  9. #9
    Is het niet handiger om het obserserv gedeelte wat algemener te maken. Het is op den duur niet te doen om voor iedere soort communicatie weer nieuwe classes te maken die ergens op reageren. Meer een publish/subscribe principe dus

    ff snel uit het hoofd, en ff grof:

    iets:
    Code:
    interface 
       
    type 
      TIetsEvent = procedure(Sender : TObject; Value : variant) of object;
      TIets = class
      public
          procedure DefineSubject(Subject : string);
          procedure Subscribe(Subject : string; VarProcedure : TIetsEvent);
          procedure Notify(Subject : string; Value : variant); 
      end;
     
    initialization 
      if (iets=nil) then
         iets := TIets.Create;
    finalization
      FreeAndNil(iets);

    MainForm:
    Code:
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      iets.DefineSubject('formcolor');   // definieer een onderwerp
      //
      iets.Subscribe(self, 'formcolor', FormColorChanged); // abonneer op onderwerp
    end; 
    
    procedure TForm1.FormColor(Sender : TObject;  Value : variant);
    begin
      self.Color := TColor(Value);
    end;

    SettingsForm:
    Code:
     
    procedure TSettingsForm.OKClick(Sender : TObject);
    begin
       iets.Notify(self, kleur);
    end;

  10. #10
    John Kuiper
    Join Date
    Apr 2007
    Location
    Almere
    Posts
    8,747
    Het is inderdaad een mooi stuk. Maar ik ben toch echt de weg kwijtgeraakt bij hoofdstuk 5.
    Wat is de reden om een observer te maken?
    Delphi is great. Lazarus is more powerfull

  11. #11
    Een observer is in principe gewoon een class die zich registreert om op de hoogte gesteld te worden van een een bepaald event. In feite verschilt het niet zoveel van 'normale' events in Delphi. Er zijn twee voorname verschillen:
    - Observer pattern beschrijft dat het mogelijk is om meerdere observers te hebben voor 1 subject
    - Observer pattern beschrijft dat observers zich kunnen registeren en deregistreren. Een observer die vrijgegeven wordt moet zichzelf dus ook deregistreren. Bij events is dat ook wel voordehandliggend, alleen is het niet als zodanig beschreven.

    Je gebruikt dit dus als je wilt dat meerdere classes een bepaald event kunnen afluisteren. Volgens mij is er in Delphi 2009 en hoger zoiets als multicast events. Dat zal ook een implementatie van dit pattern zijn, vermoed ik.
    1+1=b

  12. #12
    ff snel wat gemaakt zat er al langer over te denken voor een mechanisme als dit, optie 5 van Sam is technisch heel mooi, maar praktisch wat veel werkt (vind ik ).

    Hier een alternatief, 1e versie snel inelkaar getyped, graag comments.

    Code:
    unit notifysubscribe;
    
    interface
    
    uses classes, Contnrs;
    
    Type
        TNotifyProcedure = procedure(Sender : TObject; Value : Variant) of object;
    
        TSubscriber = class
        private
          FID: string;
          FOnNotify: TNotifyProcedure;
        public
          property ID       : string read FID write FID;
          property OnNotify : TNotifyProcedure read FOnNotify write FOnNotify;
        end;
    
        TSubject = class
        private
          FSubscribers : TObjectList;
          FID: string;
          function GetSubcriber(Index: integer): TSubscriber;
          procedure SetSubcriber(Index: integer; const Value: TSubscriber);
          function IndexOfSubscriber(SubscriberID : string) : Integer;
          function SubscriberById(SubscriberID : string) : TSubscriber;
          procedure AddSubscriber(SubscriberID : string; OnNotify : TNotifyProcedure);
          procedure RemoveSubscriber(SubscriberID : string);
        public
          constructor Create;
          destructor Destroy; override;
          procedure Notify(Sender : TObject; Value : Variant);
          property ID : string read FID write FID;
          property Subcriber[Index : integer] : TSubscriber read GetSubcriber write SetSubcriber;
        end;
    
        TNotifierSubscriber = class
        private
         FSubjects    : TObjectList;
         function IndexOfSubject(SubjectID : string) : integer;
         function SubjectById(SubjectID : string) : TSubject;
        public
          constructor Create;
          destructor Destroy; override;
          procedure DefineSubject(SubjectID : string);
          procedure UndefineSubject(SubjectID : string);
          procedure Notify(SubjectID: string; Sender : TObject; Value : variant);
          procedure Subscribe(SubjectID : string; SubscriberID : string; OnNotify : TNotifyProcedure);
          procedure UnSubscribe(SubjectID : string; SubscriberID : string);
        end;
    
    var
      Notifier : TNotifierSubscriber = nil;
          
    implementation
    
    uses Math, SysUtils;
    
    { TNotifierSubscriber }
    
    constructor TNotifierSubscriber.Create;
    begin
      FSubjects := TObjectList.Create;
    end; // Create
    
    procedure TNotifierSubscriber.DefineSubject(SubjectID: string);
    var
      idx : integer;
      MySubj : TSubject;
    begin
      idx := IndexOfSubject(SubjectID);
      if (idx = -1) then
      begin
        MySubj := TSubject.Create;
        MySubj.ID := SubjectID;
        FSubjects.Add(MySubj);
      end
      else
         raise Exception.create('[TNotifierSubscriber.DefineSubject] - Subject allready exists. [' + SubjectID + ']');
    end; // DefineSubject
    
    destructor TNotifierSubscriber.Destroy;
    begin
      FSubjects.Free;
      inherited;
    end; // Destroy
    
    function TNotifierSubscriber.IndexOfSubject(SubjectID: string): integer;
    var
      idx : integer;
    begin
      Result := -1;
      for idx := 0 to FSubjects.Count - 1 do
          if SameText(TSubject(FSubjects[idx]).ID, SubjectID) then
          begin
             Result := idx;
             break;
          end;
    end; // IndexOfSubject
    
    procedure TNotifierSubscriber.Notify(SubjectID: string;  Sender : TObject; Value: variant);
    var
      Subject : TSubject;
      idx : integer;
    begin
      Subject := SubjectById(SubjectID);
      if (Subject = nil) then
         raise Exception.create('[TNotifierSubscriber.Notify] - Subject not found. [' + SubjectID + ']')
      else
        Subject.Notify(Sender, Value);
    end; // Notify
    
    function TNotifierSubscriber.SubjectById(SubjectID: string): TSubject;
    var
      idx : Integer;
    begin
      idx := IndexOfSubject(SubjectID);
      if (idx = -1) then
         Result := nil
      else
         Result := TSubject(FSubjects[idx]);
    end; // SubjectByID
    
    procedure TNotifierSubscriber.Subscribe(SubjectID, SubscriberID : string; OnNotify: TNotifyProcedure);
    var
      Subject    : TSubject;
    begin
      Subject := SubjectById(SubjectID);
      if (Subject = nil) then
         raise Exception.create('[TNotifierSubscriber.Subscribe] - Subject not found. [' + SubjectID + ']')
      else
         Subject.AddSubscriber(SubscriberID, OnNotify)
    end; // Subscribe
    
    procedure TNotifierSubscriber.UndefineSubject(SubjectID: string);
    var
      idx : integer;
    begin
      idx := IndexOfSubject(SubjectID);
      if (idx = -1) then
        raise Exception.create('[TNotifierSubscriber.UnSubscribe] - Subject not found. [' + SubjectID + ']')
      else
        FSubjects.Remove(FSubjects[idx])
    end; // UnDefineSubject
    
    procedure TNotifierSubscriber.UnSubscribe(SubjectID, SubscriberID: string);
    var
      Subject : TSubject;
      idx : integer;
    begin
      Subject := SubjectById(SubjectID);
      if (Subject = nil) then
         raise Exception.create('[TNotifierSubscriber.UnSubscribe] - Subject not found. [' + SubjectID + ']')
      else
        Subject.RemoveSubscriber(SubscriberID)
    end; // UnSubScriber
    
    
    { TSubject }
    
    procedure TSubject.AddSubscriber(SubscriberID: string;  OnNotify: TNotifyProcedure);
    var
      Subscriber : TSubscriber;
      idx : integer;
    begin
      idx := IndexOfSubscriber(SubscriberID);
      if (idx = -1) then
      begin
        Subscriber := TSubscriber.Create;
        Subscriber.ID := SubscriberID;
        Subscriber.OnNotify := OnNotify;
        FSubscribers.Add(Subscriber);
      end
      else
        raise Exception.create('[ TSubject.AddSubscriber] - Already subscribed. [' + SubscriberID + ']')
    end; // AddSubscriber
    
    constructor TSubject.Create;
    begin
      FSubscribers := TObjectList.Create;
    end; // Create
    
    destructor TSubject.Destroy;
    begin
      FSubscribers.Free;
      inherited;
    end; // Destroy
    
    function TSubject.GetSubcriber(Index: integer): TSubscriber;
    begin
      Result := TSubscriber(FSubscribers[Index]);
    end; // GetSubScriber
    
    function TSubject.IndexOfSubscriber(SubscriberID: string): Integer;
    var
      idx : integer;
    begin
      Result := -1;
      for idx := 0 to FSubscribers.Count - 1 do
          if sametext(TSubscriber(FSubscribers[idx]).ID,SubscriberID) then
          begin
            Result :=idx;
            break;
          end;
    end; // IndexOfSubscriber
    
    procedure TSubject.Notify(Sender : TObject; Value: Variant);
    var
      idx : integer;
    begin
      for idx :=0 to FSubscribers.Count - 1do
          TSubscriber(FSubscribers[idx]).OnNotify(Sender, Value);
    end; // Notify
    
    procedure TSubject.RemoveSubscriber(SubscriberID: string);
    var
      idx : integer;
    begin
      idx := IndexOfSubscriber(SubscriberID);
      if (idx = -1) then
        raise Exception.create('[TNotifierSubscriber.RemoveSubscriber] - Subscriber not found. [' + SubscriberID + ']')
      else
         FSubscribers.Remove(FSubscribers[idx])
    end; // RemoveSubscriber
    
    procedure TSubject.SetSubcriber(Index: integer; const Value: TSubscriber);
    begin
      FSubscribers[Index] := Value;
    end; // SetSubscriber
    
    function TSubject.SubscriberById(SubscriberID: string): TSubscriber;
    var
      idx : integer;
    begin
      idx := IndexOfSubscriber(SubscriberID);
      if (idx = -1) then
         result := nil
      else
         Result := TSubscriber(FSubscribers[idx]);
    end; // SetSubsriber
    
    initialization
       if not assigned(Notifier) then
         Notifier := TNotifierSubscriber.Create;
    
    finalization
       FreeAndNil(Notifier);
    
    end.

    Gebruik:

    FrmMain.pas
    Code:
    uses notifysubscribe;
    
    procedure TfrmMain.FormCreate(Sender: TObject);
    begin
      Notifier.DefineSubject('caption');
      Notifier.Subscribe('caption', self.name, CaptionChange);
    end;
    
    procedure TfrmMain.CaptionChange(Sender: TObject; Value: Variant);
    begin
      Caption := Value;
    end;
    
    procedure TfrmMain.Button2Click(Sender: TObject);
    begin
      Notifier.Notify('caption',Self, 'Nieuwe Form Caption');
    end;
    willekeurig anderform:
    Code:
    uses notifysubscribe;
    
    procedure TForm1.CaptionChange(Sender: TObject; Value: Variant);
    begin
      Caption := Value;
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Notifier.Subscribe('caption',self.Name, CaptionChange);
    end;

  13. #13
    Wel een goed idee, maar je ziet hier zelf al een beetje het probleem. Jouw versie staat toe om een observer te reserveren voor een bepaalde string. Dat betekent dus dat je subject een lijst van strings bij moet houden. Voor een paar dingen is dat nog wel leuk, maar als je meer observers hebt en vooral meerdere strings om op te reageren, dan is het misschien wel een tikkie trager.
    Dit kun je deels verhelpen door geen objectlist maar een stringlist bij te houden. Daarin zet je dan de string die geregistreerd wordt. Aan elke string kun je een object hangen. Door de string vervolgens te sorteren (Sorted = True) zal het zoeken erin sneller gaan.

    Of het in de praktijk veel uit zal maken weet ik niet, maar je kan met zo'n generieke oplossing maar beter voorbereid zijn op te toekomst, toch?

    Maar ik vraag me af of je het zo wel een observer moet noemen. Je hebt nu een string, en een variant. Dat maakt de implementatie meteen een stuk uitgebreider. Een variant is daarbij nogal los. Je observers moeten dus heel strak weten wat daar in kan zitten.
    Ik zou eerder geneigd zijn om een eenvoudig signaal te sturen zonder de waarde mee te sturen. De observers die daar behoefte aan hebben kunnen die waarde vervolgens opvragen bij het object. Op die manier sta je veel vrijer in je implementatie

    Eén object voor meerdere typen events (zoals 'caption' in jouw geval) is in mijn ogen ook wel een beetje vreemd, maar daar kan ik me nog wel wat bij voorstellen. Het alternatief is om per monitor-bare property een notifier aan te maken.

    En tot slot vraag ik me af of het zo logisch is dat frmMain in je voorbeeld zijn eigen observer is. Er zit wel wat in, maar ik kan me ook voorstellen dat zo'n object zelf z'n zaakjes intern regelt. Voordeel is wel dat je direct ziet of je observer constructie werkt.
    Last edited by GolezTrol; 08-Feb-10 at 13:46.
    1+1=b

  14. #14
    De Subscribers, worden direct aan een Subject gekoppeld. Dus een 'Notify' van een Subject kan al direct al z'n 'Subscribers' benaderen.

    Nog even wat anders, zou er iets voor (en/of tegen) te zeggen zijn om de Notify loop, (dus daar waar alle subscribers worden aangeroepen) in een aparte thread te laten lopen ?

    Op zich heb je gelijk dat mijn oplossing wat los is, daardoor is het wel makkelijk om op een eenvoudige manier verschillende data door je applicatie heen te sturen. Ik zie me niet voor iedere TColor property (Color, Font.Color etc), een hele class aanmaken.

    Dan doe ik liever iets als:
    Delphi Code:
    1. Notify('form.backcolor', clGreen);
    2. Notify('font.color', clBlack);
    natuurlijk dien je dan in een redelijk project wel een en ander duidelijk te maken, bv:
    Delphi Code:
    1. const
    2.  notFormColor = 'form.backcolor';
    3.  notFontColor = 'font.color';
    4.  
    5. Notify(notFormColor, clGreen);
    6. Notify(notFontColor, clBlack);

    (zit me nog te bedenken, om die namen wat strakker in de code te krijgen).
    Last edited by GolezTrol; 08-Feb-10 at 20:56.

  15. #15
    Silly member NGLN's Avatar
    Join Date
    Aug 2004
    Location
    Werkendam
    Posts
    5,133
    Mooi en duidelijk artikel Sam!!

    Quote Originally Posted by mvanrijnen View Post
    zou er iets voor (en/of tegen) te zeggen zijn om de Notify loop, (dus daar waar alle subscribers worden aangeroepen) in een aparte thread te laten lopen ?
    Dat zou weinig zin hebben, aangezien de eventhandlers van de subscribers in die loop toch in de mainthread worden uitgevoerd. Je zou elke
    Code:
        TSubscriber(FSubscribers[idx]).OnNotify(Sender, Value);
    synchronized moeten uitvoeren, dus het netto resultaat van de threadcode beperkt zich dan tot:
    Code:
      for idx := 0 to FSubscribers.Count - 1 do
    Ik begrijp overigens volledig jouw wens om de implementatie van het observer-pattern te vergemakkelijken. Want om voor elke observer een aparte klasse te moeten schrijven en een private field voor te moeten declaren vind ik ook te veel werk.
    Zie eventueel mijn hybride alternatief waar je nog wel steeds een apart type voor elk subject moet aanmaken, maar waarbij je vervolgens elk willekeurig object als observer bij kunt registreren op basis van een simpele TNotifyEvent (want zoals GolezTrol al zegt: de waarde haalt die observer dan maar uit het subject). Let op: in mijn voorbeeld heet het subject 'TController'.
    (Sender as TNLDUser).Signature := 'Groeten van Albert';

Page 1 of 2 1 2 LastLast

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •