TextBox Autocomplete – Crash AccessViolationException – Workaround

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

This entry was posted in Non classé and tagged , . Bookmark the permalink.

Comments are closed.