Mega Code Archive

 
Categories / Delphi / Examples
 

ObServer Pattern. The easy way! (Updated)

Title: ObServer - Pattern. The easy way! (Updated) Question: How do implement an easy to use ObServer pattern? Answer: OBSERVER-PATTERN - THE EASY WAY! History ========= 2005/05/28 Added: Sourcecode + Sample Introduction ============== As for all articles I wrote here on Delphi3000, the following one comes from a 'real-world' situation where I need some functionality and aren't lucky enough with the already exist solutions. I'm speaking over the so called "ObServer-Pattern" or the big question: "How does my application can talk to him/herself?" But, for what in the hell does I need thuch a function? Well, this relies on the complexity your application will be. You don't need this concept in a small application but as soon as your app grows - in code size & complexity - you'll come to a point where one unit/method needs to notify other onces about a special event, e.g. that the database coredata has been changed and one or more grids needs to update their data. Okay, you can do that by simply calling some methods like this: ...OnCoreDataChanged(Sender: TObject); begin MyGridForm1.UpdateDisplay; MyGridForm2.UpdateDisplay; .... end; ...but do you think this is a good coding style if you keep in mind that there could be a lot of other places where this functions need to call? Not really! The ObServer-Pattern is a concept about a notification mechanism where objects can register a callback function which will be "fired" on predefined cases. Example: ObjectA registers itself to be notified on EventA ObjectB registers itself to be notified on EventA & B ObjectC registers itself to be notified on EventA & C ObjectD fires the EventA - you don't take care about which other objects to be notified about this event - the observer will notify all registered objects automatically. So, each time you creating a new object which should be notified about a special event, you don't need to add the notification call to every calling method - just register a callback and that's it! Implementation ================ There are many ways to implement the ObServer pattern in Delphi: using event handlers, common callback procs, ... I'll use a simple & effective way using predefined constants & interfaces. The main benefit of the predefined constants instead of notifications-strings (for example) is that you can use Delphi's Code-Insight to look for the right registration parameter (explained below). The client part ================= Let's start to implement the client part of the concept. Each object, which should receive a notification from the server (the management object where to register the event callbacks - here called: "manager") must implement a client interface. If you forget to implement this interface and trying to register an event, the manager will throw an exception. In our example, we'll create a "TFrame" which will be notified if our application is in idle-state. Each normal "TForm" can overwrite a method called "UpdateActions" which will be called each time the application isn't busy and can be used to update the action-states (e.g. toolbar buttons/items) but there's no possibility to do so in "TFrames". That's why I use this for an example which can be also used for real-world applications. Okay, this is the interface implemented in the manager unit: { IObServerClient } IObServerClient = interface ['{189AE203-BADD-41AD-95F9-58F62F6C6DAB}'] procedure ObServerNotification(AType: TNotificationType; Action, Data: Integer); end; ..and this is the code for our "TFrame"-example: { TMyFrame } TMyFrame = class(TFrame, IObServerClient) private { Private-Deklarationen } procedure ObServerNotification(AType: TNotificationType; Action, Data: Integer); public { Public-Deklarationen } constructor Create(AOwner: TComponent); override; end; The only think to do in the client part is to register all needed callback events and to implement the "ObServerNotification"-method (our callback proc). If you take a look at this method, you may notice the passed parameters "AType", "Action" and "Data". The "AType" parameters should be used to categorize the type of notification you will be informed of (e.g. "ntSettings", "ntSysCommand", ...). The "Action" paramter passes an integer constant describing the notification action (e.g. NTA_SETTINGS_CLAIMDATA) and "Data" can be used to pass additional values or a reference to an object or data-structure. Let's take a look to the "OnCreate" method of our frame and how it could looks like: constructor TMyFrame.Create(AOwner: TComponent); begin inherited Create(AOwner); { register notifications } ObServer.RegisterNotifications(Self, [ntSettings]); end; Pretty easy, isn't it? This single line tells the manager to notify "TMyFrame" whenever an event from the category "Settings" has been raised. Okay, here's our callback implementation: procedure TMyFrame.ObServerNotification(AType: TNotificationType; Action, Data: Integer); begin // e.g. reload list of customers cause the claimdata has been // modified. end; ...you may also differ the passed action like this: procedure TMyFrame.ObServerNotification(AType: TNotificationType; Action, Data: Integer); begin case Action of NTA_SETTINGS_COREDATA: ; // reload coredata NTA_SETTINGS_CLAIMDATA. ; // reload claimdata end; end; It's also possible to register multiple categories or filter specific actions: constructor TMyFrame.Create(AOwner: TComponent); begin inherited Create(AOwner); { register notifications } ObServer.RegisterNotifications(Self, [ntSettings, ntSysCommand]); { filter notification actions } ObServer.FilterActions(Self, ntClaimData, [ NTA_CLAIMDATA_HOLIDAYS, NTA_CLAIMDATA_VACATIONS]); end; For our example we'll just register a simple callback & filter a specific action: { TMyFrame } TMyFrame = class(TFrame, IObServerClient) private { Private-Deklarationen } procedure ObServerNotification(AType: TNotificationType; Action, Data: Integer); procedure UpdateActions; public { Public-Deklarationen } constructor Create(AOwner: TComponent); override; end; constructor TMyFrame.Create(AOwner: TComponent); begin inherited Create(AOwner); { register notifications } ObServer.RegisterNotifications(Self, [ntSysCommand]); { filter notification actions } ObServer.FilterActions(Self, ntSysCommand, [NTA_SYSCOMMAND_UPDATE]); end; procedure TMyFrame.ObServerNotification(AType: TNotificationType; Action, Data: Integer); begin { the following line is optional cause we've only one registered type } if AType = ntSysCommand then UpdateActions; end; procedure TMyFrame.UpdateActions; begin { e.g. update the state of a toolbar button } btnMyToolbarButton.Enabled := lbxCustomers.ItemIndex -1; end; Well, that's all you had to do to register your notification callbacks. The whole concept makes only sense if you've several objects (or frame, datamodules, forms, ...) who needs to be notified about a specific action. But, how to raise thuch a notification? That's as easy as registering the events as described above with only one single line of code: ObServer.Notify(ntSysCommand, NTA_SYSCOMMAND_UPDATE); With this call, the manager itselfs notifies all registered objects about an event of the category "SysCommand" with the passed action "NTA_SYSCOMMAND_UPDATE". A realworld implementation of this call could be: TfrmMain = class(TForm) ... public { Public-Deklarationen } procedure UpdateActions; override; ... end; procedure TfrmMain.UpdateActions; begin inherited; { notify all clients } ObServer.Notify(ntSysCommand, NTA_SYSCOMMAND_UPDATE); end; The server part ================= The server part - the core ObServer manager - is just a simple object with some public methods. You only need a global instance of this object to communicate within your whole application: [SharedDataModule.pas] var ObServer: TObServer; ... initialization ObServer := TObServer.Create; finalization ObServer.Free; Following a brief introduction for the public methods and for what they are build for: Method Paramter Description ============================================================================== RegisterNotifications | AClass: TObject | Registers a set of | Types: TNotificationTypes | notificationtypes with | | the passed class. ------------------------------------------------------------------------------ UnregisterNotifications | AClass: TObject; | Unregisters the passed | Types: TNotificationTypes | notificationtypes of | | the passed class. ------------------------------------------------------------------------------ LockNotification | AType: TNotificationType | Locks the passed | Actions: array of Integer | notification/actions | ClearBefore: Boolean | until you call unlock. | | If 'ClearBefore' is not | | set, the passed actions | | will be appended. ------------------------------------------------------------------------------ UnlockNotification | AType: TNotificationType | Unlock previous locked | Actions: array of Integer | notificationtypes. ------------------------------------------------------------------------------ UnlockAllNotifications | | Unlock all. ------------------------------------------------------------------------------ FilterActions | AClass: TObject | After registering the | AType: TNotificationType | types, you may filter | Actions: array of Integer | some specific actions. ------------------------------------------------------------------------------ IgnoreActions | AClass: TObject | The oposite of 'Filter | AType: TNotificationType | Actions' | Actions: array of Integer | ------------------------------------------------------------------------------ Notify | AType: TNotificationType | The 'core' method to | Action: Integer | raise an event! | Data: Integer | ------------------------------------------------------------------------------ It is necessary that you include a common unit containing all notification- types and action constants, so all units can access them - and Delphi's Code-Insight works 8-) Using constants instead of strings have the big advantage that typos are not possible except if two constants have the same 'id'entifier. You may include this file as part of the observer class, but I recommend to separate them. A sample unit could look like this: unit ObServerConsts; const NTA_UNASSIGNED = -1; NTD_UNASSIGNED = -1; NTA_CLAIMDATA_ALL = 0; NTA_CLAIMDATA_OFFICEHOURS = 1; NTA_CLAIMDATA_HOLIDAYS = 2; NTA_CLAIMDATA_VACATIONS = 3; ... type { TNotificationType } TNotificationType = (ntSettings, ntClaimData, ntSysCommand); { TNotificationTypes } TNotificationTypes = set of TNotificationType; Conclusion ============ I hope you've got a small inspiration about what you can do using the ObServer-pattern. It's highly recommended for larger projects and helps you managing the inner navigation traffic. Tip: Use CodeSite to track all navigations! Download: I'll include the object classes and a sample in the next few days.