It seems a nogo, but there’s a bug with AutoCompletion on Winforms since years. (.net7 here)
The only way to make it work is to set the source only once.
If you try to have a dynamic Suggestion, you could set :Text_OnChanged(object e)
{
AutoCompleteStringCollection.AddRange("hello");
// CRASH AccessViolationException
}
It will crash with AccessViolationException.
Based on my Winforms TextBox.cs reading, I found an elegant solution.
1. Set private field _fromHandleCreate to true after HandleCreated
public class MyTextBox : TextBox
{
FieldInfo? fiFromHandleCreate = typeof(TextBox).GetField("_fromHandleCreate",
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.SetField |
BindingFlags.GetField);
protected bool IsFromHandleCreate
{
get => (bool)fiFromHandleCreate.GetValue(this);
set => fiFromHandleCreate.SetValue(this, value);
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
IsFromHandleCreate = true; //important to not destroy AutoComplete handle, avoid flickering
}
}
2. Redefine AutoCompleteStringCollection
The main trick is there’s always at least one element : String.Empty, because when the collections is cleared, Autocompletion is disconnected from the Textbox (and recreation bring bug)
The second trick is to set RemovedItem to String.empty instead of removing them
The third trick is to occupate empty Slot when Adding to reduce memory consumption
/// <summary>
/// Make AutoCompleteStringCollection dynamic
/// </summary>
public class MyAutoCompleteStringCollection : AutoCompleteStringCollection
{
static readonly string EmptyEntry = String.Empty;//space magic string
public MyAutoCompleteStringCollection()
{
this.Add(EmptyEntry);//never delete this entry
}
FieldInfo? fidata = typeof(AutoCompleteStringCollection).GetField("data",
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.SetField |
BindingFlags.GetField);
/// <summary>
/// base.data private field
/// </summary>
protected ArrayList Data
{
get => (ArrayList)fidata.GetValue(this);
set => fidata.SetValue(this, value);
}
/// <summary>
/// base.Clear is Evil => it disconnect the Autocompletion....:(
/// </summary>
public new void Clear()
{
var vData = this.Data;
vData.RemoveRange(1, this.Count-1);
//notify
if (this.Count>1)
base.OnCollectionChanged(new System.ComponentModel.CollectionChangeEventArgs(System.ComponentModel.CollectionChangeAction.Refresh, null));
}
/// <summary>
/// Remove but without removing the String.Empty rentry
/// </summary>
/// <param name="value"></param>
public new void Remove(string value)
{
int index = IndexOf(value);
if (index > 0) //keep String.Empty
base[index] = string.Empty; // clean slot + notify
}
/// <summary>
/// RemoveAt but without removing the String.Empty rentry
/// </summary>
public new void RemoveAt(int index)
{
if (index > 0)//keep String.Empty
base[index] = string.Empty; // clean slot + notify
}
/// <summary>
/// Add to an unoccuped slot or a new one
/// </summary>
public new void Add(string text)
{
int index = -1;
for (int i = 1; i < Count; i++)
{
if (this[i] == string.Empty)
{
index = i;
break;
}
}
if (index > 0)
base[index] = text; // occupate the slot + notify
else
base.Add(text); // add+notify
}
/// <summary>
/// AddRange with efficient slot occupating
/// </summary>
/// <param name="values"></param>
public new void AddRange(string[] values)
{
var vData = this.Data;
int cpt = 0;
for (int x = 0; x < values.Length; x++)
{
int index = -1;
for (int i = 1; i < Count; i++)
{
if (vData[i] == string.Empty)
{
index = i;
break;
}
}
if (index > 0)
{
vData[index] = values[x]; // occupate the slot
cpt++;
}
else
break;
}
//Send last part via AddRange
if (cpt < values.Length)
vData.AddRange(values.Skip(cpt).ToArray());
if(values?.Length>0) //notify
base.OnCollectionChanged(new System.ComponentModel.CollectionChangeEventArgs(System.ComponentModel.CollectionChangeAction.Refresh, null));
}
}
Et voilà !
Stephane