Many-to-many checkboxes revisited, part 3: More UI code

by Unknown user

Read Part 2

Moving on with the UI code, I'd really like this class to handle the clicking events automatically, without my having to add in any code in the Accept loop of whatever procedure uses the class. 

The original class uses ABC to insert that code. It's derived from the ABC BrowseClass, and overrides that class's TakeEvent method (which gets called somewhere in the bowels of ABC):

cciBrowseClassB.TakeEvent                       PROCEDURE
    code
    parent.TakeEvent()
    self.DebugMsg('field ' & field() & ', event ' & event() & ', keycode ' & keycode())
    if field() = self.lc
        if event() = event:Accepted |
            and keycode() = MouseLeft |
            and self.lc{proplist:mouseuprow} = self.lc{proplist:mousedownrow} |
            and self.lc{proplist:mouseupfield} = self.lc{proplist:mousedownfield} |
            and self.lc{proplist:mousedownfield} = 1
            self.debugmsg('clicked on checkbox')
!         ! Get the current record
!         self.UpdateViewRecord()
!         ! Update the buffer
!         self.UpdateBuffer()
            get(self.ViewQ,choice(self.lc))
            self.DataQ.ID        = self.ViewQRightField
            get(self.DataQ,self.DataQ.ID)
            if errorcode()
                !self.debugmsg('no dataq record for id ' & self.ViewQRightField & ', adding now')
                self.DataQ.ID        = self.RightPrimaryID
                self.DataQ.CurrValue = 1
                self.DataQ.Changed   = true
                add(self.DataQ,self.DataQ.ID)
            else
                !self.debugmsg('found dataq record for id ' & self.ViewQRightField & ', value was ' & self.DataQ.CurrValue)
                self.DataQ.CurrValue = choose(self.DataQ.CurrValue = 1,0,1)
                self.debugmsg('setting to ' & self.DataQ.CurrValue)
                self.DataQ.Changed   = true
                put(self.DataQ)
            end
            self.setIcons()
            self.SaveCheckboxData()
        end
    end

I'm no longer deriving from ABC classes, at least for the UI code. So I have to find another mechanism. One option is hand code a method call inside the Accept loop; another is to write a template to insert that code. I've done both in the past, but in recent years I've tended to replace those options with an event handler I install using the Register function.

 The Clarion Help has this to say about Register:

REGISTER( event, handler, object [,window] [,control] )

 

REGISTER

Registers an event handling procedure.

event

An integer constant, variable, expression, or EQUATE containing an event number. A value in the range 400h to 0FFFh is a User-defined event.

handler

A LONG variable, or expression containing the return value from ADDRESS for the PROCEDURE to handle the event.

object

A LONG integer constant, variable, or expression containing any 32-bit unique value to identify the specific handler. This is generally the return value of ADDRESS(SELF) when the handler is a CLASS method.

window

The label of the WINDOW or REPORT whose event to handle. If omitted, the current target WINDOW or REPORT is assumed.

control

An integer constant, EQUATE, variable, or expression containing the field number of the specific control whose event to handle. If omitted, the event is handled for every control on the window.

Can also be prototyped as REGISTEREVENT.

REGISTER registers an event handler PROCEDURE called internally by the currently active ACCEPT loop of the specified window whenever the specified event occurs. This may be a User-defined event, or any other event. User-defined event numbers can be defined as any integer between 400h and 0FFFh.

I particularly like that I can use Register not just with procedures but also with class methods (provided I also pass in the class's address).

I added this line of code to my Initialize method to register my class's TakeMouseClick method as a handler for the Accepted event:

    register(Event:accepted,address(self.TakeMouseClick),address(self))

Here's the code for TakeMouseClick():

CML_UI_ListCheckbox.TakeMouseClick              procedure
    code
    if field() = self.ListFEQ
        if keycode() = MouseLeft |
            and self.ListFEQ{proplist:mouseuprow} = self.ListFEQ{proplist:mousedownrow} |
            and self.ListFEQ{proplist:mouseupfield} = self.ListFEQ{proplist:mousedownfield} |
            and self.ListFEQ{proplist:mousedownfield} = self.ListQCheckboxFieldNumber
            get(self.ListQ,choice(self.ListFEQ))
            self.ListQIconField = choose(self.ListQIconField=TrueValue,FalseValue,TrueValue)
            put(self.ListQ)
        end
    end

That is pretty straightforward code: if this is the correct list box and the keycode is a mouse click and the mouse was clicked and released on the correct column and the current row, get the queue record and flip its value. 

But wait - I've introduced some new properties to enable me to manipulate the queue. I have a property for the queue (ListQ) and a reference to the LONG field that holds the icon setting (ListQIconField). 

And I've also added a property for the number of the queue field that will be used for the checkbox. In my first attempt my code assumed that I would always have the icon field as the first field in the list box, but that isn't a safe assumption. 

To handle all of these fields my Initialize method prototype now looks like this:

Initialize           procedure(*Queue listQ,*long listQIconField,long listFEQ,long listQCheckboxFieldNumber=1)

And here's the code:

CML_UI_ListCheckbox.Initialize                  procedure(*Queue listQ,*long listQIconField,long listFEQ,long listQCheckboxFieldNumber=1)
    code
    self.ListQ &= listQ
    self.ListFEQ = ListFeq
    self.ListQCheckboxFieldNumber = listQCheckboxFieldNumber
    self.ListQIconField &= listQIconField
    self.ListFEQ{PROPLIST:Icon,self.ListQCheckboxFieldNumber} = FalseValue
    self.ListFEQ{PROP:IconList,TrueValue} = '~CML_Checked.ico'
    self.ListFEQ{PROP:IconList,FalseValue} = '~CML_UnChecked.ico'  
    self.ListFEQ{PROPLIST:Picture,self.ListQCheckboxFieldNumber} = '@p p'
    pragma('link (CML_UnChecked.ico)')
    pragma('link (CML_Checked.ico)')
    register(Event:accepted,address(self.TakeMouseClick),address(self))

Just for fun, I added a couple of buttons to the demo, one to check all and one to uncheck all:

These buttons both do something similar - they loop through the queue and set the icon field to true or false. Now, there's no point in having two loops that do almost the same thing. Here are the two methods and the common utility method:

CML_UI_ListCheckbox.CheckAll                    procedure
x                                                   long
    code
    self.SetAll(TrueValue)
    
CML_UI_ListCheckbox.SetAll                    procedure(bool flag)
x                                                   long
    code
    loop x = 1 to records(self.ListQ)
        get(self.ListQ,x)
        self.ListQIconField = flag
        dbg.write('self.ListQIconField = ' & flag)
        put(self.ListQ)
    end
    
    
CML_UI_ListCheckbox.UncheckAll                  procedure
    code
    self.SetAll(FalseValue)

All this class needs to operate is a control and the queue that provides the data to that control (with a Long field following the field that will be used to represent the checkbox). That means you can use it equally well with a hand coded queue as with an ABC browse, even one that is page loaded. 

But so far all I've done is tackle the UI side of things. Next up: Options for persisting the checkbox values in a database

Download the source