Many-to-many checkboxes revisited, part 9: From user input to persister and back

by Unknown user

Read Part 8

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.