A "Quicken Type" Find Using Xdialog

by Cal Locklin

A common issue for Alpha users is to find a specific record based on a name or other value. While this can be done easily with Find by Key, sometimes that's not very useful (i.e., finding "Smith" could mean looking through a lot of records). So, I decided to develop a method that would allow the user to select the desired value from a list that had additional information, such as the address, phone, or other field values. I also had a customer that wanted to select a record based on active records or all records. Finally, I wanted to make it as easy as possible for the user, and I've always like the auto-fill capability of Quicken (as you start typing, the next matching value fills in automatically1,2. The logical solution seemed to be an xdialog.

It was easy to figure out what I wanted the xdialog to look like:



The hard part was making it work. Every time the "Find by" radio button was changed, the list needed to be re-created using the correct filter based on the "Status" setting of Active or All. The list would also need to be re-created every time the Active or All radio button was changed. In addition, whenever the list was being recreated because the "Find by" button was changed, the fields in the list would have to be re-arranged so the first field was the one being searched. The header would also need to be changed to match the new order. Then we need to search the list every time the user types a character in the Find box.

First let's look at how the list is built and why....

The list was built using the Table.external_record_content_get() function because it was easy and fast. I haven't checked the limits but it is nearly instantaneous on my 2.4 Gig system when creating a list of 2500 records - which was plenty for this membership list. (If the list is 200,000 lines I can guarantee there will be a wait unless another technique is used to cut the list into smaller sections.) I didn't use the Keylist_Build method because, even though it puts things in nice columns, I couldn't find any way to search the list on a specific column. (Yes, it finds the first match to the first key pressed but that wasn't adequate for my purposes.)

By separating the field values with " -- ", I could easily retrieve any value in the selected line with a simple Word() function using " -- " defined as the word separator.

The one bad thing about this was that the display wasn't very nice originally because the columns didn't line up when using the default font. That was easily fixed by changing the list to Courier font which uses fixed width characters.

Here's a Table.external_record_content_get() function that could be used to get my list of names from the Members table. Of course, it would all be on one line in the A5 Code Editor:
mlist = table.external_record_content_get( "members", "padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Menumberf+' -- '+Licensenof+' -- '+Mmbr_Statf", "Lastnamef+Firstnamef", "Mmbr_Statf='A'" )

Note that the name was "calculated" to combine first and last names for display purposes and then padded with blank spaces to maintain the alignment of the fields.

When getting a list by member number, the function is almost the same except the sequence of fields is changed and the sort order is changed. This was mostly a cut-and-paste operation since all the info was already available in the other version. (Note that this is the 'real' function because it includes the variable 'filter' that is used to determine whether to list All or only Active records.):
mlist = table.external_record_content_get( "members", "Menumberf+' -- '+padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Licensenof+' -- '+Mmbr_Statf", "Menumberf", filter)

To rebuild the list when a radio button was pressed, I simply set up an event processor (!fby_*) in each radio button.
Find by: (fby:Name!fby_*) (fby:Member #!fby_*) (fby:License #!fby_*)
(For those not familiar with xdialog, the choice of "fby_*" was basically arbitrary. As far as A5 is concerned, it could have been called "xyadk_*" as long as it ended with "_*".)

Note that I used separate radio buttons because I wanted them to appear horizontally but the event name is still identical in all three "Find by" buttons. This means there is only one event needed to handle the "Find by" in the Code section of the xdialog script.

I then checked for the Change event in the Code section of the xdialog:


IF left( a_dlg_button, 4 ) = "fby_"
    IF a_dlg_button = "fby_change"
        <Code here to rebuild to desired list.>
    END IF
    a_dlg_button = ""	'Make sure the dialog doesn't close.
END IF
Finally, we need to search and highlight the list as the user begins typing a value in the Find box. To do this, we need to search the list for the closest match every time a letter is added (or removed) in the Find box. Just like rebuilding the list itself above, the search is triggered by an event processor in the Find box (!fin_*  in this case):
Find: [.20,1find_name!fin_*]

For the purposes of this article, I've included a fairly simple, sequential search routine that simply checks one line at a time until a value equal to or greater than the Find value is located. At that point, the 'selected' variable in the dialog box ('chc' in this case) is made equal to the value of that line and the xdialog automatically jumps to it and highlights it. This simple search routine works fine for demonstration purposes up to about 40 lines. Delays start becoming noticeable after that. I have a much faster binary search routine that theoretically works instantaneously from a user perspective on over 1,000,000,000 lines but the routine is longer and more complex. This binary search routine was used in my comparison tests below. Contact me at mailto:CALocklin@chartermi.net?Subject=&Body= for information on obtaining it as an addin.

Once the correct entry is located, the user has three options for closing the dialog and starting the actual Find in the table. For those who feel they must take the extra time to use the mouse, there is always the option to grab the mouse and click the OK button or double click the selection in the list box. However, for all the serious data entry people who don't like switching back and forth all the time, the fastest and easiest way is to just press the Enter key.

Comparison with Progressive Lookup described in Alpha Newsletter #16.

This method is similar in many ways to the Progressive Lookup described by Jim Chapman in newsletter 16. There are two main differences in the two methods - initial loading method and search method. First, the progressive lookup uses the Keylist_Build method for loading the list and the method presented here uses Table.external_record_content_get(). Second, the search method used by the progressive lookup needs to re-create the list every time a letter is typed whereas the method presented here uses the same list and just locates a different line.

Following are the results of some tests run on my system. I have not tested it on a network but I believe a networked system would result in additional time for loading the initial search list and when updating the list as each letter is typed in the progressive lookup. It shouldn't affect the search routine used by the method presented here because that is just a search on a local string variable which should be handled by the workstation.

The time to load the initial list with 5,000 records seems to be about the same for both methods (a noticeable fraction of a second) but the Keylist_Build method becomes noticeably faster as the number of records increases. At 50,000 records the times were 3.5 seconds vs. 14 seconds. However, this initial difference in load speed must be balanced off against the search speed and the "annoyance level" of the search.

With 5,000 records there was a perceptible delay with the progressive lookup when typing the search text but it wasn't a problem. A similar test on 50,000 records showed some rather significant differences. The actual search was very annoying using the progressive lookup because after each letter was typed there was another few seconds before the next letter could be typed. Using the binary search with the method presented here, there seemed to be a perceptible (i.e., don't blink) delay in the screen re-draw but it was able to keep up with my typing even when I entered "987" as fast as I could.

The one place the method presented here has a definite advantage is when the initial list can be filtered. For example, if you have a list with 50,000 orders but only 2,000 are currently active and you only need to search on the active records, the method presented here would only have to run the search once. The progressive lookup method would have to re-run the filter/query every time a letter was pressed. I ran a test like this and the method presented here took about .72 seconds to present the initial list and the search, of course, happened as fast as I could type. Using the progressive lookup method, the initial search took about 1.4 seconds and each letter that was typed took nearly as long because all 50,000 records had to be queried after each letter.

Conclusion

As with so many other things in the computer world, each method has it's advantages. The progressive lookup is probably easier for most people to build because much of it can be generated with the genies. For smaller lists showing all or most records in the table, the progressive lookup works just fine. On longer lists, the method presented here, if used with a binary search routine, would provide more speed once the initial list was created. The question that has to be answered by you as the developer is, "At what point is it better to make the user wait longer for the list to load in order to make the lookup process itself work more smoothly." At 50,000 records, the total time to complete a search based on entering 4 characters was very similar for both methods. If everything was planned ahead of time, the progressive lookup won by a couple seconds but I believe most users would often end up doing as I did - making typing mistakes due to the delays thus causing even more delays.

My personal opinion at this point is that the progressive lookup is good up to somewhere between 3-5,000 lines and the method presented here is better from there to about 20-30,000 lines when all records are being searched. Beyond that, the wait times are enough to annoy anybody. At that point it becomes a matter of deciding what will annoy the user most - then using the other method.

However, if the situation is such that the initial list can be filtered because the records being searched are a small part of the total, the method presented here will be much faster when used with a binary search routine.

Here's the complete script. (For those who haven't tried it yet, note that the %dlg% section can now be highlighted with a background color as shown here. Unfortunately, the %code% section can't yet - but I'm still hoping. The color choices are up to the user.)



'Find by name, member number, license number.
'Set up defaults for a find by Name showing Active members.
fby = "Name"
mlist = table.external_record_content_get( "members", "padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Menumberf+' -- '+Licensenof+' -- '+Mmbr_Statf", "Lastnamef+Firstnamef", "Mmbr_Statf='A'" )
hdr = "Name                              Member #                License #     Status"
show_all = "Active"

'Show a dialog to prompt the user.
dlg_text = <<%dlg%
{ysize=.3}{units=F}{sp};
{font=MS Sans Serif,8,b}Find: {font=MS Sans Serif,8,n}[.20,1find_name!fin_*]
{font=MS Sans Serif,8,b}  Find by: {font=MS Sans Serif,8,n}(fby:Name!fby_*) (fby:Member #!fby_*) (fby:License #!fby_*);{sp};
{font=MS Sans Serif,8,b}Status: {font=MS Sans Serif,8,n}(show_all:Active!sta_*) (show_all:All!sta_*);{sp};
{font=Courier,10}{text=80,1hdr};
[.80,20chc^#mlist!mli_*]{font=MS Sans Serif,8,n};
{line=1,0};
<*15OK> <15Cancel>
%dlg%

ok_pressed = .F.	'Used to verify OK button or double click.

DIM dlg_result as C
dlg_result = ui_dlg_box( "SELECT MEMBER", dlg_text, <<%code%
'Check to see if the user double-clicked a line.
IF left( a_dlg_button, 4 ) = "mli_"
    IF a_dlg_button = "mli_dblclick"
        ok_pressed = .T.
    ELSE
        a_dlg_button = ""
    END IF
END IF
'If the user typed a letter, find the closest matching line.
IF left( a_dlg_button, 4 ) = "fin_"
    IF a_dlg_button = "fin_change" .and. find_name <> ""
        ptr = Search_str( mlist, find_name )
        chc = ptr.value
    END IF
    a_dlg_button = ""
END IF
'If the user changed the "Find by", rebuild the list.
IF left( a_dlg_button, 4 ) = "fby_"
    IF a_dlg_button = "fby_change"
        mlist = Get_mlist( show_all, fby )
        'Since the search is changing, blank out the name.
        find_name = ""
        'jump back to the Find box.
        ui_dlg_ctl_goto( "SELECT MEMBER", "[.20,1find_name!fin_*]" )
    END IF
    a_dlg_button = ""
END IF
'If the user changed the Status filter selection, rebuild the list.
IF left( a_dlg_button, 4 ) = "sta_"
    IF a_dlg_button = "sta_change"
        mlist = Get_mlist( show_all, fby )
        find_name = ""
        ui_dlg_ctl_goto( "SELECT MEMBER", "[.20,1find_name!fin_*]" )
    END IF
    a_dlg_button = ""
END IF
'If the user clicked the OK button, make sure something was selected before 
'allowing the dialog to close.
IF a_dlg_button = "OK"
    IF chc = ""
        msg = "Please make a selection or press the Cancel button."
        ui_msg_box( "NOTHING SELECTED", msg, 48 )
        a_dlg_button = ""
    ELSE
        ok_pressed = .T.
    END IF
END IF
%code%)

'If the user cancelled, quit.
IF .not. ok_pressed
    END'
END IF

'Parse the member number from the string. For the sake of 
'convenience, the member number is always in the 2nd position 
'except when looking up by member number.
IF fby = "Member #"
    mem_no = word( chc, 1, " -- " )
ELSE
    mem_no = word( chc, 2, " -- " )
END IF

'Set the index and find the member's record.
parentform.Index_SetExplicit( "Menumber_" )
parentform.find( mem_no )

END'
'End of program. Only functions follow.

FUNCTION Search_str as P ( Str_in as C, Find_what as C )
    cnt = 0
    pos = 0
    value = ""
    WHILE cnt < line_count( str_in )
        cnt = cnt + 1
        IF word( str_in, cnt, crlf() ) >= find_what
            value = word( str_in, cnt, crlf() )
            pos = cnt
            cnt = 99999999	'Stop the WHILE
        END IF
    END WHILE
    'If we reach the end with no match, show the last line.
    IF cnt = line_count( str_in ) .and. pos = 0
        pos = cnt
        value = word( str_in, cnt, crlf() )
    END IF
    Search_str.pos = pos
    Search_str.value = value
END FUNCTION

FUNCTION Get_mlist as C ( show_all as C, fby as C )
    IF show_all = "All"
        filter = ""
    ELSE
        filter = "Mmbr_Statf='A'"
    END IF
    SELECT 
      CASE fby = "Name"
          mlist = table.external_record_content_get("members","padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Menumberf+' -- '+Licensenof+' -- '+Mmbr_Statf","Lastnamef+Firstnamef",filter)
          hdr = "Name                              Member #                License #     Status"
      CASE fby = "Member #"
          mlist = table.external_record_content_get("members","Menumberf+' -- '+padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Licensenof+' -- '+Mmbr_Statf","Menumberf",filter)
          hdr = "Member #                Name                              License #     Status"
      CASE fby = "License #"
          mlist = table.external_record_content_get("members","Licensenof+' -- '+Menumberf+' -- '+padr(trim(Lastnamef)+', '+trim(Firstnamef),30,' ')+' -- '+Mmbr_Statf","Licensenof",filter)
          hdr = "License #     Member #                Name                              Status"
    END SELECT
	
    Get_mlist = mlist

END FUNCTION

Cal Locklin has worked extensively with xdialog since V5 was introduced, and has shared his expertise many Alpha users. He can be reached at CALocklin@charter.net or www.aimsdc.net.

1 For example, if you type "S", you might see Sanders (the first "S" record). When you type "M", Smedly might appear. And so on.

2 Alpha does have this capability in field rules, but it becomes unusably slow with more than a couple hundred records.