![]() |
|
Published 1999-04-01 Printer-friendly version
Im a really a big fan of Clarion. It does almost everything I want to do in a simple and easy manner. Anything that it doesnt do out of the box is usually possible with a little ingenuity and a little template or class. For quite some time now Ive been coveting a feature often found in other programs (most notably Microsoft Outlook). When you are looking at a list of items, be it contacts, messages, notes, etc., you can open it just like you might double-click on an item in a Clarion browse. The corresponding form appears, as expected.
The difference, however, is that Outlooks update form is independent of its list of items. While the form is still open, you can return to the list to select another item for update, or whatever else you desire. Once you finish with the form, the instigating list is updated (assuming that it is still open itself). Youll also notice that each update window is represented as a different program in your task bar.
My goal was to get something similar to this in a regular Clarion application, without turning the world upside down. I realized that having the form appear as a different program on the task bar would probably require that the Form be in a different EXE, and communicate with the calling browse via some kind of complicated inter-window messaging. (Im sure there are other ways, but they all seemed intimidating and disrupting.) I was willing to forego this taskbar luxury (and hardship), as long as the form operated independently of the browse. For me it was sufficient to have the form appear as a separate entry on the standard Window menu of my main frame.
Given this, I had to decide how to achieve it. As I mentioned earlier, I didnt want to do everything differently. I still wanted to use Clarions regular BrowseUpdateButtons template on the Browse, and SaveButton template on the Form. I also felt that Clarions own inter-thread communication would be sufficient for my needs. I decided to start my journey by revisiting Clarions Browse/Form interaction.
In a normal situation, the user indicates that they want to perform an update from a browse. The browse reads the record into memory (or initializes it for an insert), then it sets aGlobalRequest variable and calls the form. The form executes and performs the action requested by the browse. This is all nice and simple. (If you trace through the code with debugger, it seems a little more convoluted. There is a plethora of methods that get called to handle the user interaction, reading the record, calling the form, and handling the response from the form. We can ignore much of this, though.)
The biggest roadblock was the multi-threaded aspect. I intended to useSTART to execute the form instead of calling it directly. This meant that it would be on a different thread than the browse, with a different version ofGlobalRequest andGlobalResponse, and a different copy of the files record buffer and pointer.
I felt that the best way to handle this would be to use a few non-threaded global variables. These would not have the THREAD attribute, which means that all executing threads share the same copy. Thats all fine and dandy withGlobalRequest, but handling the file in this manner wouldnt be quite that simple Or would it? It turns out that theFileManager object is also non-threaded. This means that the browse can callAccess:Primary.SaveFile, and store the ID in a non-threaded global variable, then the form can use this value to pass intoAccess:Primary.RestoreFile. The final thing that I needed to pass was the thread number of the calling browse, so that the form could communicate with the browse thread when it was done. (The browse can determine the forms thread by capturing the return value of theSTART command.)
Now that I had a basic plan, I had to decide on the best approach. If youve been an avid reader of my articles over the years, youll know that if Im planning on doing something a bunch of times, then Ill do it with a template (sometimes in conjunction with a class). This is no exception. Considering that I might want to apply this to every single browse/form combination in my application, Im certainly not going to hand-code the solution.
In this particular case I did something rather rare. Normally I would prototype a solution in hand-code, then transfer it to a template once I felt that I fully understood it. This time, however, I decided to jump directly into the template. It turns out that this was not a mistake, as the template was simple and worked the first time (to my surprise and delight). I should stress, though, that this is not normally the best approach, and you should usually test your potential solution using hand-code before trying to templatize it. (a.k.a. Dont try this at home, folks. <g>)
I decided that three templates were needed: one global extension to provide the global variables, one that would attach itself to the BrowseUpdateButtons control template, and one to go with the forms SaveButton control template. In case youve forgotten, you attach one template to another by having the new templateREQ(ExistingTemplate(OtherChain)). The browse and form procedures might actually be in different DLL APPs, so the global extension should be placed wherever there is one of our new browse or form templates, as well as in the base DLL.
Before I start getting into the meat and potatoes of each template, there is one other issue that we must consider. As I mentioned earlier, the form must tell the browse when its finished. However, the browse may have been closed before the form, in which case we dont want to post a "completed" event to a nonexistent thread (or worse, a new procedure utilizing the same thread number as the old browse). To alleviate this well create a global queue that contains all browse+form pairs currently open. The form will be responsible for adding and deleting its entry in the queue, and if the browse exits it will clear all of its entries. That way the form can check if the browse is still open before posting the event. This global, non-threaded queue must also created by the global extension.
Our global extension template is responsible only for the population of variables to be used by the browse and form templates. It must be aware of whether the variables should be defined as local to this APP, or declared as external in another DLL. If the APP is a "base" DLL, then it must export the variables for sharing with other APPs above. This may sound complicated, but is really much easier to understand when you look at the template code. The header of the procedure is nothing special:
#EXTENSION(mhThreadedBFGlobal,''),APPLICATION
#BOXED('Mike Hanson''s Public Domain Templates')
#DISPLAY('Threaded Browse+Form Global Support')
#ENDBOXED
Note that Ive omitted the descriptions from the first line to shorten it here. Also notice that there is not one#PROMPT here, as the template doesnt need to be configured beyond its mere placement. Now we have to generate the variables:
#AT(%GlobalData)
#IF(%GlobalExternal)
#SET(%ValueConstruct, ',EXTERNAL,DLL(dll_mode)')
#ELSE
#CLEAR(%ValueConstruct)
#ENDIFMH::TBF::EVENT:Completed EQUATE(EVENT:User+999)
MH::TBF::Caller BYTE%ValueConstruct
MH::TBF::SaveID USHORT%ValueConstruct
MH::TBF::Request BYTE%ValueConstruct
MH::TBF::Queue QUEUE%ValueConstruct
Browse BYTE
Form BYTE
END!QUEUE
#ENDAT
Ive prefixed all of my labels with "MH::TBF::". This prevents the variable names from conflicting with all of the other labels in the system. The%GlobalExternal template token indicates whether this is the base APP, or whether it sits on top of another (requiring that variable definitions be flagged with theEXTERNAL,DLL(dll_mode) attributes. These external attributes are placed after each variable, except inner variables of a group or queue. Youll also see the definition of our event equate. The template simply assumes thatEVENT:User+999 will not be used anywhere else in your APP. Now lets deal with exporting these from a DLL:
#AT(%DLLExportList),WHERE(%ProgramExtension='DLL' AND ~%GlobalExternal) $MH::TBF::Caller $MH::TBF::SaveID $MH::TBF::Request TYPE$MH::TBF::Queue TCB$MH::TBF::Queue $MH::TBF::Queue #ENDAT
Note that this bit of code is generated only if the current app is a DLL, and it is the base APP. The variables are exported by name with a preceding dollar sign ($). In the case of the queue, it also requires TYPE and TCB entries to accompany the object itself.
This template will attach itself to the existing BrowseUpdateButtons control template. To add it to the procedure, you must go into the Extensions window, highlight the existing Browse Update template, then press the [Insert] button. Our template header looks like this:
#EXTENSION(mhThreadedBrowse,''),REQ(BrowseUpdateButtons(ABC))
#BOXED('Mike Hanson''s Public Domain Templates')
#DISPLAY('Threaded Browse Support')
#ENDBOXED
Now we need to intercept the regular call to the form, and START it instead (after assigning values to our various global variables, of course). Sometimes one template can change values used by another template, and consequently can change the way the other template generates its code. In this case, unfortunately, we cannot do this. The BrowseUpdateButtons template is going to try to call the form, whether we like it or not. This means that we must get in there right before it has a chance to do this, and handle this operation ourselves.
The ABC templates use theWindowClass.Run(ID,Request) method to handle many outside calls. (Im not really sure why it was done this way, but thats the way it works.) In our case we can use this to our advantage. There is an embed within this generated method called%BeforeProcedureCall, which is where well place our code:
#AT(%BeforeProcedureCall,%UpdateProcedure),PRIORITY(5000) MH::TBF::Caller = THREAD() MH::TBF::SaveID = Access:%Primary.SaveFile() MH::TBF::Request = Request START(%UpdateProcedure) RETURN RequestCancelled #ENDAT
This embed uses the name of the procedure to be called to determine its context, which is why you see%UpdateProcedure in the#AT line. The code saves the browses thread, the current file state, and the requested action. Then it STARTs the update procedure. Finally it returnsRequestCancelled before the method has a chance to call the form normally. You may be wondering why Im usingRequestCancelled; well at this point nothing has changed in the file, so I just want the browse to go back to what it was doing. If the form actually changes the record, it will pass that information along later by posting an event back to the browse. Hence, the following piece of the template:
#AT(%WindowManagerMethodCodeSection,'TakeEvent','(),BYTE'),PRIORITY(2000) IF EVENT() = MH::TBF::EVENT:Completed SELF.Reset(True) RETURN Level:Benign END!IF #ENDAT
When the browse sees our special event, it knows that something has changed and it does a forced refresh. Because its unlikely that this event is processed by any other code, we short circuit the event processing by returningLevel:Benign.
The final aspect of the browse template is to clear the queue entries when the browse is closed (to prevent any orphaned forms from improperly posting threads to nonexistent parent browses):
#AT(%WindowManagerMethodDataSection,'Run','(),BYTE')
MH::TBF::X BYTE,AUTO
#ENDAT
#AT(%WindowManagerMethodCodeSection,'Run','(),BYTE'),PRIORITY(9990)
LOOP MH::TBF::X = RECORDS(MH::TBF::Queue) TO 1 BY -1
GET(MH::TBF::Queue, MH::TBF::X)
ASSERT(~ERRORCODE())
IF MH::TBF::Queue.Browse = THREAD()
DELETE(MH::TBF::Queue)
ASSERT(~ERRORCODE())
END!IF
END!LOOP
#ENDAT
Notice that weve got both data and code being generated into theRun method. This is a differentRun from the one that we used to intercept the call to the form. This one is the wrapper for the entire browse window.
This template has to work closely with the browse template to make things work. The header is just as simple as the browses:
#EXTENSION(mhThreadedForm,''),REQ(SaveButton(ABC))
#BOXED('Mike Hanson''s Public Domain Templates')
#DISPLAY('Threaded Form Support')
#ENDBOXED
The first thing that we need is a couple of local variables:
#AT(%DataSection) MH::TBF::Local:SaveID USHORT(0) MH::TBF::Local:Caller BYTE(0) #ENDAT
Were using non-threaded global variables to pass the state between the browse and form. Theres a chance that other browse+form combinations could be communicating at the same time. Therefore, we must grab the values of these variables as soon as possible, so that we dont get confused with values not intended for us. In the case of theMH::TBF::Request variable, well store that inGlobalRequest, which is threaded. Now we have to do something with these variables:
#AT(%WindowManagerMethodCodeSection,'Run','(),BYTE'),PRIORITY(10)
!-----Check if STARTed from mhThreadedBrowse
IF MH::TBF::Caller
!-----Get Values from Global Variables
MH::TBF::Local:Caller = MH::TBF::Caller; CLEAR(MH::TBF::Caller)
MH::TBF::Local:SaveID = MH::TBF::SaveID; CLEAR(MH::TBF::SaveID)
GlobalRequest = MH::TBF::Request; CLEAR(MH::TBF::Request)
!-----Add B+F ThreadPair to Queue
MH::TBF::Queue.Browse = MH::TBF::Local:Caller
MH::TBF::Queue.Form = THREAD()
ADD(MH::TBF::Queue, MH::TBF::Queue.Browse, MH::TBF::Queue.Form)
ASSERT(~ERRORCODE())
END!IF
#!**********
#PRIORITY(9990)
!-----Check if calling mhThreadedBrowse is still open
IF MH::TBF::Local:Caller
MH::TBF::Queue.Browse = MH::TBF::Local:Caller
MH::TBF::Queue.Form = THREAD()
GET(MH::TBF::Queue, MH::TBF::Queue.Browse, MH::TBF::Queue.Form)
IF ~ERRORCODE()
DELETE(MH::TBF::Queue)
ASSERT(~ERRORCODE())
IF ReturnValue = RequestCompleted
POST(MH::TBF::EVENT:Completed,,MH::TBF::Local:Caller)
END!IF
END!IF
END!IF
#ENDAT
This is a significant chunk of code. It actually does two things: before the#PRIORITY(9990) its setting up the form, and after the#PRIORITY statement the form is exiting. The first thing we do is check to see if the global Caller variable has a value. If not, then this form was called directly. (The form can actually handle both types of calls.) Then we save those volatile global variables in their local equivalents. Finally, we store an entry in the global queue to represent this browse+form combination.
After the#PRIORITY statement, we check to see if were working in a threaded context. If so, then we determine if the calling browse is still open by looking at the global queue. If so, it deletes the queue entry. If the form was completed successfully (and not cancelled), then it posts an event to the parent browse.
The only other thing that the form has to do is to restore the record after the file is opened:
#!AT(%WindowManagerMethodCodeSection,'Init','(),BYTE'),PRIORITY(7510) #!Legacy embed used to prevent other code from slipping in #AT(%AfterFileOpen),PRIORITY(10) IF MH::TBF::Local:Caller Access:%Primary.RestoreFile(MH::TBF::Local:SaveID) END!IF #ENDAT
Notice that were using a legacy embed here rather than the new ABC one. This is because there is no priority that we can use with the Init method to slip the code between the opening of the files and this legacy embed. We wouldnt want another template (or a developer) trying to manipulate the record buffer before it is properly initialized.
Personally, I think this template is really cool. It extends the Clarion Browse+Form metaphor to make it far less modal, without requiring that you completely change the way you work. This is a perfect example of how Clarion can be easily enhanced with the marriage of inspiration and a little effort. See you next time!
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224