0

I'm trying to figure out the best way to distribute state changes to multiple forms which make up an application.

In my scenario, I have a number of hardware devices which are monitored by my application. As an example, one of the devices is a GPS device. Data from these devices come in and information is then stored in one big state object. As an example, GPS positions coming in are stored with both present and historic positions made available to the application.

The application has many different forms and windows available to the user. As an example, the GPS has a form which displays the visible satellites overhead, as well as a form displaying the signal-to-noise ratios and another showing the GPS track over time.

One solution I've considered is to using something like the Observer pattern on my state object. New forms subscribe to the state object and then receive push notifications when the state object changes (new GPS position forces a push to the forms, who then re-paint and re-update their own states).

IObserver = interface
    procedure Update;
end;

IObservable = interface
    procedure Subscribe(Observer : IObserver);
end;

TObserverForm = class(TForm, IObserver)
    // ....
    procedure Update;
    begin
        // State has changed, update
    end;
end;

TApplicationState = class(IObservable);
    private
        FObservers : TList<IObserver>;
        FPosition : TPoint;
    public
        procedure Subscribe(Observer : IObserver);
        begin
            FObservers.Add(Observer);
        end;

        procedure PushUpdate;
        begin
            foreach Observer in Observers
                Observer.Update;
        end;

        property Position : TPoint read GetPosition write SetPosition;

        procedure SetPosition(Pos : TPoint);
        begin
            FPosition : Pos;
            // Notify all observers
            Self.PushUpdate;
        end;

end;

The above is a crude mock-up of what I'm considering. This relies on state calling Subscribe on each TObserverForm that is created during the application life-cycle (and un-subscribing when they're destroyed).

I can't really see any downsides to this. This solution could be tailored somewhat so that the TObserverForms only update and react to certain types of push updates.

Is there something fundamental missing here? Are there more logical/simple solutions to this common problem?

weblar83
  • 681
  • 11
  • 32
  • 2
    The observer pattern is quite simple. If it fits your needs (it sounds like it does) then use it. I think this question is rather opinion based as there's more than one possible approach to most problems. I voted to close, but feel free to ask a separate question if you stumble upon some specific problem with your implementation. – René Hoffmann Aug 11 '16 at 15:34
  • @RenéHoffmann thanks for your response. It does seem to fit my needs but I was mainly wondering how others have gone about replicating this and whether there is a better approach than this. I understand what you're saying though, thanks. – weblar83 Aug 11 '16 at 15:36
  • 1
    FWIW - I like it. I may be tempted to have the idea of subscribing to specific data types rather than every data type. Also, I have done this with a subscription for a period of time - the observer needs to re-subscribe if it wants to keep receiving the data. This avoids the problems of subscribers no unsubscribing before closing leaving the state sending data to nothing for as long as it is running (mine was a UDP transport, though, so connectionless). – Michael Vincent Aug 11 '16 at 15:36
  • Maybe your question better fits into http://programmers.stackexchange.com/ where more abstract questions can be asked. – René Hoffmann Aug 11 '16 at 15:38
  • 1
    @RenéHoffmann when referring other sites, it is often helpful to point that [cross-posting is frowned upon](http://meta.stackexchange.com/tags/cross-posting/info) – gnat Aug 11 '16 at 16:06
  • @MichaelVincent, I also agree re the subscribing to specific data types or specific events. I already have a similar kind of subscription period in the form of a hardware timeout timer which is active when a hardware device is not sending me data. After a period of time, the device is "disabled" pending user intervention. Thanks for your comment – weblar83 Aug 11 '16 at 17:53

1 Answers1

3

I think you should first determine what you try to get.

Should the event receiving and propagation be:

  • single threaded or multithreaded
  • synchronous or asynchronous
  • cross-platform or Windows/VCL-only

For simplistic - single-thread synchronous VCL - application you may just use multi-events from http://www.Spring4D.org library. All interested forms just declare the event handler and register/unregister it in the Tracker object.

type iTrackerUpdated = iEvent<TNotifyEvent>;

type TMyGPSTracker = class.....
     public
       property OnUpdate: iTrackerUpdated read FOnUpdate;

       procedure AfterCreation; override;

       property Coords: TMyGPSCoords read FCoords write SetCoords;
     end;

var GPSTracker: TMyGPSTracker;

procedure TMyGPSTracker.AfterCreation;
begin
  inherited;
  FOnUpdate := TEvent<TNotifyEvent>.Create; // auto-freed on destruction
end;

procedure TMyGPSTracker.SetCoords(const NewValue: FCoords);
begin
  if NewValue = FCoords then exit;
  FCoords := NewValue;
  OnUpdate.Invoke(Self);
end;

And in forms then something like that

type TMyForm = class(TForm)
.....

 private
    procedure CoordsUpdated(Sender: TObject);
 public
    procedure AfterCreation; override;
    procedure BeforeDestruction; override;
 end;

procedure TMyForm.CoordsUpdated(Sender: TObject);
begin
  Caption := (Sender as TMyGPSTracker).Coords.ToString();
end;

procedure TMyForm.AfterCreation;
begin
  inherited;
  GPSTracker.OnUpdate.Add( CoordsUpdated );
end;

procedure TMyForm.BeforeDestruction;
begin
  inherited;
  GPSTracker.OnUpdate.RemoveAll( Self );
end;

This is all it takes for such a situation. However...

If some form might take a loooong processing of the change - then it would block both other forms updating and the new coords acquiring from GPS driver.

In such a situation you better

  1. extract GPS object into a separate idle-priority thread, that would keep updating data even when the main VCL thread is long busy.
  2. switch to asynchronous event propagation, for VCL the simplest thing would be using PostMessage API. Then you should know that a busy form might "collect" few alerts before it would be ready to process them - and would only have to process one of those.

http://docwiki.embarcadero.com/RADStudio/Berlin/en/Understanding_the_Message-Handling_System

const WM_GPS_UPDATE = WM_USER + 10;

type TMyGPSTracker = class.....
     public
       property OnUpdateAlertForms: TThreadList<TForm> read FOnUpdate;
       property UpdateCounter: Cardinal read FUpdateCounter;     

       property Coords: TMyGPSCoords read FCoords write SetCoords;
     end;

var GPSTracker: TMyGPSTracker;

procedure TMyGPSTracker.SetCoords(const NewValue: FCoords);
var Form: TForm;
begin
  if NewValue = FCoords then exit;

  Inc(FUpdateCounter);
  FCoords := NewValue;

  for Form in FOnUpdate do
      if Form.HandleAllocated and Form.Showing then
         PostMessage( Form.WindowHandle, WM_GPS_UPDATE,
                       FUpdateCounter, LPARAM(Pointer(Self)) );
  // in some rare cases sometimes this might post messages to nowhere
  // but the only consequence would be a single non-update
end;

And then

type TMyForm = class(TForm)
.....

 private
    procedure CoordsUpdated(var Message: TMessage); message WM_GPS_UPDATE;
    var LastCoordUpdateProcessed: Cardinal; 
 end;

procedure TMyForm.CoordsUpdated(var Message: TMessage);
begin
  if LastCoordUpdateProcessed >= UpdateCounter then exit;

  Caption := GPSTracker.Coords.ToString();
  LastCoordUpdateProcessed := GPSTracker.UpdateCounter;
end;

procedure TMyForm.FormShow(Sender: TObject);
begin
  GPSTracker.OnUpdateAlertForms.Add( Self );
end;

procedure TMyForm.FormHide(Sender: TObject);
begin
  GPSTracker.OnUpdateAlertForms.Remove( Self );
end;

For non-VCL cross-platform targets you would have to find similar asynchronous messaging tools and replace PostMessage and Windows GDI Handles with their corresponding means.

Arioch 'The
  • 15,799
  • 35
  • 62
  • thanks for your comprehensive reply. The application is a VCL application with asynchronous data reception and event propagation - there could be 5 or more hardware devices connected simultaneously, each sending data asynchronously. The forms will be very quick to update so there won't be any long processing cycles during the updates. – weblar83 Aug 11 '16 at 17:50
  • Good overall reply, but it is a very bad idea to store the handle to any windows control (including forms), as controls may be recreated at any time. In your example, you could simply store the TForm reference, since accessing the Handle property always returns the correct handle. In scenarios where a handle must be stored, you can use the TWinControl.RecreateWnd event (http://docwiki.embarcadero.com/Libraries/Seattle/en/Vcl.Controls.TWinControl.RecreateWnd) to be notified when the window is recreated so you can update the stored handle. – Jon Robertson Aug 11 '16 at 18:05
  • 1
    Sorry, bad info and I took too long to edit. If you need to be notified when a window is recreated, you can implement a message handler for CM_RECREATEWND. But avoid storing window handles if at all possible. See http://stackoverflow.com/questions/3474227/postmessage-returns-invalid-window-handle-in-thread/3474466#3474466 – Jon Robertson Aug 11 '16 at 18:20
  • @JonRobertson yes, storing `TForm` reference is possible, though it potentially might case dangling pointer problem is de-registering is skipped ( programmer error or some exception in form object's destroying sequence before de-registering ). And in my practice for forms recreating hwnd is exceptionally rare case... But mostly, when thinking about PostMessage I was "inside" windows API and did not thought about high level types like TForm :-) – Arioch 'The Aug 11 '16 at 18:33
  • Fixed a subtle error - needed `TThreadList` not `TList` or there coud be possibility of [de]registration going on exactly when the GPS tracker is being in the for-loop – Arioch 'The Aug 11 '16 at 18:50
  • @weblar83 no matter how those devices work - what matters is how your program interacts with them. It might poll them all from the single main thread for example, or ask device driver to send Windows Events which would be processed in that very single main thread. Or it might create separate per-device threads. That is what matters, what goes on inside your app. Also the question is if the device list is static or can be changed while there are forms referencing those devices. In the latter case you would have to make a `TDictionary` container – Arioch 'The Aug 11 '16 at 18:54
  • Then new devices would increase (never decrease!) internal devices-global var counter to acquire their id number and register themselves in the dictionary (thread-aware must it be !!!) and would send that their id within the message. The form then would check if the given by TMessage ID still exists in the dictionary before ANY processing ( reg, dereg, update), thread-blocking the dictionary for all the processing period. – Arioch 'The Aug 11 '16 at 18:58
  • another possible implementation would be using message bus pattern but I do not know if stable thread-safe low-overhead bus implementation for Delphi exists. – Arioch 'The Aug 11 '16 at 19:01
  • @JonRobertson but your `accessing the Handle property always returns the correct handle` is only true for single-thread case, and then multi-event approach is all much simpler thing and PostMessage is not needed at all. In MT case calling .Handle is actually an idea even worse than using `TList` - sudden recreation of the window outside of main VCL thread is not what Delphi would expect from us :-) – Arioch 'The Aug 11 '16 at 19:09
  • weblar83 wasn't using a thread implementation. As I know you know, accessing VCL handles outside of the main thread is a major no-no. Various design decision such as this and AllocateHWnd prevent the VCL from being thread safe. Even outside of the VCL, updating a Windows GUI from multiple threads is very tricky to do safely. So yes, accessing the Handle property from another thread is a very bad idea, since .Handle will create/recreate the window handle if necessary. – Jon Robertson Aug 11 '16 at 20:24
  • See, not everyone knows this. weblar83 indeed did not talk about threads in the very post, but above in this comments (s)he talks about asynchronity, and while I am not sure what that should mean, it might include MT-design of the application. However for single-thread design multi-event is much simpler than PostMessage anyway so for single-thread there is no reason to bother with handles at all – Arioch 'The Aug 11 '16 at 21:48