4

Good morning!

I have a "fancy" search function in Microsoft Access where the list of possible options shrinks as you type in the search field. Unfortunately the computer and server can't keep up with these rapid requeries of the data.

Currently the command to requery with the field in the 'onchange' function of the search box. I'd like to add a delay so it only runs the requery when the search box has not changed for a second. Thus if someone types in a 8 letter word, it isn't running 8 requeries.

The current idea I have for it, which I know there must be something better, is..

"On change, set search box value to X and wait 1 second. After 1 second, if X = search box value, run the requery. An issue is that it would be rapidly rewriting the X value and have a 'wait' command floating for each letter.

Hopefully there's a way to write an event trigger of "When field X has changed, but not changed for the past second."

Thank you!

As requested, here is my current code

'Create a string (text) variable
    Dim vSearchString As String
'Populate the string variable with the text entered in the Text Box SearchFor
    vSearchString = SearchFor.Text
'Pass the value contained in the string variable to the hidden text box SrchText,
'that is used as the sear4ch criteria for the Query QRY_SearchAll
    SrchText = vSearchString
'Requery the List Box to show the latest results for the text entered in Text Box SearchFor
    Me.SearchResults.Requery
    Me.SearchResults2.Requery
'Tests for a trailing space and exits the sub routine at this point
'so as to preserve the trailing space, which would be lost if focus was shifted from Text Box SearchFor
    If Len(Me.SrchText) <> 0 And InStr(Len(SrchText), SrchText, " ", vbTextCompare) Then
        'Set the focus on the first item in the list box
            Me.SearchResults = Me.SearchResults.ItemData(1)
            Me.SearchResults.SetFocus
        'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of  the List Box
            DoCmd.Requery
        'Returns the cursor to the the end of the text in Text Box SearchFor,
        'and restores trailing space lost when focus is shifted to the list box
            Me.SearchFor = vSearchString
            Me.SearchFor.SetFocus
            Me.SearchFor.SelStart = Me.SearchFor.SelLength
            Exit Sub
    End If
'Set the focus on the first item in the list box
'    Me.SearchResults = Me.SearchResults.ItemData(1)
    Me.SearchResults.SetFocus    
'Requery the form to refresh the content of any unbound text box that might be feeding off the record source of  the List Box
    DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor
    Me.SearchFor.SetFocus
    If Not IsNull(Len(Me.SearchFor)) Then
        Me.SearchFor.SelStart = Len(Me.SearchFor)
    End If

Obviously this is not MY code, it's from somewhere on the interweb. It works fantastic for databases stored locally, but everything is moving to our Sharepoint server which is running on a 386 in a moldy basement powered by a narcoleptic gerbil.

Andrew Truckle
  • 13,595
  • 10
  • 45
  • 105
  • The issue is that the search field can be anywhere from 1 to 255 characters. What I want is for the requery to run when the search box is changed AND has not been changed in the past second. That way it's not running 8 requeries when you type in an 8 letter word. – Robert Patrician Jul 30 '18 at 17:16
  • Access doesn't allow for asynchronous computing, and planning tasks can be done with forms and triggers, but is a bit hacky. While it can be done, I recommend you don't. – Erik A Jul 30 '18 at 17:17
  • There is a LOT of stuff in my database that falls under the "I recommend that you don't" category. This particular fix falls under "Software solution for a hardware problem" – Robert Patrician Jul 30 '18 at 17:54

4 Answers4

2

You can simply use the Timer of the current form. No need for a separate form or anything.

Private Sub DoSearch()

    ' Your current code
    ' but you should look into removing as many "Requery" from there as possible!

End Sub

Private Sub SearchFor_Change()

    ' Wait for x Milliseconds until the search is started.
    ' Each new change restarts the timer interval.
    ' Use 1000 (1 s) for slow typists or a really slow server
    ' 200 ms feels right for a normal typist
    Me.TimerInterval = 200

End Sub

Private Sub Form_Timer()

    ' Disable timer (will be enabled by the next SearchFor_Change)
    Me.TimerInterval = 0
    ' Now run the search
    DoSearch

End Sub

Note: you may need to move some of the cursor-handling code from DoSearch() to SearchFor_Change(), specifically:

Me.SearchFor.SelStart = Len(Me.SearchFor)
Andre
  • 24,160
  • 6
  • 28
  • 67
1

Assign a shortcut key like (Ctrl+ J) to the logic in on change event and call it on demand once you have finished typing search keyword.

  1. Remove on change event.
  2. Create other procedure which has the logic of on change event and assign a shortcut key
  3. Press shortcut to get search suggestion

Other approach

Add below validation to Change event which will check for length of string and will trigger only if length of string is >=8

Private Sub txtSearch_Change()

    If Len(Nz(txtSearch.Text, 0)) >= 8 Then


    End If

End Sub
Santosh
  • 11,722
  • 3
  • 36
  • 67
  • Unfortunately I'm not the only one using this database, and the 'fanciness' of the lists shrinking as you type is one of the selling points to management. – Robert Patrician Jul 30 '18 at 18:02
  • @RobertPatrician `fanciness` will come at a cost. This approach will eat the least resource and still will get suggestion while pressing shortcut key. Else change search box to combo box. – Santosh Jul 30 '18 at 18:07
  • 1
    I would use `If Len(Nz(txtSearch.Text, 0)) >= 4` since the first searches are less interesting and more expensive. 3 or 4 char could be a good compromise between *fancy* and *efficient* – Patrick Honorez Jul 30 '18 at 18:18
1

I'm going a little outside my comfort area, since I hardly use MS Access forms, but why are you bothering the Server/Database so much? In my experience, each query costs the same amount of time, whether it returns 1 record or 100,000 records.

So even before the user types anything, why don't you just do a single query to return a sorted list. After that, it takes almost no time to use VBA to process the results and find everything in the list that starts with whatever the user types in (it's sorted after all).

Except for the initial load, users who are local to the database or on the other side of the world will experience the same snappy response from your interface.

----------

Like I said, I haven't messed with Access Forms a lot, so this is more of a strict VBA solution. Maybe there is a better way to do it without going outside the Access Forms box that someone could enlighten us with.

You should basically just call LoadItemList when you load the form, or whenever you need to.

Public dbConn As ADODB.Connection
Private ItemList As Variant
Private RecordCount As Long

Sub LoadItemList()
Dim SQL As String
Dim RS As New ADODB.Recordset

    SQL = "SELECT T.Name FROM Table T"
    Set RS = dbConn.Execute(SQL)
    If Not RS.EOF Then
        ItemList = RS.GetRows
        RecordCount = UBound(ItemList, 2) - LBound(ItemList, 2) + 1
    End If
End Sub

Then replace DoCmd.Requery with AddItemtoCombobox SearchResults, SearchFor.Text

Sub AddItemtoCombobox(Control As ComboBox, Filter As String)
Dim Index As Long
    Control.Clear
    If Not IsEmpty(ItemList) Then
        For Index = 0 To RecordCount - 1
            If ItemList(Index) Like Filter Then Control.AddItem ItemList(Index)
        Next
    End If
End Sub

Again, maybe there is a better way that is built into Access...

Profex
  • 1,320
  • 8
  • 19
  • In most situations your approach is the best, in my experience. Bravo! – Gene Jul 30 '18 at 18:46
  • Well it's a "Like * & searchfield & *" criteria, so if you searched "west" it would bring back both "Western apparel" and "George West" and "Northwest industries" However I don't know how to do the kind of filtering you're suggesting. Right now the query pulls back all the records from the database where they match the search field, and puts them in the combobox. Where would I get started to have an approach where it loads ALL records into the combobox first, and then just filters it down based on the search field? – Robert Patrician Jul 30 '18 at 19:07
  • Like I said, Access is a little outside my comfort area, but I would just save the entire Recordset to an array or something and then loop through the entire list. Use VBA's `Like` statement on each item; If it matches, then add it to the combo box. Maybe there is better way, but I can't think of it at the moment. – Profex Jul 30 '18 at 19:15
0

The technical term that you're looking for is debounce.

What you can do is on your on change event, keep track of the current search string

in terms of pseudocode.

sub onChange()
   Form.timerinterval = 0
   setSearchString
   form.timerinterval = delay

So in terms of the explanation, if your on change is called, disable the timer. Update your search string, then reset the timer to fire after a certain amount of time. The form should be a hidden form that contains the code that you want to execute

Eric Yang
  • 1,816
  • 8
  • 15
  • @ErikvonAsmuth while Access doesn't have Application level timers there are form level timers. You can set the TimerInterval then do this same logic in the `Form_Timer` event. – Brad Jul 30 '18 at 17:22
  • What about using a "DoUntil" as a delay for the check? So something like "On change, set value X to searchbox value and set DoOnTime value to Now() plus 1 second. Dountil DoOnTime value = now(), then if X = searchboxvalue then run the command... This is getting spaghetti fast. – Robert Patrician Jul 30 '18 at 17:24
  • 1
    I would not busy wait. That chews up CPU cycles – Eric Yang Jul 30 '18 at 17:25
  • Busy cycles on the CPU are preferrable to calling multiple queries on the server, which I'm fairly sure is a 386 in a moldy basement powered by a narcoleptic hamster. – Robert Patrician Jul 30 '18 at 17:50
  • @RobertPatrician is the data being filtered so large that it's not acceptable to download it all then filter it locally instead of running the query on a server every time? – Brad Jul 30 '18 at 17:54
  • @brad Hah, I wish. Right now we're talking about 1000 records across three tables. When I use this same search function on tables that are stored locally, there is no problem. The responses are instant. This is very much a server problem that I have to code around. Currently if I type in a long word, it can take a full minute to finish all the requeries. However, the data is worked on by multiple people at the same time. I plan for this database to systematically devour other databases to create an integrated system. – Robert Patrician Jul 30 '18 at 17:59
  • I'd call 1000 records a very small number. What is your filtering method specifically? If it is requeries to the server maybe that is slow (like you're seeing) but you can also filter the recordset you have on your form already without going to the server. – Brad Jul 30 '18 at 18:03
  • 1
    @EricYang I'm trying to understand the code posted. What do you mean by "addtosearchstring" ? – Robert Patrician Jul 30 '18 at 18:06
  • Sorry, bad name. Basically you need to store the search string in some sort of global store so that the other form can see it. So in your onChange handler, set the global variable for search string, and your other hidden from, when it triggers, can search against that string – Eric Yang Jul 30 '18 at 18:13
  • @Brad My filtering method is currently to requery the recordset after each change of the search box. How would I get started with filtering the existing records instead of constant requeries? To put this in perspective, currently it's actually requerying multiple times for each onchange event, because there are multiple combo boxes. This gives one search function for multiple categories; a list of programs and a list of contacts. Thus I can put in "Nephrology" or "smith" in the search box and see all results in the different boxes. – Robert Patrician Jul 30 '18 at 18:28
  • @RobertPatrician can you edit your current code into your question (comments are not going to be a good spot)? – Brad Jul 30 '18 at 19:07
  • @Brad Done. From elsewhere in the thread someone suggested changing to just pulling all the data for the combo boxes at the start, and then filtering instead of requerying. Unfortunately I don't know where to begin with such an approach. – Robert Patrician Jul 30 '18 at 19:18