Hey all, I thought I would share an issue I solved in CPRS and share a technique.
In CPRS, when the Encounter dialog is launched, it is shown in **modal** mode, meaning that the rest of the application is not available until after the dialog is closed. For a few reasons, this is not an ideal user experience. This encounter dialog allows the user to specify associated diagnosis codes and procedure codes, health factors etc to a visit. Sometimes, to pick a correct code, I need to go back and reference my note, or look at other parts of CPRS. And when the form is modal, it is a hassle to close it out, and then look and then relaunch. So what needs to be changed to allow the dialog to be **non-modal**? I'll get into this, but first discuss closures.
In Javascript (which I have used just a bit), when an anonymous function is created, the reference to the function captures not only the location of the code, but also a copy of the variable table that exists at the time of creation. For example, one might see code like this (copied from
https://javascript.info/callbacks):
//Some code here. 'A'
loadScript('/my/script.js', function(script) {
//Some code here. 'B'
loadScript('/my/script2.js', function(script) {
//Some code here. 'C'
loadScript('/my/script3.js', function(script) {
//Some code here. 'D'
// ...continue after all scripts are loaded
});
});
});
//More action here 'E'
Code like this drives me crazy when I look at javascript code. Because my feeble mind likes to think about how code operates in sequence. I.e. A, then B, then C, etc
But javascript seems to be written to handle the uncertainty of the internet, and allows things to happen in a sequence that can't be known at design time. So in the code above, what really happens is that code 'A' is executed, and then code 'E' and on to any other part of the program. Sometime later (or perhaps not at all), if the loadScript function succeeds, THEN B is executed. And likewise sometime later C and then D will be executed.
I don't like this because the user can continue to interact with the program, possibly steering it in a different direction, while these latent tasks are still hovering in the ether. A real life example of this is the Veridigm program we use for pharmacy web prescribing. If I change a user's desired pharmacy, it sends this request off to the server, and often there is a delay before this change is echoed back to the display part. So if I go forward with sending in a script for a patient, it may be that the old pharmacy name is still displayed by their name. And I wonder, is my script going to go to that old location, or the new one? I can press 'SEND' at any time, but I usually wait until everything is in sync. I don't know if that is necessary or not. The state of the program seems indeterminate.
Back to CPRS. I solve the above problem by disallowing certain actions (for example selecting a new patient) while I have the encounter dialog displaying in non-modal format. But I'll illustrate the initial situation in standard CPRS
Procedure CodeBlockA()
var a, b : integer
begin
//Some init code
a := 5;
b := CodeBlockB(a); //Some followup code
end;
function CodeBlockB(input : integer) : integer
//Some init code
a := 1;
b := CodeBlockB(a)
//Some followup code
Result := b*2;
end;
funtion CodeBlockC(input : integer) : integer
var a, b : integerbegin
//Some init code
DialogForm.ShowModal; //<---- wait for user to close dialog
Result := DialogForm.Result + 8; //Some followup code
end;
Essentially we have CodeBockA -> CodeBlockB -> CodeBlockC -> show dialog modally
And after the dialog is closed, we Finish CodeBlockC, then finish CodeBlockB, then finish codeBlockA.
And again, the user is not allowed to do anything else in CPRS until the dialog is closed. The first step I took to solving this was to split each function into two parts, with the send part being a callback. So like this.
Procedure CodeBlockA()
var a, b : integer
begin
//Some init code
end;
Procedure HandleCodeBlockACallback;
begin
//Some followup code for CodeBlockA
end;
function CodeBlockB(input : integer) : integer
//Some init code
a := 1;
CodeBlockB(a)
end;
Procedure HandleCodeBlockBCallback;
begin
//Some followup code for CodeBlockB
end;
funtion CodeBlockC(input : integer) : integer
var a, b : integerbegin
//Some init code
DialogForm.Show; //<-- does NOT wait for user to close dialog
end;
Well, this is a start, but it doesn't show how the callbacks get called or how the callback functions have access to needed variables. So I next used a TStringList as the data storage for my "closure." A TStringList is essentially a list of strings, imaging a shopping list, with each line containing one item like 'Milk' or 'bread'. And each line is allowed to also store a corresponding number, which is usually a pointer, but it can be typecast to an integer etc. I actually extended the TStringList object so that it has some helper functions that should be self explanatory, and I'll call this a TCallbackDataList
var
CallbackData : TCallbackDataList; <-- instantiated elsewhere
Procedure CodeBlockA()
var a, b : integer
begin
//Some init code
a := 5;
b := 2;
//PUSH DATA AND CALLBACKS INTO 'STACK', I.E. PSEUDO-CLOSURE
CallbackData.AddInt('CodeBlockA.b', b)
CallbackData.AddProc('CALLER=CodeBlockA', @HandleCodeBlockACallback);
CodeBlockB(a, CallbackData); end;
function CodeBlockB(input : integer; CallbackData : TCallbackDataList) : integer
//Some init code
a := 1;
b := -12
//PUSH DATA AND CALLBACKS INTO 'STACK', I.E. PSEUDO-CLOSURE
CallbackData.AddInt('CodeBlockB.b', b)
CallbackData.AddProc('CALLER=CodeBlockB', @HandleCodeBlockBCallback);
CodeBlockB(a, CallbackData);
end;
funtion CodeBlockC(input : integer; CallbackData : TCallbackDataList) : integer
var a, b : integerbegin
//Some init code
DialogForm.CallbackData := CallbackData;
DialogForm.OnClose := HandleDialogClose;
DialogForm.Show; //<-- does NOT wait for user to close dialog
end;
Procedure TDialogForm.HandleDialogClose(Sender : TObject);
begin
if not assigned(Self.CallbackData) then exit;
CallbackData.PopThenCallProc(); <-- call last proc on stack
//above will effect calling HandleCodeBlockBCallback()end;
//------------------------------------------
Procedure HandleCodeBlockBCallback(CallbackData : TCallbackDataList);var b : integer;
begin
//Some followup code for CodeBlockB
b := CallbackData.ExtractAndDeleteInt('CodeBlockB.b')
//Some followup code for CodeBlockA
//NOTE that we have access to variables set up in CodeBlockA
//so b = -12
CallbackData.PopThenCallProc();
//above will effect calling HandleCodeBlockACallback()
end;
Procedure HandleCodeBlockACallback(CallbackData : TCallbackDataList);
var b : integer;
begin
b := CallbackData.ExtractAndDeleteInt('CodeBlockA.b')
//Some followup code for CodeBlockA
//NOTE that we have access to variables set up in CodeBlockA
//so b = 2
end;
So this is a rather complicated way of achieving what I wanted. Later versions of Delphi support native closures, and I can't wait to someday get TMGCPRS ported up to a newer version. .... Someday.
Here is the final sequence of events.
- CodeBlockA
- CodeBlockB
- CodeBlockC
- DialogForm.Show (non-modal)
- Return to rest of program allowing user to do whatever they want. Certain parts of program are disabled while DialogForm is active
- When DialogForm is closed --> HandleDialogClose handler called
- CallbackData.PopThenCallProc calls HandleCodeBlockBCallback
- CallbackData.PopThenCallProc calls HandleCodeBlockACallback
- (some additional cleanup stuff called to ensure DialogForm is closed etc)
Anyway, it works. I'm sure there is an easier way and I will hit my head at making this overly complicated. But you have to start somewhere!
Thanks for reading
Kevin