Many-to-many checkboxes revisited, part 10: A summary before moving on
by Unknown user
I don't think I've ever written a ten part series before, but then again there aren't that many topics in Clarion programming that catch my interest quite as much as moving business logic out of embed points and into testable, reusable, maintainable code.
It's true that this code already was a class in its first iteration, but another subject that's close to my heart is refactoring code to make it more testable, reusable and maintainable. And the principle is still the same - take that business logic and and break it up into as many classes as makes sense. For simple logic, often a single class will do. But more often than not I find even code that looks fairly simple at the start can easily be broken down into two or more classes.Â
A code walkthrough
I've already talked in some detail about the individual classes, so in this installment I'll focus on the flow of code through the program. I've also included a download of all the source you need to build the sample program for yourself (just in case you don't want to get it from the ClarionMagLibrary Git repository).
Here's the running program:
And here's the full source for that program:
PROGRAM MAP END include('CML_UI_ListCheckbox.inc'),once include('CML_Data_ManyToManyLinks.inc'),once include('CML_Data_ManyToManyLinksPersisterForTPS.inc'),once include('CML_System_Diagnostics_Logger.inc'),once include('keycodes.clw'),once dbg CML_System_Diagnostics_Logger x long CheckboxQueue queue Checked bool CheckedIcon long CheckedText string(30) ID long end ListCheckbox CML_UI_ListCheckbox Links CML_Data_ManyToManyLinks TpsPersister CML_Data_ManyToManyLinksPersisterForTPS WINDOW WINDOW('UITest'),AT(,,179,247),GRAY,FONT('Microsoft Sans Serif',8) LIST,AT(9,10,161,185),USE(?List),VSCROLL,FROM(CheckboxQueue), | FORMAT('14L(2)|M~X~100L(2)|M~Text~') BUTTON('Check All'),AT(8,199,47,14),USE(?ButtonCheckAll) BUTTON('Uncheck All'),AT(66,199,47,14),USE(?ButtonUncheckAll) BUTTON('Close'),AT(124,226,47),USE(?ButtonClose),STD(STD:Close) BUTTON('Toggle All'),AT(124,199,47,14),USE(?ButtonToggleAll) END CODE loop x = 1 to 100 clear(CheckboxQueue) CheckboxQueue.CheckedText = 'String ' & x CheckboxQueue.ID = x add(CheckboxQueue) end open(window) TpsPersister.SetFilename('LinksData') Links.Persister &= TpsPersister Links.LeftRecordID = 1 Links.Load() ListCheckbox.ToggleAndAdvanceWithSpaceKey = true ListCheckbox.Initialize(CheckboxQueue,CheckboxQueue.CheckedIcon,CheckboxQueue.ID,?List,,links) ListCheckbox.LoadCheckboxData() accept case accepted() of ?ButtonCheckAll ListCheckbox.CheckAll() of ?ButtonUncheckAll ListCheckbox.UncheckAll() of ?ButtonToggleAll ListCheckbox.ToggleAll() end end close(window) Links.Save()
Let's walk through the code.Â
First up, the queue declaration:
CheckboxQueue queue Checked bool CheckedIcon long CheckedText string(30) ID long end
The first field in the queue is a BOOL (which is actually SIGNED, which is actually LONG, but the idea is it's a true/false flag). This field is not actually displayed; instead an icon is displayed because the list format string says that the first field is really an icon, and when a field is an icon the field immediately following it needs to be a long which indicates which icon will be displayed, in this case a checked checkbox or an unchecked checkbox. More on how that works in a moment.Â
The third field in the queue is really the second field you see in the actual list box - the descriptive text.Â
The last field is not displayed, and it contains the ID of the "right side" record. The idea is you have one record in memory (the "left side" record) and you want to know to which "right side" records this one record is linked. For example in the case of a student who is attending a number of classes, the student ID is the left side ID, and the class ID is the right side ID. The idea of which is left and which is right is somewhat arbitrary.Â
The code next declares some object instances:
ListCheckbox CML_UI_ListCheckbox Links CML_Data_ManyToManyLinks TpsPersister CML_Data_ManyToManyLinksPersisterForTPS
If I wanted to create these instances at runtime I would declare them as reference variables and then add statements in the code to create new instances and assign them to those references. I would also have to dispose of the references when done to avoid memory leaks. In fact when I'm working with classes I find I create instances on the fly far more often than I declare them as in this example. But if you can get away with it, letting the compiler create (and dispose) your classes does make life a little simpler and avoids the problem of accidentally referencing a null reference variable and crashing the program.Â
There's a bit of code at the beginning of the procedure that loads 100 records into the queue so there's some data to play with. At this point none of the records is marked as checked. In the original version of this UITest program I Â randomly set some checked values, but that isn't necessary now as any previously checked values will be loaded up when the program runs.Â
Next I open the window, then set up the objects I need to manage the checkboxes:
TpsPersister.SetFilename('LinksData') Links.Persister &= TpsPersister Links.LeftRecordID = 1 Links.Load() ListCheckbox.ToggleAndAdvanceWithSpaceKey = true ListCheckbox.Initialize(CheckboxQueue,CheckboxQueue.CheckedIcon,CheckboxQueue.ID,?List,,links) ListCheckbox.LoadCheckboxData()
TpsPersister is a class I created more as a test support than anything else. In an upcoming installment I'll create something a bit more flexible that can be used with your existing database. But for now TpsPersister is adequate. I assign the persister instance to the Links object (which maintains the state of links between the "left" and "right" lists in memory). Since I'm just using this code in a demo I fix the "left" record ID to a value  of 1 (although the default value of 0 would work just as well here).Â
I then Load() any saved linking data. The Load() method looks to see if it has a reference to a persister object, and if it does have a reference it calls that object's Load() method. In fact the reference is of type CML_Data_ManyToManyLinksPersister, so I can plug in any class derived from that class, such as CML_Data_ManyToManyLinksPersisterForTPS.Â
The ListCheckBox object is the one that handles user interface functionality; it has a property that will toggle the selection and advance the list box selector bar, but at present this only works with simple list controls, not with ABC browses. It's fun, so I left it in.Â
Next is an Initialize call, passing in the various queues, controls, and the Links object. And it occurs to me as I look at this code that I have an explicit assignment of the persister object, but I pass the links object as a parameter and assign it in the method. Hmm. It would be better to be consistent, and I'm leaning towards passing the object as a parameter in both cases.Â
Finally (in this code block) there's a call to LoadCheckboxData(). But this also seems odd at first. Why do I have a Load() method earlier and the a LoadCheckboxData() method here?Â
The Load() method executes on the object that keeps the linking data in memory; its job is to go to the database and load up any data that links this left side record to these right side records.Â
The LoadCheckboxData() method doesn't care about the database. It simply says "I have these queue records - show me which are linked."Â
Perhaps more descriptive method names would beÂ
LoadAllLinkingData
and
LoadDisplayableCheckboxData
Yes, I think that's an improvement:
TpsPersister.SetFilename('LinksData') Links.SetPersister(TpsPersister) Links.LeftRecordID = 1 Links.LoadAllLinkingData() ListCheckbox.ToggleAndAdvanceWithSpaceKey = true ListCheckbox.Initialize(CheckboxQueue,CheckboxQueue.CheckedIcon,CheckboxQueue.ID,?List,,links) ListCheckbox.LoadDisplayableCheckboxData()
In this example there will only be one call to each Load... method, because all of the data I want to view is always in the queue. But I want to be able to use this code with a page loaded browse, and I've decided to load up all of the linking data from the database and then apply just the relevant data for each page of data.Â
Separating the UI from the link management from the database this way has another benefit: in the original class every time I made a change I had to write that change out to the database; in this version I can maintain all of the linking data in memory until such time as I decide I want to commit the changes. I don't have to commit all at once - I can always call the Save() method after every change if that's what I want to do, and only the one changed record will be written or deleted.Â
There's really only one more line of code that's needed, and that's a call to the Links.Save() method. But for consistency I'm going to rename that to SaveAllLinkingData.Â
The source download is below. Just load up either the .sln or the .cwproj file, compile and run.Â
NOTE: The zip does include the CML classes used by this example, so if at some later date you want to compile this with the latest version you should delete all the CML_* files from the directory.Â
As I said earlier, the TPS persister is more of test support than a really useful class. When it's time to integrate this code into an app I'll almost certainly want to use a file declared in that app.Â
But before I get to that point I think it's time to try attaching this code to an existing ABC browse. I'll dive into that next time.Â