EDIT: NAV2016 has come out with new “Event” functionality, and looking into how that affects my Observer code I realised that what I had built was sort-of a combination of two different patterns, Observer and something a bit more like PubSub – the difference being the latter is “Cross-session”. See my follow-up blogs here: Observer in NAV2016 and PubSub in NAV2016
My previous post shows a Control Add-In we developed to show a resource schedule/calendar (called the Whiteboard). When we first built it we were doing it for a single user with data that didn’t change very often. However we soon needed to add more users and be able to track changes made from both the Add-In and the underlying NAV tables, in real-time (or close enough to).
Tracking changes from the Add-In is easy enough, as all the collections implement IObservable and I have provided ApplicationVisible Events to NAV for when these are changed. But how could we get these changes between users (and possibly between Service Tiers) or if something on the NAV end changes the data? Solving one solves the other, because whenever my event fires I propagate any changes into the NAV tables so I just need to track the NAV changes.
So I wanted to implement the IObservable or Observer Pattern in NAV. However the standard pattern is based an Event model that NAV just does not have (without using Dot Net interop*) so I figured that I could instead push the Notification “events” into a Table that acts as a FIFO Queue (which is not a new idea – there are a few implementations of a Observable Pattern Queue around).
The Eureka moment came when I realised NAV already has something that almost does exactly what I want – i.e. Change Log. I can’t use Change Log as I would want to clear my Queue’s out and therefore I need to keep separate Queue’s per User, and I also only want it running sometimes. But the structure of recording the changes is almost a straight copy from the Change Log code.
I’ve ended up with 3 tables:
Observable Table: "Table ID" Integer Object.ID WHERE (Type=CONST(Table)) Change Observer: "Table ID" Integer "Observable Table" "Server ID" Integer "Session ID" Integer Change Notification: "Table ID" Integer "Observable Table" "Server ID" Integer "Session ID" Integer "Entry No." Integer AutoIncrement "Type of Change" Option Insert,Modify,Delete,Rename "Record ID" RecordID ... (other fields to indicate what has changed)
As you can see I decided that it was good enough for my implementation for the Observer to be identified by Server and Session ID’s. I think this could be extended to include other identifiers but this was the simplest approach.
Initially I didn’t have “Observable Table” as a separate table at all, but it is needed for the triggers in CU1 to work, specifically GetDatabaseTableTriggerSetup which is run once per Table per Session, and if you don’t return TRUE flags the other triggers are then not run.
C/AL Codeunit 1 ApplicationManagement ... GetDatabaseTableTriggerSetup(...) ChangeLogMgt.GetDatabaseTableTriggerSetup(TableId,OnDatabaseInsert,OnDatabaseModify,OnDatabaseDelete,OnDatabaseRename); ObserverMgt.GetTableTriggerSetup(TableId,OnDatabaseInsert,OnDatabaseModify,OnDatabaseDelete,OnDatabaseRename); //TVT OBSERVER OnDatabaseInsert(RecRef : RecordRef) ChangeLogMgt.LogInsertion(RecRef); ObserverMgt.NotifyAll(RecRef,0); //TVT OBSERVER OnDatabaseModify(RecRef : RecordRef) ChangeLogMgt.LogModification(RecRef); ObserverMgt.NotifyAll(RecRef,1); //TVT OBSERVER OnDatabaseDelete(RecRef : RecordRef) ChangeLogMgt.LogDeletion(RecRef); ObserverMgt.NotifyAll(RecRef,2); //TVT OBSERVER OnDatabaseRename(RecRef : RecordRef;xRecRef : RecordRef) ChangeLogMgt.LogRename(RecRef,xRecRef); ObserverMgt.NotifyAll(RecRef,3); //TVT OBSERVER
Then on the Page I want to listen for changes and do something with them I add a PingPong control (called Timer) and start Listening – remembering to Stop Listening when I’m done.
C/AL OnQueryClosePage(CloseAction : Action None) : Boolean ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking"); Timer::AddInReady() IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN CurrPage.Timer.Ping(1000); Timer::Pong() CallUpdate; CurrPage.Timer.Ping(1000); LOCAL CallUpdate() ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification); WITH TempChangeNotification DO BEGIN IF FINDSET THEN REPEAT IF "Type of Change" = "Type of Change"::Delete THEN BEGIN ... END ELSE IF RecRef.GET("Record ID") THEN BEGIN ... END; UNTIL NEXT = 0; END;
If you want the rest of the code let me know – I’m not exactly sure how best to do that through WordPress.
* For a Dot Net interop implementation of this you might want to start with (or just use) Vjeko’s Mediator pattern which is related to the Observer pattern but more for passing messages. The reason I didn’t use this is because 1) I didn’t really want to add push message DotNet code into Insert, Modify, Delete triggers and 2) I realised I didn’t need it for the “simple” changes I wanted to track.