AutoComplete with DataSource

Download the code for this article (22KB)

.Net 2.0 introduced autocompletion in TextBox and ComboBox. It is obvious that autocomplete is not very useful when the number of options is small. However, when the number of option becomes too many, pre-filling of all options to an AutoCompleteStringCollection becomes impractical, especially when the data is coming from a remote computer. An alternative is to replace the AutoCompleteCustomSource in a TextChanged event, however, users are getting random AccessViolationException when trying to replace it.  In this article I will demonstrate another alternative, using a BindingSourceas the data source of options, bypassing the .Net framework and call the underline Windows API directly.

The first thing I need to do is to port the Windows autocomplete APIs to managed code. The autocompletion API is exposed as a COM object, so I need to write managed version of its interfaces:

[ComImport]
[InterfaceType(ComInterfaceType::InterfaceIsIUnknown)]
[Guid(“EAC04BC0-3791-11D2-BB95-0060977B464C”)]
[SuppressUnmanagedCodeSecurity]
interface  class IAutoComplete2
{
[PreserveSig] int Init(
HandleRef hwndEdit,
IEnumString^ punkACL,
String^ pwszRegKeyPath,
String^ pwszQuickComplete
);
void Enable( [MarshalAs(UnmanagedType::Bool)] bool value);

int SetOptions(int dwFlag);

void GetOptions([Out]IntPtr pdwFlag);
};
Second, I need to create the autocomplete object and query the IAutoComplete2 interface in order to change its options:

Type^ autoCompleteType = Type::GetTypeFromCLSID(CLSID_AutoComplete);
try{
autoComplete2 =(IAutoComplete2^)(Activator::CreateInstance(autoCompleteType));
}
catch(Exception^ e)
{
Marshal::ReleaseComObject(autoComplete2);
autoComplete2 = nullptr;
}

Third, I need to bind it to an TextBox control:

bool AutocompleteBindingSource::Bind()
{
if (nullptr==this->autoComplete2)
return false;
try
{
this->autoComplete2->SetOptions((int)ControlToBind->AutoCompleteMode);
this->autoComplete2->Init(
HandleRef(ControlToBind,ControlToBind->Handle),
this,
String::Empty
,String::Empty);
return true;
}
catch(Exception^e)
{
return false;
}

}

Finally, I need to implement IEnumString to populate a list of options for the autocomplete object. Luckily, .Net has declared this interface, so I don’t need to port it to managed code, however, I still need to write my binding code in my implementation of IEnumString.

void AutocompleteBindingSource::Reset()
{
this->current = 0;
if(BindingSource!=nullptr)
this->size=BindingSource->Count;
}

int AutocompleteBindingSource::Next(
int celt, [Out, MarshalAs(UnmanagedType::LPArray, ArraySubType=UnmanagedType::LPWStr, SizeParamIndex=0)] array<String^>^ rgelt, IntPtr pceltFetched)
{
if (celt < 0)        {
return E_INVALIDARG;
}
int index = 0;
while ((this->current < this->size) && (celt > 0))
{
Object^ item=this->BindingSource->default[this->current];

bool useDisplayMember=false;

if(!String::IsNullOrEmpty(DisplayMember))
{
ICustomTypeDescriptor^ customTypeDescriptor=dynamic_cast<ICustomTypeDescriptor^>(item);
if(customTypeDescriptor!=nullptr)
{
PropertyDescriptorCollection^ propertyDescriptorCollection=
customTypeDescriptor->GetProperties();
if(propertyDescriptorCollection!=nullptr)
{
PropertyDescriptor^ propertyDescriptor=propertyDescriptorCollection->default[DisplayMember];
if(propertyDescriptor!=nullptr)
{
rgelt[index] = propertyDescriptor->GetValue(item)->ToString();
useDisplayMember=true;
}
}
}
}

if(!useDisplayMember)
{
if(item!=nullptr)
{
rgelt[index] = item->ToString();
}
}
this->current++;
index++;
celt–;
}
if ((pceltFetched != IntPtr::Zero))
{
Marshal::WriteInt32(pceltFetched, index);
}
if ((celt != 0))
{
return 1;
}
return 0;

}

Here the DisplayMember property is the name of the property in the data source to be displayed. If the property specified by the value of the DataMember property does not exist, I use ToString to get a text representation of the current item in the data source.

You may want to ask, where is the filtering code? Well, that is implemented by BindingSource class.

System::Void FormTest::textBoxDemo_TextChanged(System::Object^  sender, System::EventArgs^  e)
{
static bool inThisFunction=false;
if(!inThisFunction)
{
inThisFunction=true;
if(String::IsNullOrEmpty(textBoxDemo->Text))
bindingSourceAutoComplete->Filter=nullptr;
else
{
System::String^ addText=textBoxDemo->Text+”og/NextElement”;
dataSetDemo->Tables[0]->DefaultView->Sort=”Text”;
if(dataSetDemo->Tables[0]->DefaultView->FindRows(addText)->Length==0)
{
System::Data::DataRow^ row=dataSetDemo->Tables[0]->NewRow();
row->default[0]=addText;
dataSetDemo->Tables[0]->Rows->Add(row);
}
bindingSourceAutoComplete->Filter=
String::Format(“{0} LIKE ‘{1}%’”
,dataSetDemo->Tables[0]->Columns[0]->Caption
,textBoxDemo->Text);
}
if(textBoxDemo->SelectionStart>0)
{
autocompleteBindingSource1->Reset();
autocompleteBindingSource1->Bind();
String^ text=textBoxDemo->Text;
int selectionStart=textBoxDemo->SelectionStart;
int selectionLength=textBoxDemo->SelectionLength;
textBoxDemo->SelectionStart=0;
textBoxDemo->SelectionLength=0;
textBoxDemo->SelectAll();
System::Windows::Forms::SendKeys::SendWait(“{BACKSPACE}”);
textBoxDemo->Text=text;
textBoxDemo->SelectionStart=selectionStart-1;
textBoxDemo->SelectionLength=selectionLength+1;
System::Windows::Forms::SendKeys::SendWait(textBoxDemo->SelectedText);
}
inThisFunction=false;
}
}

Somehow Windows caches the candidate list. If I don’t clear the text in the input  box, my IEnumString implementation won’t be asked again for candidate strings (pointed out by Andy Gilman).

The BindingSource class checks the data source to see if they support the IBindingListView. If IBindingListView is supported, the BindingSource class delegates sorting and filtering to the data source.  In this sample, the data source of the BindingSource object is a DataSet, and the DataMember of BindingSource object is the name of the first table , so BindingSource creates a DataViewas its data source. The DataView class implements IBindingListView and filters its data using expressions parsed from the filter string. In reality, the data source could be a business object that implements IBindingListView and supports filtering and sorting with stored procedures.

This sample does not consider compound autocomplete object support. If you want to get your options from multiple sources, you need to use IObjMgrto add sources to the autocomplete object.

Reference

Advertisement

20 responses to “AutoComplete with DataSource”

  1. Hi Sheng,

    I’ve read a number of your responses on stackoverflow.com regarding enabling autocomplete to search the entire string, not just the beginning. You mention that one should pass ACO_NOPREFIXFILTERING to IAutoComplete2.SetOptions to make it work on Vista or higher.

    I downloaded your code and modified line 143 of AutoCompleteBindingSource.cpp from:
    this->autoComplete2->SetOptions((int)ControlToBind->AutoCompleteMode);
    to:
    this->autoComplete2->SetOptions(0x100);

    Just to see whether searching all text would work. However it doesn’t. Other options (such 0x01, etc) seem to work. Am I missing something? I have Vista Home OS and Visual Studio 2010.

    Thanks a lot in advance!

    1. You also need either the ACO_AUTOSUGGEST flag or the ACO_AUTOAPPEND flag in the option flag. If the lower 2 bit are 0, youare having ACO_NONE effectively.

  2. Hi Sheng,

    Thanks for a prompt response!

    I actually downloaded another sample you posted at http://social.msdn.microsoft.com/Forums/en/winforms/thread/e599b2e2-3cda-43ad-b15f-a69b4fea1a75 (with a link to AutoComplete.zip) and played with C# version.

    I commented out everything from TextChanged event handler in FormAutoComplete.cs, reduced the number of entries generated to 100 to make it simpler, and changed a line that calls SetOptions in InitializeEditControl() to:

    iAutoComplete2.SetOptions(0x0101);

    What happens is that all of the entries are displayed in the dropdown, i.e. they’re not filtered at all. If I change the argument back to 0x0001, then it filters on prefix, in a standard fashion.

    So I don’t think ACO_NOPREFIXFILTERING actually does any filtering with “contains” logic. It simply displays all the options without any filtering. Reading the definition at http://msdn.microsoft.com/en-us/library/bb762479%28v=vs.85%29.aspx , it actually makes sense. That’s what they’re saying — disable filtering, and always display all options:

    “Disable prefix filtering when displaying the autosuggest dropdown. Always display all suggestions.”

    Let me know what you think!

    Thanks again, Turar

    1. Yes, you either have the default BeginWith filtering or no filtering at all. If no filtering, your “contains” logic need to happen before passing the filtered candidate list to the autocomplete object.

      1. Ahh, I see. Now it’s starting to make sense. Thanks!

  3. hello,

    i’m trying to make the dropdown window of and autocomplete custome source (a string list) view on DoubleClick on the textbox.
    my problem is that i would like to see all the option in the dropdown windows (like a combobox do), but i MUST use a textbox.
    any suggestions?

    1. Not sure what you are trying to do. Are you trying to remove the BeginWith filter on candidate strings?

      1. yes, remove the BeginWith, and make the dropdown list show like a combobox do.

      2. Check the previous comments between Turar and me.

  4. i’ve remove the BeginWith, using iAutoComplete2.
    what i need now is to show the dropdown list on event (let’s say double click, or a button click)

    1. You can send keystrokes.

      1. i’ve try with SendKeys.Keys(“{ENTER}”) because i need a keystroke that do not produce text, but it does not works 😦 thanks

      2. finally i’ve made a component extending the default autocomplete function of the textbox
        http://code.google.com/p/cltextbox/

  5. sir can you tell me how i set font in datagrideview combobox autocomplete list.dot say go to setting and and change the font it wont work .it work only for combo .

    actuay im talking about autocomplete list which popup when i typing on combo

    if you can give me answer with example project dat would be greatfull sir
    thank you

    give me reply as sonn as possible im stukc on this

    1. font customization is not supported.

  6. the download is not working

  7. Hi Sheng,

    Can you tell me why your implementation won’t select the items using the up and down keys like it would normally do?

    1. I know this is three years old but in case anyone else lands here, you need to include the option ACO_UPDOWNKEYDROPSLIST when setting the options to get this to work.

  8. This works well for Suggest but doesn’t for Append. When I set the mode to Append or SuggestAppend, as soon as I reset the enumerator, it populates the textbox with the first item in the list.

Leave a Reply to Turar Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.