Managing multiple selection list boxes on a form

Amidst the spirited discussion about dots in form object names is a post about multiple entity selection or collection list boxes on a form. I thought I’d share an idea I’ve been working with that is showing promise dealing with these.

My goal is a consistent naming convention. The list boxes are very cool in terms of the selected items, position and current item properties (or data sources) but naming these things can get confusing quickly. And there’s the question of the form object name (the list box widget on the form). I think using dots is a bad idea for form object, BTW. So what’s better?

First I adopted a few rules:

  1. The form object name will end with “_LB”
  2. I’ll create a Form object with that name for each list box
  3. This object will have 4 properties:
  • data
  • curItem
  • pos
  • selected

In practice it looks like this:

A very simple method makes set up easy:

// LB_init_obj ()
// ------------------
// Purpose: init a listbox object on Form
C_OBJECT ( $0 )

$0:= New object (\
"data"; Null;\
"curItem"; Null;\
"pos";0;\
"selected"; Null)

which in practice looks like:

Case of 
	: ($form_event=On Load)
		Form.docs_LB:=LB_init_obj
		...
End case 

Now I have a consistent naming convention. When I know the name of the listbox form object I know the name of all the important parts. Since all those parts are in an object I can write various methods to operate on that object without even knowing the name of the listbox.

The only tradeoff is the fixed name of the data. Here is that same listbox in action:

Clicking on a row on the left selects that record which has a number of related Documents as well as a list of keywords in a data field. So the contents of the Document listbox are:

Form.cells_LB.curItem.docs

All that just happens when the row is clicked. If I want to stick with no coding to make this work I have to accept using that non-conforming name. However, it’s very simple to simply assign that reference to my convention in the cells_LB:

Case of 
	: (FORM Event.code=On Selection Change)
		Form.docs_LB.data:=Form.cells_LB.curItem.docs
End case

If you are thinking this approach leads to rapidly increasing the amount of memory used remember that we are working with references. The memory footprint is very small even when you use these tricks on multiple, large collections or entity selections. And the responsiveness is imperceptible.

To show how nicely this cleans up the listbox issues here’s a debugger screen shot:

3 Likes

I use a similar convension although the names are a bit different, so I agree 100% that this design pattern is really practical.

One minor difference is that I also have a meta property. As documented, it is more efficient to use a preexisting meta object than to call New object each time the expression is invoked, which would create identical objects most of the time, only to dispose them right away.

1 Like

That’s great. Thanks for pointing it out. I’ve been so focused on data oriented stuff I haven’t even look at this.

Miyako,
This is a great tip. I finally got around to working with it. This is a v17.4 project and noticed a couple of things. (For anyone not familiar with what we are discussing check out the Text Theme near the bottom of the page Miyako links.)

Two things: first, I could not get the Form.meta object to work unless it was used directly For instance, this works:

		Form.meta.fontWeight:="normal"
		
		If ($o.repeat)  // a repeat
			Form.meta.fontWeight:="bold"
			Form.meta.stroke:="#ff0000"
			Form.meta.fontStyle:="italic"
		Else 
			Form.meta.stroke:="#090909"
			Form.meta.fontStyle:="normal"
		End if 
		
		$0:=Form.meta

but this doesn’t:

		$return_o:=Form.meta  // New object
		$return_o.fontWeight:="normal"
		
		If ($o.repeat)  // a repeat
			$return_o.fontWeight:="bold"
			$return_o.stroke:="#ff0000"
			$return_o.fontStyle:="italic"
		Else 
			$return_o.stroke:="#090909"
			$return_o.fontStyle:="normal"
		End if 
		
		$0:=$return_o

When it runs it seems like only the last iteration is applied to the entire listbox.
I’m just wondering why there is a difference.

Can you talk a little more about this? I have been completely profligate in my creation of objects to use as parameters and return objects. But it makes sense there is some overhead involved with them beyond simple parameters and return objects. But perhaps it’s more effort to optimize them than accept the overhead? I could imagine a listbox of thousands of lines could present considerations. What are some guidelines or limits to keep in mind?

I’m mindful of how frequently lots of 4D code gets written to optimize something only to create problems later on…

I am sorry that is not what I meant by “using the same object”. If you modify the “meta” object directly, you mutate the source.

Suppose you only have 2 possibilities, “selected” or “not selected”. Then it makes sense to have 2 meta objects, not 100,000 meta objects for 100,000 lines, right? That is what I meant, 1 object per colour combination.

In your example, you have 1 object for all colour schemes. That won’t work, because by changing the meta (metum…?) you alter all the meta (metae…?).

Here is an example:

Notice in the form method (oh I love that we can now link directly to the code online) I have a pair of meta objects:

Form.servers.metas:=New collection(New object;New object)
Form.servers.metas[0].stroke:="#F5EEF8"
Form.servers.metas[0].fill:="#633974"
Form.servers.metas[1].stroke:="#633974"
Form.servers.metas[1].fill:="#F5EEF8"

And the listbox expression is like

meta_dialog (Form.servers.sel;Form.servers.metas[0];Form.servers.metas[1])

Which calls

C_COLLECTION($1)
C_OBJECT($2;$3;$0)

If ($1.indexOf(This)#-1)
	$0:=$2
Else 
	$0:=$3
End if 

So the expression is just alternating between 2 existing objects.

Note: This is a “single select” listbox that displays less than 10 lines. “indexOf” can be expensive so this part of the code is not scalable. But having as many meta objects as the number of colour combinations is always practical,

Thanks - I get it now.
And that is quicker.

Great idea, Kirk.

One small problem: if the listbox is not in the main form but in a subform, it doesn’t work. For listboxes in subforms, 4D will maintain Form.box.selected but will ignore Form.box.curItem and Form.box.pos.

If you set up the form to use process variables, on the other hand, it works fine. Weird. If I’d not been slapped down so often recently, I’d call it a bug.

Jeremy

Is the subform’s data source an object variable? Form in the context of a subform is its bound variable in the parent form, but only if the data source is an object variable.

Miyako,

Yes, it is. I can send you a small sample database which demonstrates the issue, if you’re interested.

Jeremy

Miyako,

I’ve made a small (35k zipped) demo database, in v18R2, and put it here: https://www.dropbox.com/s/ls8x3n91u2qwu4h/test.4dbase.zip?dl=1

Form BoxIsInMain contains an entity listbox, data in Form.box.data and control variables in Form.box.current, Form.box.pos and Form.box.sel. If you run the method RunInMain and click on a line in the listbox, you’ll drop into the debugger by TRACE and you’ll see that the current, pos and sel properties of Form.box are correctly set.

Form BoxIsInSubform contains BoxIsInMain as a subform. If you run the method RunInSubform and click on a line in the listbox, you’ll drop into the debugger by TRACE at the same place. Here, though, you’ll see that while the sel property of Form.box is correct, the current and pos properties don’t exist. (Creating them in code when setting up the listbox doesn’t help: they will exist but will remain empty and zero respectively).

Using process variables instead of properties of Form.box works fine in the subform, so that’s an easy workaround.

Am I missing something?

Jeremy

It looks like a bug.

The problem seems to be, that Form is not evaluated correctly in a listbox in a subform. I say that, because OBJECT Get pointer(Object subform container) which is identical to Form in this context, works.

download

Maybe it all works if the subform bound variable was not an object, thus decoupling it from Form.

Miyako,

Thanks very much. Should I try to report it as a bug, or could you do it? I’m not a partner, so I suspect it will carry more weight coming from you.

By the way, the hypothesis in your last sentence is correct. If I change the subform variable to type Text, all properties of Form.box are correctly evaluated in the subform. For my purposes, though, that’s not a viable option (at least, it’s a less viable option than using process variables until the bug is fixed).

Jeremy

Not really. reports from customers always have priority. But my responsibility is to help 4D customers in Japan, as they have no access to customer support in English or French.

Jeremy,
Let me propose a different approach for you to consider. First, instead of using process variables just use Form.

In RunInSubform you pass an object. This is a good idea. I defined it and added an empty object named subform. Next I assigned the expression for the subform to Form.subform - or the object I just defined.

Finally I changed your listbox to populate the list On load rather than On bound variable change. I don’t find On bound variable change reliable working with subforms that are linked to an object on the parent Form as I am doing. Changing a value of a property won’t trigger the On bound variable change event. I never use process variables like this so that may work more reliably. I don’t know.

The effect is the same - the listbox populates when the subform loads. And as you see the pos value is correct.

Kirk,

Thanks - that’s very interesting. Leaving aside the phase in which you populate the data (I take your point, but it’s obviously nothing to do with the bug), the only difference between your code (which works) and mine (which doesn’t) is that you use an object you define yourself as the listbox variable, whereas I use an automatic form variable (sorry if my terminology is bad): is that correct?

I’ve reported the issue as a bug, which I think it clearly is.

Jeremy