Monday, November 15, 2004

Sex and the single list

Okay, the "sex" part was a blatant attempt to court visitors. This has nothing to do with sex at all, I'm just including the word "sex" several times in the title and first paragraph of the article. This is about dialogboxes in the Notes client; specifically, a single ordered-list dialog.

The user will see a single-value listbox, three buttons to manipulate the listbox content, specifically "Move Up", "Move Down" and "Remove", an OS-style field to enter new content, an "Add to list" button, and the usual OK/Cancel pair. With these tools, the user can create and manage the content of a multi-value field, putting the values into any desired order without having to do any cutting, pasting or re-typing. Optionally, ND6 designers can add a "Sort" button, just in case the user wants alphabetical sorting.

In order to present that dialog, we are going to need some hidden stuff as well. We can't get to the choice list for a listbox directly, but we can make the listbox get its values from a field we can get to.

Create a form (or subform, if you prefer) and call it "(OrderedListDialog)". Set the form's background colour to "System" (use the little monitor picture on the colour picker). Why? Well, even though we can do pretty much whatever we want with colours, users expect to see dialog boxes that look like dialog boxes, and "System" does just that.

At the top of the form, create four always-hidden fields. (If these fields are not hidden, the content of the dialogbox may move lower in the dialog window as values are added to the list). The first field should be called "UseNumbers". It will be text, editable, do not select "Allow multiple values", and it will either contain "1" or "0". The second is called "Values", and will be text, editable, and this time do select "Allow muliple values". The third is called "StrippedValues". Set that to text, computed, multi-value and enter the following formula :

@If(UseNumbers = "1"; @Right(Values; ". "); Values)

The fourth and final field is called Numbers. It is text, computed, multi-value, and its formula is a hard-coded number list :

"01" : "02" : "03" : "04" : "05" : "06" : "07" : "08" : "09" : "10" : "11" : "12" : "13" : "14" : "15" : "16" : "17" : "18" : "19" : "20" : "21" : "22" : "23" : "24" : "25" : "26" : "27" : "28" : "29" : "30" : "31" : "32" : "33" : "34" : "35" : "36" : "37" : "38" : "39" : "40" : "41" : "42" : "43" : "44" : "45" : "46" : "47" : "48" : "49" : "50" : "51" : "52" : "53" : "54" : "55" : "56" : "57" : "58" : "59" : "60" : "61" : "62" : "63" : "64" : "65" : "66" : "67" : "68" : "69" : "70" : "71" : "72" : "73" : "74" : "75" : "76" : "77" : "78" : "79" : "80" : "81" : "82" : "83" : "84" : "85" : "86" : "87" : "88" : "89" : "90" : "91" : "92" : "93" : "94" : "95" : "96" : "97" : "98" : "99"

Yes, you could use three-digit numbers and go to 999. This is an R5 compromise; in ND6 you could just as easily use the new @Transform and @Member to add numbers to the results when numbering is required without hard-coding anything (again thanks, Damien) . If you're stuck with a hard-coded list in R5 (as I am right now), then don't just type it — do as I did, write a quick web form (a couple of clicks in TextPad will give you most of the page), create the string in a field using a JavaScript "for", then copy 'n' paste. Laziness is just another word for productvity if you know how to do lazy right.

Now create a table. Make it fixed width, five columns by nine rows. Oh, and make sure that it isn't hidden — users really, really hate blank dialogs. Select the whole table, and set all borders to zero (users don't have to know you're using a table, and only masochists would use layout regions). While everything is still selected, set the font to Default Sans Serif 9 point (smaller text looks better in dialogs, and the default sans-serif is usually the best option).

Go to the second row and merge the three centremost cells. This is where you'll add text such as "Select a value in the list below and use the buttons provided to move your selection." Leave a row blank. Go to the second cell in the fourth row, and merge it with the cell below it. This merged cell will contain the listbox. Create a listbox field called "SortSelection", single value, and make it two inches wide by three inches high (5cm by 7.5 cm), fixed, and set the font to Default Sans Serif 9pt. Select "Use formula for choices", and enter StrippedValues as the formula. Select "Refresh fields on keyword change" and "Refresh choices on document refresh".

Leave a row blank (yes, again) then in the second cell of the seventh row, type "Add New Value : ". In the cell below that, create an editable text field, OS style, two inches wide by 0.250 inches high, fixed, and set the font to Default Sans Serif 9pt. Call the field NewValue.

Now all we need is a bunch of buttons to play with. Go to the fourth column of the fifth row. Create six buttons (Create —> Hotspot —> Button), and put a carriage return between each button so they appear one above the other. Make them all fixed width, 2 inches (5cm) wide. The first three should have their font set to dark grey. Label them "Move Up", "Move Down", "Remove", "Move Up", Move Down", and "Remove".

I know — two inches looks pretty darned wide, but trust me, there is a reason for that. Now (and here's a really, truly kewl trick), go to the <HTML> tab of the button properties, and enter a name value for each (I use greyedMoveUp, greyedMoveDown, greyedRemove, activeMoveUp, activeMoveDown, and activeRemove). What we just did, class, was prepare the dialog for internationalization.

<PopularCultureReference>
<Voice value="Isaac Hayes"/>
<Context value="South Park"/>
<Character value="Chef"/>
Children, you don't have to hand-code a thousand buttons with hide-whens or create a bajillion dialog forms just because LotusScript won't let you change the labels. Use JavaScript on your sweet lady dialog, and make love to her all night long. I have a song that might help...
</PopularCultureReference>

Hey, I guess this is about sex after all! One of the few really useful things about the Notes client JavaScript object model is that buttons are accessible via document.forms[0].elements['ButtonName'], and that the .value of each is the button label text. You can change the value at run-time from the form's onload event (or anywhere else that makes sense). The English labels may fit neatly inside a 1.1-inch wide button set, but you'll need some room for French, Spanish or German.

<Rant>That little discovery was made entirely by accident, previewing a form meant for the web in the Notes client. It's not documented anywhere except in a couple of postings I made on the developerWorks fora (Notes.Net), but dammit, it should be. In big, bold, red letters on the default opening page of Designer Help. I wonder how much time and effort has been wasted by developers creating multilingual applications because stuff like this isn't documented. It's not just dev time, you also create a maintenance nightmare when you have several parallel forms/pages that have to stay in sync, or even several buttons that should be identical except for the label on the same form/page.</Rant>

The Notes client won't let you create mutable JavaScript (that is, you can't change the code by injecting a formula like you can on the web), so you'll need to add one or more hidden fields to carry either a language value or the actual label values themselves. Similarly, the help text can be computed to whatever language the user requires (in Formula Language, of course).

Select the greyed buttons and set the hide-when to !(SortSelection = ""). Select the lower three (the ones with black text) and set their hide-when to SortSelection = "". This way, the user will not see what appear to be active buttons until they make a selection in the list. The top three buttons don't need a formula of any kind, cuz they don't do anything. The bottom three are the real ones. Ideally, you'd like to hide "Move Up" when the first value is selected and so forth, but gimme a break! I've done enough figgerin' for you already.

Go to Remove first. The formula for this button is simply :

tempValues : = @Trim(@Replace(StrippedValues; SortSelection; ""));
FIELD UseNumbers : = "1";
FIELD Values : = @Subset(Numbers; @Elements(tempValues)) + ". " + tempValues;
@Command([ViewRefreshFields])

Note that we've set the UseNumbers field to a "true" value. The final list is available in two versions, numbered and unnumbered. If you want the unnumbered version, you use the StrippedValues field of the dialogged document, if you want numbers, use Values. It's all about reusability — you shouldn't have to change anything on the form to plug it into a new application. The values in the listbox are always unnumbered in this version of the dialog. My tests have shown that users find changing numbers in the dialog less comfortable than an unnumbered list. Your mileage may vary.

Move Up and Move Down are a bit more complicated, but not much. We have to determine where the slection is in the list, then figure out the parts that won't change, then switch the selection with the value above or the value below. Getting the position is just a matter of using @Member against the whole list. Remember, we're getting the list from StrippedValues, so the first line of each formula will be :

position : = @Member(SortSelection; StrippedValues);

In Move Up, the button should do nothing at all when the selection is at the top already, so the next line would be :

@If(position = 1; @Return(""); "");

Now we need to know how big the total list is :

count : = @Elements(StrippedValues);

Next, we'll need the list of values below the selection that will not change. If the selection is at the bottom, this list will be empty, otherwise, we'll take a subset :

bottom : = @If(position = count; ""; @Subset(StrippedValues; - (count - position)));

Similarly, we need the stuff at the top that won't change. Keep in mind that the value immediately above the selection will be swapped :

top : = @If(position = 2; ""; @Subset(StrippedValues; position - 2));

Finally, we'll get the swap value :

swap : = @Subset(@Subset(StrippedValues; position - 1); -1);

Now we can assemble the values and complete the action :

tempValues : = @Trim(top : SortSelection : swap : bottom);
FIELD UseNumbers : = "1";
FIELD Values : = @Subset(Numbers; @Elements(tempValues)) + ". " + tempValues;
Command([ViewRefreshFields])

Using the same kind of logic, the MoveDown formula becomes :

position : = @Member(SortSelection; StrippedValues);
count : = @Elements(StrippedValues);
@If(position = count; @Return(""); "");
bottom : = @If(position = count - 1; ""; @Subset(StrippedValues; - (count - (position + 1))));
top : = @If(position = 1; ""; @Subset(StrippedValues; position - 1));
swap : = @Subset(@Subset(StrippedValues; position + 1); -1);
tempValues : = @Trim(top : SortSelection : swap : bottom);
FIELD UseNumbers : = "1";
FIELD Values : = @Subset(Numbers; @Elements(tempValues)) + ". " + tempValues;
@Command([ViewRefreshFields])

One more button to go. "Add to list" should not only add a new value to the list, it should also select the value so that the user can move it immediately without having to manually select it. The formula is pretty simple :

@If(NewValue = ""; @Return(""); @IsMember(NewValue; StrippedValues); @Return(@Prompt([OK]; "Duplicate Entry"; "The value you are trying to add is already in the list.")); "");
tempValues : = StrippedValues : NewValue;
FIELD UseNumbers : = "1";
FIELD Values : = @Subset(Numbers; @Elements(tempValues)) + ". " + tempValues;
@Command([ViewRefreshFields]);
FIELD SortSelection : = NewValue;
FIELD NewValue : = "";
@True

Now it's just a matter of formatting the table. Make columns 1, 3 and 5 0.125" (as narrow as you can in metric). Columns 2 and 4 should be 2"/5cm (or as close to this as Designer will let you get). Now, go to the cell just above the Move Up, etc., buttons, and change the text properties to Arial and increase the font size until the first three buttons are more-or-less centred vertically beside the listbox. You can use the same font-size technique to adjust the height of the "white space" rows we left in the table, but you'll need to select the whole row to make it work.

That's all there is to creating the dialog in its basic form. Now to put it to use. Since this is for re-use, and we can't be sure that the fields in the dialog even remotely resemble the item names in our application, we can't simply use @DialogBox. With LotusScript, you can dialog any document you want — even one that isn't really there. Create an agent, and call it CallSingleListDialog. Set it to run "Manually from the agent list" and (in R5) set the Documents to run against to "Run once (@Commands may be used) " or (in ND6+) set the target to "None". The code is not very complex. In this example, I will be calling the dialog from a document open in edit mode using @Command([ToolsRunMacro]; "(CallSingleListDialog) ") in an action that's hidden in read mode — the simplest scenario. In this case, the "real" document does not use numbering within the field.

Option Public
Option Declare
'Don't ever let me catch you not using
'Option Declare or Option Explicit

Dim s As NotesSession
Dim ws As NotesUIWorkspace
Dim thisDb As NotesDatabase
Dim currentUIDoc As NotesUIDocument
Dim currentDoc As NotesDocument
Dim dialogDoc As NotesDocument
Dim success As Variant

Sub Initialize
Set s = New NotesSession
Set ws = New NotesUIWorkspace
Set thisDb = s.CurrentDatabase
Set currentUIDoc = ws.CurrentDocument
Set currentDoc = currentUIDoc.Document
Set dialogDoc = thisDb.CreateDocument

Call dialogDoc.ReplaceItemValue("UseNumbers", "0")
Call dialogDoc.ReplaceItemValue("Values", currentDoc.GetItemValue("ExistingField"))
success = ws.Dialogbox("(OrderedListDialog)", True, True, False, False, False, False, "Manage List Values", dialogDoc, True, False)

If success Then
Call currentDoc.ReplaceItemValue("ExistingField", dialogDoc.GetItemValue("StrippedValues"))
End If
End Sub

Here endeth the Lesson. As I mentioned, the variation that will be posted on OpenNTF.org (after the fourth article in the series) will be somewhat more fully-featured and versatile, including code for internationalization and several examples of how to use the dialog. If you find errors or improvements to be made in the posted version, please use the comments feature to tell me about them — I do check, fix problems and incorporate upgrades in dot versions. If you find the code useful, use the rating system and (optionally) the comments to let other people know that the code is worth taking a look at. There's a lot of stuff in the Code Bin, and not all of it is as useful or necessary as the poster believes (some is just a lot of code to replicate a built-in feature, some is reposting of old routines, apparently so the poster can find it again later), and even I might be under unwarranted delusions of brilliance (but I doubt it). By the same token, if the code sucks, rate it as sucky. I'm not proud ... or tired. I just want to make sure that the Code Bin is and remains a worthwhile visit for OpenNTF.org's users and members. Sometimes I think a code review might be useful, but then it'd be just like the Sandbox, where a posting can take forever to appear — and I'm pretty sure that Bruce, Nathan, Anil and the gang have better things to do with their time.

4 comments:

Damien said...

Quick tip, to create a list of numbers in R5, use the permuted concatanation operator (*+). I'm pretty sure it will actually run faster than using the list concatation operator (the old engine was so damn stupid about building lists).

digits0 := @Explode( "0;1;2;3;4;5;6;7;8;9");
digits1 := @Explode( "1;2;3;4;5;6;7;8;9;0");
numbers := digits0 *+ digits0 *+ digits1;

Stan Rogers said...

Better than kewl -- consider it incorporated, commented and credited.

Damien said...

Oops, what I posted doesn't work, I guess I should always test my code!

This works:

digits := @Explode( "0;1;2;3;4;5;6;7;8;9");
numbers := @Subset(digits *+ digits *+ digits; -999);

Stan Rogers said...

Got it, thanks.