Search code examples
c#bindinglistaddrange

How can I improve performance of an AddRange method on a custom BindingList?


I have a custom BindingList that I want create a custom AddRange method for.

public class MyBindingList<I> : BindingList<I>
{
    ...

    public void AddRange(IEnumerable<I> vals)
    {
        foreach (I v in vals)
            Add(v);
    }
}

My problem with this is performance is terrible with large collections. The case I am debugging now is trying to add roughly 30,000 records, and taking an unacceptable amount of time.

After looking into this issue online, it seems like the problem is that the use of Add is resizing the array with each addition. This answer I think summarizes it as :

If you are using Add, it is resizing the inner array gradually as needed (doubling)

What can I do in my custom AddRange implementation to specify the size the BindingList needs to resize to be based on the item count, rather than letting it constantly re-allocate the array with each item added?


Solution

  • You can pass in a List in the constructor and make use of List<T>.Capacity.

    But i bet, the most significant speedup will come form suspending events when adding a range. So I included both things in my example code.

    Probably needs some finetuning to handle some worst cases and what not.

    public class MyBindingList<I> : BindingList<I>
    {
        private readonly List<I> _baseList;
    
        public MyBindingList() : this(new List<I>())
        {
    
        }
    
        public MyBindingList(List<I> baseList) : base(baseList)
        {
            if(baseList == null)
                throw new ArgumentNullException();            
            _baseList = baseList;
        }
    
        public void AddRange(IEnumerable<I> vals)
        {
            ICollection<I> collection = vals as ICollection<I>;
            if (collection != null)
            {
                int requiredCapacity = Count + collection.Count;
                if (requiredCapacity > _baseList.Capacity)
                    _baseList.Capacity = requiredCapacity;
            }
    
            bool restore = RaiseListChangedEvents;
            try
            {
                RaiseListChangedEvents = false;
                foreach (I v in vals)
                    Add(v); // We cant call _baseList.Add, otherwise Events wont get hooked.
            }
            finally
            {
                RaiseListChangedEvents = restore;
                if (RaiseListChangedEvents)
                    ResetBindings();
            }
        }
    }
    

    You cannot use the _baseList.AddRangesince BindingList<T> wont hook the PropertyChanged event then. You can bypass this only using Reflection by calling the private Method HookPropertyChanged for each Item after AddRange. this however only makes sence if vals (your method parameter) is a collection. Otherwise you risk enumerating the enumerable twice.

    Thats the closest you can get to "optimal" without writing your own BindingList. Which shouldnt be too dificult as you could copy the source code from BindingList and alter the parts to your needs.