Mega Code Archive

 
Categories / Delphi / Examples
 

Reducing Source Code Complexity in your application

Title: Reducing Source Code Complexity in your application Question: Have you ever written an application where things have to know when things happen, such as when an object gets freed then you need to update some UI screen or remove some depency. Or in the case of a paint program where when a mode change requires a cursor change, buttons to enable or disable or push down... if something gets deleted then you have to do this and that etc... I have a solution that will keep your code clean of linking code. Answer: This uses Delphi's built in Messaging in TObject. Whats in here: Explaining on the concept. Example implementation. Source code of the MessageCenter is listed at the end. Download full demo. There are times when you write an application that turns into a linking nightmare when your system needs to react to certain conditions. Examples are Mode changing in a paint program requires cursor changes, an object being updated needs to update some UI element or disable and enable controls, when an object gets freed you need to remove dependencies. In other words there are side effects that you need to happen as a result of something changing in your application. Coding these side effects can produce some nasty code that is like a big spider web. The solution to the problem is to use a "Message Center". I have created a easy to use MessageCenter class that uses the built in messaging capablity already built into TObject. Source code is at the end of this artical. 1. Concept of the message center The concept is simple, you have a central "hub" that receives maybe all actions that happen in your program. Certain parts of your program need to change when these events happen. Instead of hard coding these "reactions" into your code, you send the message of the event to the message center in a record structure. Anything that needs to react or change based on the event is registered with and notified by the MessageCenter. 2. Example Implementation This app is an image editor where you can have multiple images opened at once. Each Image is opened in a Form class of TForm_ImageEdit. A graphical list of buttons are listed at the top of the main form, there is one button per opened image and a picture of the image is drawn on the surface of the button. Users can click the button and active the form for that image. The rule of the system is A button should be added when a new form is added. The button should remove when the form is removed. The button should push down when the editor form becomes active. First define the MessageID and the record for the message. const MID_ImageEdit = 14936; type TMID_ImageEdit = packed record MessageID: Cardinal; // This is required field for Dispatching Action: (aDestroyed, aActivated); ImageEdit: TForm_ImageEdit; end; Then within the TForm_ImageEdit Broadcast the messages... procedure TForm_ImageEdit.FormDestroy(Sender: TObject); var M: TMID_ImageEdit; begin with M do begin M.MessageID:= MID_ImageEdit; M.Action:= aClosed; M.ImageEdit:= Self; end; GetMessageCenter.BroadcastMessage(Self, M); end; procedure TForm_ImageEdit.FormActivate(Sender: TObject); var M: TMID_ImageEdit; begin with M do begin M.MessageID:= MID_ImageEdit; M.Action:= aActivated; M.ImageEdit:= Self; end; GetMessageCenter.BroadcastMessage(Self, M); end; Now to edit the main form At some point in your main form when you create the Image Editor, add this code after creation: F:= TForm_ImageEdit.Create(Self); // Listen to messages GetMessageCenter.AttachListner(Self, F); // Next few lines will add the button for the new form at the top of the main window. . . . This way the Main form will receive messages from the ImageEditor window. So now Add this MessageHandler to your main form: Create this method to receive messages of type MID_IMageEdit: procedure ImageEditorWindowChanged(var Msg: TMID_ImageEdit); message MID_ImageEdit; And implement it in this way procedure TForm_NMLDA.ImageEditorWindowChanged(var Msg: TMID_ImageEdit); begin case Msg.Action of aDestroyed: begin ImageEditorClosed(Msg.ImageEdit); GetMessageCenter.DetachListner(Self, Msg.ImageEdit); end; aActivated: EditorFocused(Msg.ImageEdit); end; end; ImageEditorClosed method will remove the button from the main form EditorFocused will push down the button associated with the ImageEditor. ------------------------- Thats all, you have low coupling and you may attach as many listners as you like. This concept has a lot of potential and it will make your complex apps very simple and maintainable. Here is the code: ================================= unit MessageCenter; { William Egge public@eggcentric.com Created Feb - 28, 2002 You can modify this code however you wish and use it in commercial apps. But it would be cool if you told me if you decided to use this code in an app. The goal is to provide an easy way to handle notifications between objects in your system without messy coding. The goal was to keep coding to a minimum to accomplish this. That is why I chose to use Delphi's built in Message dispatching. This unit/class is intended to be a central spot for messages to get dispatched, every object in the system can use the global GetMessageCenter function. You may also create your own isolated MessageCenter by creating your own instance of TMessageCenter.. for example if you had a large subsystem and you feel it would be more effecient to have its own message center. The goal is to capture messages from certain "Source" objects. Doc: procedure BroadcastMessage(MessageSource: TObject; var Message); The message "Message" will be sent to all objects who called AttachListner for the MessageSource. If no objects have ever called AttachListner then nothing will happen and the code will not blow up :-). Notice that there is no registration for a MessageSource, this is because the MessageSource registration happens automatically when a listner registers itself for a sender. (keeping external code simpler) procedure AttachListner(Listner, MessageSource: TObject); This simply tells the MessageCenter that you want to receive messages from MessageSource. procedure DetachListner(Listner, MessageSource: TObject); This removes the Listner so it does not receive messages from MessageSource. Technique for usage with interfaces: If your program is interface based then its not possible to pass a MessageSource but it IS possible to pass an object listner if it is being done from within the object wanting to "listen" (using "self"). To solve the problem of not being able to pass a MessageSource, you can add 2 methods to your Sender interface definition, AttachListner(Listner: TObject) and DetachListner(Listner: TObject). Internally within those methods your interfaced object can call the MessageCenter and pass its object pointer "Self". Info: Performance and speed were #1 so... MessageSources are sorted and are searched using a binary search so that a higher number of MessageSources should not really effect runtime performance. The only performance penalty for this is on adding a new MessageSource because it has to do an insert rather than an add, this causes all memory to be shifted to make room for the new element. The benifit is fast message dispatching. There is no check for duplicate MesssageListners per Sender, this would have slowed things down and this coding is usefull only when you have bugs. And hoping you prevent bugs, you do not have to pay for this penalty when your code has no bugs. } interface uses Classes, SysUtils; type TMessageCenter = class private FSenders: TList; FBroadcastBuffers: TList; function FindSenderList(Sender: TObject; var Index: Integer): TList; public constructor Create; destructor Destroy; override; procedure BroadcastMessage(MessageSource: TObject; var Message); procedure AttachListner(Listner, MessageSource: TObject); procedure DetachListner(Listner, MessageSource: TObject); end; // Shared for the entire application function GetMessageCenter: TMessageCenter; implementation var GMessageCenter: TMessageCenter; ShuttingDown: Boolean = False; function GetMessageCenter: TMessageCenter; begin if GMessageCenter = nil then begin if ShuttingDown then raise Exception.Create('Shutting down, do not call GetMessageCenter during shutdown.'); GMessageCenter:= TMessageCenter.Create; end; Result:= GMessageCenter; end; { TMessageCenter } procedure TMessageCenter.AttachListner(Listner, MessageSource: TObject); var L: TList; Index: Integer; begin L:= FindSenderList(MessageSource, Index); if L = nil then begin L:= TList.Create; L.Add(MessageSource); L.Add(Listner); FSenders.Insert(Index, L); end else L.Add(Listner); end; procedure TMessageCenter.BroadcastMessage(MessageSource: TObject; var Message); var L, Buffer: TList; I: Integer; Index: Integer; Obj: TObject; begin L:= FindSenderList(MessageSource, Index); if L nil then begin // Use a buffer because objects may detach or add during the broadcast // Broadcast can be recursive. Only broadcast to objects that existed // before the broadcast and not new added ones. But do not broadcast to // objects that are deleted during a broadcast. Buffer:= TList.Create; try FBroadcastBuffers.Add(Buffer); try for I:= 0 to L.Count-1 do Buffer.Add(L[I]); // skip 1st element because it is the MessageSender for I:= 1 to Buffer.Count-1 do begin Obj:= Buffer[I]; // Check for nil because items in the buffer are set to nil when they are removed if Obj nil then Obj.Dispatch(Message); end; finally FBroadcastBuffers.Delete(FBroadcastBuffers.Count-1); end; finally Buffer.Free; end; end; end; constructor TMessageCenter.Create; begin inherited; FSenders:= TList.Create; FBroadcastBuffers:= TList.Create; end; destructor TMessageCenter.Destroy; var I: Integer; begin for I:= 0 to FSenders.Count-1 do TList(FSenders[I]).Free; FSenders.Free; FBroadcastBuffers.Free; inherited; end; procedure TMessageCenter.DetachListner(Listner, MessageSource: TObject); var L: TList; I, J: Integer; Index: Integer; begin L:= FindSenderList(MessageSource, Index); if L nil then begin for I:= L.Count-1 downto 1 do if L[I] = Listner then L.Delete(I); if L.Count = 1 then begin FSenders.Remove(L); L.Free; end; // Remove from Broadcast buffers for I:= 0 to FBroadcastBuffers.Count-1 do begin L:= FBroadcastBuffers[I]; if L[0] = MessageSource then for J:= 1 to L.Count-1 do if L[J] = Listner then L[J]:= nil; end; end; end; function TMessageCenter.FindSenderList(Sender: TObject; var Index: Integer): TList; function ComparePointers(P1, P2: Pointer): Integer; begin if LongWord(P1) Result:= -1 else if LongWord(P1) LongWord(P2) then Result:= 1 else Result:= 0; end; var L, H, I, C: Integer; begin Result:= nil; L:= 0; H:= FSenders.Count - 1; while L begin I:= (L + H) shr 1; C:= ComparePointers(TList(FSenders[I])[0], Sender); if C L:= I + 1 else begin H:= I - 1; if C = 0 then begin Result:= FSenders[I]; L:= I; end; end; end; Index := L; end; initialization finalization ShuttingDown:= True; FreeAndNil(GMessageCenter); end.