Custom Combobox Control (Read Only, with AutoComplete) Autocomplete problems.

I have a Custom Control that I've made for an Application I'm developing that has Autocomplete and Readonly properties added.  My problem is that the autocomplete function seems to "error" when a user is typing too fast; i.e., the control is trying to find a match, but another keystroke is entered that ends up overriding the current "match find".  The problem is that if I have an entry for "Custom 1", and I rapidly type in c, u, s, it will select "Custom 1", and append the 'u' and 's' to the end; it's not properly selecting the text.

Here is the relevant sub's for autocompletion:

Private Sub CompletionCombo_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyUp
        Dim sTypedText As String
        Dim iFoundIndex As Integer
        Dim oFoundItem As Object
        Dim sFoundText As String
        Dim sAppendText As String

        If Me.Items.Count > 0 Then
            'Allow select keys without Autocompleting
            Select Case e.KeyCode
                Case Keys.Back, Keys.Left, Keys.Right, Keys.Up, Keys.Delete, Keys.Down, Keys.ControlKey, Keys.ShiftKey, Keys.Alt, Keys.ShiftKey, Keys.MButton, Keys.LButton, Keys.RButton, Keys.Home, Keys.End
                    Exit Sub
            End Select

            'Get the Typed Text and Find it in the list
            If Me.Text.Length > 0 And Me.Text.Length <> Me.SelectedText.Length Then
                sTypedText = Me.Text
                iFoundIndex = Me.FindString(sTypedText)

                'If we found the Typed Text in the list then Autocomplete
                If iFoundIndex >= 0 Then
                    'Get the Item from the list (Return Type depends if Datasource was bound
                    ' or List Created)
                    oFoundItem = Me.Items(iFoundIndex)

                    'Use the ListControl.GetItemText to resolve the Name in case the Combo
                    ' was Data bound
                    sFoundText = Me.GetItemText(oFoundItem)

                    'Append then found text to the typed text to preserve case
                    sAppendText = sFoundText.Substring(sTypedText.Length)
                    Me.Text = sTypedText & sAppendText

                    'Select the Appended Text
                    Me.SelectionStart = sTypedText.Length
                    Me.SelectionLength = sAppendText.Length
                    'Me.SelectedIndex = iFoundIndex
                End If
            End If

        End If
    End Sub

If someone knows of a way to trap these events somehow, to prevent the selection errors, I would appreciate it.  I have been struggling to correct this, and I need to do so as I am almost ready to implement my application.  Please do not refer me to existing Autocomplete Combobox pages; I'm only interested in fixing the problem in my own control, mainly to learn how to do it, and because I have got everything else working properly for my control, and don't want to have to redo all of the programming already done for my comboboxes in my forms.

The problem (as I figure it) is that while the control is attempting to do an autocomplete, the user has already typed in another key, so that when the match is found, there is extra text, and the autocompletion (keyup) routine does not properly select the text.  I have tried many different ways to trap this, but I have been wholly unsuccessful.  I *THINK* that what needs to be done is that some kind of buffer needs to save keystrokes entered, and send them to the keyup event only AFTER the routine to match has successfully finished.  I have no idea if this is what needs to be done, but in any case, I'm quite desperate, and clueless on how to achieve this!

Thanks,

Jake
LVL 14
jake072Asked:
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Bob LearnedCommented:
I have an AutoComplete function for a ComboBox, which duplicates the way that Internet Explorer does AutoComplete.  It is a very nice class, but it is very advanced code.  Would you still be interested?

Bob
jake072Author Commented:
Well, I guess it'd be worth a look.  I'd prefer to use my own combo, as my users prefer the text selection method that I am currently using, but perhaps I could pick up some pointers.

Thanks,

Jake
amyhxuCommented:
Take a look at this auto complete combo, it should work for you:
http://www.freevbcode.com/ShowCode.asp?ID=7007
Why Diversity in Tech Matters

Kesha Williams, certified professional and software developer, explores the imbalance of diversity in the world of technology -- especially when it comes to hiring women. She showcases ways she's making a difference through the Colors of STEM program.

jake072Author Commented:
TheLearnedOne,

Thanks for the code...  However, I need a class that inherits from ComboBox though, or it will break A LOT of coding that I've already done!

amyhxu,

That code solved the problem of the keystrokes, however it added behaviour that was very undesired: for instance, it doesn't work right with control keys, it won't let you type something in, unless it's in the list (I need to be able to do this, as in my program if they type an entry in that doesn't exist, the system will ask them if they want to create a new entry)...

I tried my best to try and port the code from freevbcode to work as I need it, however, I can't get it to work at all; it won't append text or select text for some reason (at least my conversion won't)!

What I did was simply make my procedure use the KeyPress event...  This solved the keystroke problem, but now the combo won't append text to what I typed, nor will it do a selection for me.  Does anyone know why??
(I.e., I took my code above, replaced the KeyEventArgs parameter with KeyPressEventArgs, and changed the handler to handle KeyPress;  Additionally, I had to cast the KeyChar from the KeyPressEvent to an int using Val(e.KeyChar))

Here's to hoping that someone can still help!

Thanks for the time though,

Jake
Bob LearnedCommented:
You could put that code into a UserControl that inherits from ComboBox, and easily enable AutoComplete, but you have to like the effect first, before going down that road.

Bob
jake072Author Commented:
TheLearnedOne,

I've tried setting up a demo form, to test your code, and I must say that I quite like it!  However, I have NO CLUE about how to go about placing the code into a UserControl...  This seems like a very good solution, and I've cleared it with my boss.

There are a couple of problems though:

1) As mentioned, I'm a little lost on how to use this code in a user control (help?!?)
2) Could I pass a dataset with a column to use, in place of an arraylist?  (I'm thinking that it would be better to pass a dataset, rather than build an arraylist from data [baring in mind that I'm not nearly as experienced as you]; so if it's faster to simply create an arraylist from a dataset, then I'm sure that I can figure out how to do that...)

Thanks in advance,

Jake
amyhxuCommented:
Hi Jake,

The combobox from freevbcode doesn't allow adding items to the list (because you mentioned "readonly"?). But you can modify it to make it work as you expected. This may take some effort though. If you can get Bob's method to work, then definitely that's the way to go.
jake072Author Commented:
amyhxu,

Thanks very much for the responses, and time.

I'm hoping that Bob will be able to walk me through creating a control with his code (if he's not cursing my inabilities as a programmer :) )

Thanks again,

Jake
Bob LearnedCommented:
Fast and furious, down and dirty, quick solution:

=================================================

'Source URL:
'http://63.236.73.220/showthread.php?t=277805

Imports System.ComponentModel
Imports System.Runtime.InteropServices

Public Class AutoCompleteComboBox : Inherits System.Windows.Forms.ComboBox

#Region " Windows Form Designer generated code "

  Public Sub New()
    MyBase.New()

    'This call is required by the Windows Form Designer.
    InitializeComponent()

    'Add any initialization after the InitializeComponent() call

  End Sub

  'UserControl overrides dispose to clean up the component list.
  Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
    If disposing Then
      If Not (components Is Nothing) Then
        components.Dispose()
      End If
    End If
    MyBase.Dispose(disposing)

    Marshal.ReleaseComObject(IAutoComp)

  End Sub

  'Required by the Windows Form Designer
  Private components As System.ComponentModel.IContainer

  'NOTE: The following procedure is required by the Windows Form Designer
  'It can be modified using the Windows Form Designer.  
  'Do not modify it using the code editor.
  <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
    components = New System.ComponentModel.Container
  End Sub

#End Region


  Private m_registryPath As String = String.Empty
  Private m_quickComplete As String = String.Empty
  Private m_listWords As ArrayList = New ArrayList

  Private m_enabled As Boolean = True
  Private m_autoComp As Type
  Private IAutoComp As IAutoComplete

  ' HKEY_CLASSES_ROOT\Interface\{00000101-0000-0000-C000-000000000046}\IEnumString
  <ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00000101-0000-0000-C000-000000000046")> _
  Private Interface IEnumString

    Function [Next](ByVal celt As Integer, ByVal rgelt() As String, ByRef pceltFetched As Integer) As Integer

    Function Skip(ByVal celt As Integer) As Integer

    Function Reset() As Integer

    Sub Clone(ByRef ppenum As IEnumString)

  End Interface 'IEnumString'


  <ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("EAC04BC0-3791-11D2-BB95-0060977B464C")> _
  Private Interface IAutoComplete

    Function Init(ByVal hwndEdit As IntPtr, _
      <MarshalAs(UnmanagedType.IUnknown)> ByVal punkACL As Object, _
      ByVal pwszRegKeyPath As String, _
      ByVal pwszQuickComplete As String) As Int32

    ' We must use <PreserveSig()> otherwise after using
    ' autocomplete, fields will not return the next time.

    <PreserveSig()> _
    Function Enable(<[In]()> ByVal fEnable As Boolean) As Boolean

    <PreserveSig()> _
    Function SetOptions(<[In]()> ByVal dwFlag As UInt32) As Integer

    <PreserveSig()> _
    Function GetOptions(<Out()> ByRef pdwFlag As UInt32) As Integer

  End Interface 'IAutoComplete.


  Private Enum ShowOptions
    None = 0
    AutoSuggest = &H1
    AutoAppend = &H2
    Search = &H4
    FilterPrefixes = &H8
    UseTab = &H10
    UpDownKeyDropsList = &H20
    RightToLeft = &H40
  End Enum 'ShowOptions'



  Private Class EnumString : Implements UCOMIEnumString

    ' UCOMIEnumString is the same as the IEnumString interface
    ' see more at Msdn on http://msdn.microsoft.com/library/d...mi_d2l_89uv.asp

    Private _strArray As ArrayList
    Private _pos As Integer = 0


    Public Property Array() As ArrayList
      Get
        Return _strArray
      End Get
      Set(ByVal Value As ArrayList)
        _strArray = Value
      End Set
    End Property 'Array'


    Public Function [Next](ByVal celt As Integer, ByVal rgelt As String(), _
      ByRef pceltFetched As Integer) As Integer Implements UCOMIEnumString.Next

      Dim retval As Integer = 1

      pceltFetched = 0
      While Not _pos = _strArray.Count AndAlso Not pceltFetched = celt
        rgelt(pceltFetched) = _strArray(_pos)
        pceltFetched += 1
        _pos += 1
      End While

      If Not pceltFetched.CompareTo(celt) = -1 Then
        retval = 0
      End If

      Return retval

    End Function


    Public Function Skip(ByVal celt As Integer) As Integer Implements UCOMIEnumString.Skip

      Dim retval As Integer = 1
      _pos += celt

      If Not _pos = _strArray.Count Then
        retval = 0
      End If

      Return retval

    End Function 'Skip'


    Public Function Reset() As Integer Implements UCOMIEnumString.Reset

      _pos = 0

      Return _pos

    End Function 'Reset'


    Public Sub Clone(ByRef ppenum As UCOMIEnumString) Implements UCOMIEnumString.Clone

      ' create a Clone of this Class
      ppenum = DirectCast(Me, EnumString)

    End Sub 'Clone'

  End Class 'EnumString'


  Public Sub InitializeAutoCompletion()

    Dim handleEdit As IntPtr = Me.ComboBoxEditHandle()

    Dim iEnumList As New EnumString
    iEnumList.Array = m_listWords

    m_autoComp = Type.GetTypeFromCLSID(New Guid("{00BB2763-6A77-11D0-A535-00C04FD7D062}"))

    IAutoComp = DirectCast(Activator.CreateInstance(m_autoComp), _
      IAutoComplete)

    ' Initialize IAutoComplete.
    IAutoComp.Init(handleEdit, iEnumList, RegistryPath, QuickComplete)

    ' Set IAutoComplete Options.
    IAutoComp.SetOptions(Convert.ToUInt32(ShowOptions.AutoSuggest Or _
      ShowOptions.UpDownKeyDropsList Or ShowOptions.AutoAppend))

    ' Enable IAutoComplete.
    IAutoComp.Enable(True)

  End Sub 'New'


  <Description("Turn on/off the AutoComplete function"), DefaultValue(True)> _
  Public Shadows Property Enabled() As Boolean
    Get
      Return m_enabled
    End Get
    Set(ByVal Value As Boolean)
      m_enabled = Value

      If Not IAutoComp Is Nothing Then
        If Value Then
          IAutoComp.SetOptions(Convert.ToUInt32(ShowOptions.AutoSuggest Or ShowOptions.UpDownKeyDropsList Or ShowOptions.AutoAppend))
        Else
          IAutoComp.SetOptions(Convert.ToUInt32(ShowOptions.None))
        End If
      End If
    End Set
  End Property 'Enabled'


  Private Declare Function FindWindowEx _
    Lib "user32" Alias "FindWindowExA" _
    (ByVal hWnd1 As IntPtr, ByVal hWnd2 As IntPtr, _
    ByVal lpsz1 As String, ByVal lpsz2 As String) As IntPtr


  Private Function ComboBoxEditHandle() As IntPtr

    ' Get the handle of the inner Edit control.
    Dim editHandle As IntPtr = FindWindowEx(Me.Handle, IntPtr.Zero, _
      vbNullString, vbNullString)

    Return editHandle

  End Function 'ComboBoxEditHandle'


  <Description("Load the word list from a DataTable.")> _
  Public Overloads Sub LoadWords(ByVal dataWords As DataSet, ByVal tableName As String, ByVal columnName As String)

    Me.LoadWords(dataWords.Tables(tableName), columnName)

  End Sub 'LoadWords(dataset, table, column)'


  <Description("Load the word list from a DataTable.")> _
  Public Overloads Sub LoadWords(ByVal tableWords As DataTable, ByVal columnName As String)

    Me.WordList = New ArrayList

    ' Load a data reader into the word ArrayList.
    For Each rowCurrent As DataRow In tableWords.Rows

      Dim wordCurrent As String = rowCurrent(columnName).ToString

      Me.WordList.Add(wordCurrent)

    Next rowCurrent

  End Sub 'LoadWords(table, column)'


  <Description("Load the word list from any kind of DataReader.")> _
  Public Overloads Sub LoadWords(ByVal readerWords As IDataReader, ByVal columnName As String)

    Me.WordList = New ArrayList

    ' Load a data reader into the word ArrayList.
    While readerWords.Read

      Dim wordCurrent As String = readerWords(columnName).ToString

      Me.WordList.Add(wordCurrent)

    End While

  End Sub 'LoadWords(reader, column)'


  <Description("Get/Set the Windows Registry location for storing the word list.")> _
  Public Property RegistryPath() As String
    Get
      Return m_registryPath
    End Get
    Set(ByVal Value As String)
      m_registryPath = Value
    End Set
  End Property 'RegistryPath'


  <Description("")> _
  Public Property QuickComplete() As String
    Get
      Return m_quickComplete
    End Get
    Set(ByVal Value As String)
      m_quickComplete = Value
    End Set
  End Property 'QuickComplete'


  <Description("The list of words that are used in the auto-completion process.")> _
  Public Property WordList() As ArrayList
    Get
      Return m_listWords
    End Get
    Set(ByVal Value As ArrayList)
      m_listWords = Value
    End Set
  End Property 'WordList'


End Class 'AutoCompleteComboBox'

=================================================

I added some overloaded methods:

LoadWords(DataSet, TableName, ColumnName)
LoadWords(DataTable, ColumnName)
LoadWords(IDataReader, ColumnName)

The only way that I could see implementing this is to use an ArrayList.

Usage:
      Dim listWords As New ArrayList
      listWords.Add("Green")
      listWords.Add("Red")
      listWords.Add("Yellow")
      listWords.Add("Blue")
      listWords.Add("Magenta")
      listWords.Add("Maroon")
      listWords.Add("Cyan")

      Me.ComboBox1.WordList = listWords
      Me.ComboBox1.InitializeAutoCompletion()

Bob

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
jake072Author Commented:
Wow!

I had just finished programming my own control, and now you've done it for me (go figure!)

Anyways, I just thought I'd post what I'd come up with, but I think as you're the Guru, I shall use yours :)

My Code (Using the code you had given me, in a Class called AutoComplete):

Public Class ComboBoxFix
    Inherits Windows.Forms.ComboBox

    Sub New()
    End Sub

    Private Declare Function FindWindowEx _
        Lib "user32" Alias "FindWindowExA" _
        (ByVal hWnd1 As IntPtr, ByVal hWnd2 As IntPtr, _
        ByVal lpsz1 As String, ByVal lpsz2 As String) As IntPtr

    Public Function ComboBoxEditHandle(ByVal comboCurrent As Windows.Forms.ComboBox) As IntPtr

        ' Get the handle of the inner Edit control.
        Dim editHandle As IntPtr = FindWindowEx(comboCurrent.Handle, IntPtr.Zero, vbNullString, vbNullString)

        Return editHandle

    End Function 'ComboBoxEditHandle'

    Private Sub ComboBoxFix_DisplayMemberChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.DisplayMemberChanged
        If Not (MyBase.DataSource Is Nothing) Then
            Dim _arrayList As New ArrayList
            If TypeOf (MyBase.DataSource) Is DataTable Then
                Dim dt As DataTable = MyBase.DataSource
                Dim dr As DataRow
                For Each dr In dt.Rows
                    _arrayList.Add(dr(MyBase.DisplayMember.ToString()))
                Next
            End If
            Dim completer As New AutoComplete(ComboBoxEditHandle(Me), _arrayList)
        End If
    End Sub

    Private Sub ComboBoxFix_DataSourceChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.DataSourceChanged
        If Not (MyBase.DisplayMember = "") Then
            ComboBoxFix_DisplayMemberChanged(sender, e)
        End If
    End Sub
End Class

Thanks very much Bob, I really appreciate this!

Just out of curiosity, is my solution viable (I know that it works for me, but perhaps I'm doing something terribly wrong???)  Btw, I have the last sub (DataSourceChanged) just to account for the fact that if a user enters a new value from the combobox, the arraylist will be redone...  I programmed it this way so I wouldn't have to change any of my existing code (I'm lazy).

One last question (sorry) Is this component going to be adding things to the registry?

Thanks once again Bob, I really can't tell you how happy I am!

Jake
Bob LearnedCommented:
There a lot of ways to skin a cat :)

After looking at your code, it may have worked, I am just not 100% sure.

Bob
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
Visual Basic.NET

From novice to tech pro — start learning today.