Many-to-many checkboxes revisited, part 9: From user input to persister and back
by Unknown user
When I sat down to add all the new persistence code into my UI example program I realized that while I could load data from the persister into the ManyToManyLinks instance and have those checkboxes appear checked in my listbox, I had no way of communicating the user's selection of a record back to the ManyToManyLinks instance.
There are several places in the CML_UI_ListCheckbox code where I update the queue on the user taking an action, so in each of those places I added a call to a new SetManyToManyLinkForCurrentQRecord method:
CML_UI_ListCheckbox.SetManyToManyLinkForCurrentQRecord procedure
code
if not self.ManyToManyLinks &= NULL
if self.ListQIconField = CML_UI_ListCheckbox_TrueValue
self.ManyToManyLinks.SetLinkTo(self.ListQRightRecordID)
else
self.ManyToManyLinks.ClearLinkTo(self.ListQRightRecordID)
end
end
This method looked great but it didn't compile, because while I already have a SetLinkTo method, I don't have a ClearLinkTo method. That was easy enough to fix, although for symmetry with the SetLink* methods I added two ClearLink* methods:
CML_Data_ManyToManyLinks.ClearLinkTo procedure(long rightRecordID)
code
self.ClearLinkBetween(self.LeftRecordID,rightRecordID)
CML_Data_ManyToManyLinks.ClearLinkBetween procedure(long leftRecordID,long rightRecordID)
code
clear(self.LinksData.LinksQ)
self.LinksData.LinksQ.LeftRecordID = LeftRecordID
self.LinksData.LinksQ.RightRecordID = rightRecordID
get(self.LinksData.LinksQ,self.LinksData.LinksQ.LeftRecordID,self.LinksData.LinksQ.RightRecordID)
if not errorcode() then delete(self.linksData.LinksQ).
But I had another problem: I didn't have a self.ListQRightRecordID property, because none of my UI code had had to deal with this requirement before. That meant adding an ID field to the queue:
CheckboxQueue queue
Checked bool
CheckedIcon long
CheckedText string(30)
ID long
end
and adding a property to the CML_UI_ListCheckbox class and changing the Initialize prototype:
CML_UI_ListCheckbox Class,Type,Module('CML_UI_ListCheckbox.CLW'),Link('CML_UI_ListCheckbox.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
ToggleAndAdvanceWithSpaceKey bool
ListFEQ long
ListQ &queue
ListQCheckboxFieldNumber long
ListQIconField &long
ListQRightRecordID &long
...
Initialize procedure(*Queue listQ,*long listQIconField,*long listQRightRecordID, long listFEQ,long listQCheckboxFieldNumber=1,CML_Data_ManyToManyLinks ManyToManyLinks)
...
and then calling the Initialize method, passing the new field:
ListCheckbox.Initialize(CheckboxQueue,CheckboxQueue.CheckedIcon,CheckboxQueue.ID,?List,,links)
With these changes I could run my UITest program and have my checkbox settings loaded on startup and saved on exit with the Links.Load() and Links.Save() method calls.
Cleaning up the code
But there were still a few things that bothered me about the code. For one, while I'd created a class to maintain the queue of record associations, the class really didn't add any value. It was simply a container for the class. So I got rid of the class and declared the queue type in CML_Data_ManyToManyLinksDataQ.inc:
CML_Data_ManyToManyLinksDataQ queue,TYPE
LeftRecordID long
RightRecordID long
end
Everywhere I had previously used the class and the queue reference, I now used just the queue reference. That took a bit of searching and replacing, and when I was done I ran my unit tests again to make sure I hadn't messed anything up.
Storing data efficiently
Another thing I didn't like about the class was the brute force approach to saving data. If I had fifty linking records and I removed one of them, why not just remove that single record instead of wiping out all the records and recreating all but one?
To do that I needed to change CML_Data_ManyToManyLinksDataQ so I could track the state of a link in the list box as well as the presence of a link in the database:
CML_Data_ManyToManyLinksDataQ queue,TYPE
LeftRecordID long
RightRecordID long
IsLinked bool
IsPersisted bool
end
That meant my ClearLinkBetween method could no longer assume the mere presence of a linking record. I changed the code to:
CML_Data_ManyToManyLinks.ClearLinkBetween procedure(long leftRecordID,long rightRecordID)
code
if self.GetLinkRecord(leftRecordID,rightRecordID)
self.LinksDataQ.IsLinked = FALSE
put(self.LinksDataQ)
end
Similarly I changed IsLinkBetween to:
CML_Data_ManyToManyLinks.IsLinkBetween procedure(long leftRecordID,long rightRecordID)!,bool
code
if self.GetLinkRecord(LeftRecordID,RightRecordID)
if self.LinksDataQ.IsLinked then return true.
end
return false
and SetLinkBetween to:
CML_Data_ManyToManyLinks.SetLinkBetween procedure(long leftRecordID,long rightRecordID)
code
if not self.GetLinkRecord(LeftRecordID,RightRecordID)
clear(self.LinksDataQ)
self.LinksDataQ.LeftRecordID = LeftRecordID
self.LinksDataQ.RightRecordID = rightRecordID
self.LinksDataQ.IsLinked = true
add(self.LinksDataQ)
else
self.LinksDataQ.IsLinked = true
put(self.LinksDataQ)
end
Both methods need to look for an existing queue record, which is why I extracted that code into a GetLinkRecord method:
CML_Data_ManyToManyLinks.GetLinkRecord procedure(long leftRecordID,long rightRecordID)!,bool
code
clear(self.LinksDataQ)
self.LinksDataQ.LeftRecordID = LeftRecordID
self.LinksDataQ.RightRecordID = rightRecordID
get(self.LinksDataQ,self.LinksDataQ.LeftRecordID,self.LinksDataQ.RightRecordID)
if not errorcode() then return true.
return false
The CML_Data_ManyToManyLinksPersister's Load method remains the same, but the Save method looks quite a bit different:
CML_Data_ManyToManyLinksPersister.Save procedure(long leftRecordID,CML_Data_ManyToManyLinksDataQ linksDataQ)
x long
code
if self.OpenDataFile()
dbg.write('CML_Data_ManyToManyLinksPersister.Save')
loop x = 1 to records(linksDataQ)
get(LinksDataQ,x)
if LinksDataQ.IsLinked and not LinksDataQ.IsPersisted
self.AddLinkRecord(LinksDataQ.LeftRecordID,LinksDataQ.RightRecordID)
LinksDataQ.IsPersisted = true
put(linksDataQ)
elsif not LinksDataQ.IsLinked and LinksDataQ.IsPersisted
self.RemoveLinkRecord(LinksDataQ.LeftRecordID,LinksDataQ.RightRecordID)
LinksDataQ.IsPersisted = false
put(linksDataQ)
end
end
self.CloseDataFile()
end
This code loops through the queue records and if a record is linked but not persisted it adds a linking record, and if a record is not linked but is persisted it removes the linking record. It also sets the IsPersisted flag to the current state as appropriate so the next time around the Save call will not attempt to re-save or re-remove a record.
The AddLinkRecord and RemoveLinkRecord methods are virtuals, and are just stubs in CML_Data_ManyToManyLinksPersister. They are implemented in CML_Data_ManyToManyLinksPersisterForTPS:
CML_Data_ManyToManyLinksPersisterForTPS.AddLinkRecord procedure(long leftRecordID,long rightRecordID)!,derived
code
dbg.write('AddLinkRecord(' & leftRecordID & ',' & rightRecordID & ')')
clear(Links:Record)
Links:LeftRecordID = LeftRecordID
Links:RightRecordID = rightRecordID
add(LinksDataFile)
if errorcode()
dbg.write('Error adding record: ' & error())
else
dbg.write('Success adding record')
end
CML_Data_ManyToManyLinksPersisterForTPS.RemoveLinkRecord procedure(long leftRecordID,long rightRecordID)!,derived
code
dbg.write('RemoveLinkRecord(' & leftRecordID & ',' & rightRecordID & ')')
clear(Links:Record)
Links:LeftRecordID = LeftRecordID
Links:RightRecordID = rightRecordID
get(LinksDataFile,Links:kLeftRight)
if not errorcode()
dbg.write('deleting record')
delete(LinksDataFile)
end
There's still work to be done here, not least of which is adding some measure of error handling. But this code does work. The unit tests pass, and in my UITest program my checkbox settings are preserved between runs.
Next time, a summary of the work done to date.