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.