/* * FastObjectListView - A listview that behaves like an ObjectListView but has the speed of a virtual list * * Author: Phillip Piper * Date: 27/09/2008 9:15 AM * * Change log: * 2014-10-15 JPP - Fire Filter event when applying filters * v2.8 * 2012-06-11 JPP - Added more efficient version of FilteredObjects * v2.5.1 * 2011-04-25 JPP - Fixed problem with removing objects from filtered or sorted list * v2.4 * 2010-04-05 JPP - Added filtering * v2.3 * 2009-08-27 JPP - Added GroupingStrategy * - Added optimized Objects property * v2.2.1 * 2009-01-07 JPP - Made all public and protected methods virtual * 2008-09-27 JPP - Separated from ObjectListView.cs * * Copyright (C) 2006-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com. */ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Windows.Forms; namespace BrightIdeasSoftware { /// /// A FastObjectListView trades function for speed. /// /// /// On my mid-range laptop, this view builds a list of 10,000 objects in 0.1 seconds, /// as opposed to a normal ObjectListView which takes 10-15 seconds. Lists of up to 50,000 items should be /// able to be handled with sub-second response times even on low end machines. /// /// A FastObjectListView is implemented as a virtual list with many of the virtual modes limits (e.g. no sorting) /// fixed through coding. There are some functions that simply cannot be provided. Specifically, a FastObjectListView cannot: /// /// use Tile view /// show groups on XP /// /// /// public class FastObjectListView : VirtualObjectListView { /// /// Make a FastObjectListView /// public FastObjectListView() { this.VirtualListDataSource = new FastObjectListDataSource(this); this.GroupingStrategy = new FastListGroupingStrategy(); } /// /// Gets the collection of objects that survive any filtering that may be in place. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override IEnumerable FilteredObjects { get { // This is much faster than the base method return ((FastObjectListDataSource)this.VirtualListDataSource).FilteredObjectList; } } /// /// Get/set the collection of objects that this list will show /// /// /// /// The contents of the control will be updated immediately after setting this property. /// /// This method preserves selection, if possible. Use SetObjects() if /// you do not want to preserve the selection. Preserving selection is the slowest part of this /// code and performance is O(n) where n is the number of selected rows. /// This method is not thread safe. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override IEnumerable Objects { get { // This is much faster than the base method return ((FastObjectListDataSource)this.VirtualListDataSource).ObjectList; } set { base.Objects = value; } } /// /// Move the given collection of objects to the given index. /// /// This operation only makes sense on non-grouped ObjectListViews. /// /// public override void MoveObjects(int index, ICollection modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate() { this.MoveObjects(index, modelObjects); }); return; } // If any object that is going to be moved is before the point where the insertion // will occur, then we have to reduce the location of our insertion point int displacedObjectCount = 0; foreach (object modelObject in modelObjects) { int i = this.IndexOf(modelObject); if (i >= 0 && i <= index) displacedObjectCount++; } index -= displacedObjectCount; this.BeginUpdate(); try { this.RemoveObjects(modelObjects); this.InsertObjects(index, modelObjects); } finally { this.EndUpdate(); } } /// /// Remove any sorting and revert to the given order of the model objects /// /// To be really honest, Unsort() doesn't work on FastObjectListViews since /// the original ordering of model objects is lost when Sort() is called. So this method /// effectively just turns off sorting. public override void Unsort() { this.ShowGroups = false; this.PrimarySortColumn = null; this.PrimarySortOrder = SortOrder.None; this.SetObjects(this.Objects); } } /// /// Provide a data source for a FastObjectListView /// /// /// This class isn't intended to be used directly, but it is left as a public /// class just in case someone wants to subclass it. /// /// /// Create a FastObjectListDataSource /// /// public class FastObjectListDataSource(FastObjectListView listView) : AbstractVirtualListDataSource(listView) { #region IVirtualListDataSource Members /// /// Get n'th object /// /// /// public override object GetNthObject(int n) { if (n >= 0 && n < this.filteredObjectList.Count) return this.filteredObjectList[n]; return null; } /// /// How many items are in the data source /// /// public override int GetObjectCount() { return this.filteredObjectList.Count; } /// /// Get the index of the given model /// /// /// public override int GetObjectIndex(object model) { int index; if (model != null && this.objectsToIndexMap.TryGetValue(model, out index)) return index; return -1; } /// /// /// /// /// /// /// /// public override int SearchText(string text, int first, int last, OLVColumn column) { if (first <= last) { for (int i = first; i <= last; i++) { string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) return i; } } else { for (int i = first; i >= last; i--) { string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) return i; } } return -1; } /// /// /// /// /// public override void Sort(OLVColumn column, SortOrder sortOrder) { if (sortOrder != SortOrder.None) { ModelObjectComparer comparer = new ModelObjectComparer(column, sortOrder, this.listView.SecondarySortColumn, this.listView.SecondarySortOrder); this.fullObjectList.Sort(comparer); this.filteredObjectList.Sort(comparer); } this.RebuildIndexMap(); } /// /// /// /// public override void AddObjects(ICollection modelObjects) { foreach (object modelObject in modelObjects) { if (modelObject != null) this.fullObjectList.Add(modelObject); } this.FilterObjects(); this.RebuildIndexMap(); } /// /// /// /// /// public override void InsertObjects(int index, ICollection modelObjects) { this.fullObjectList.InsertRange(index, modelObjects); this.FilterObjects(); this.RebuildIndexMap(); } /// /// Remove the given collection of models from this source. /// /// public override void RemoveObjects(ICollection modelObjects) { // We have to unselect any object that is about to be deleted List indicesToRemove = new List(); foreach (object modelObject in modelObjects) { int i = this.GetObjectIndex(modelObject); if (i >= 0) indicesToRemove.Add(i); } // Sort the indices from highest to lowest so that we // remove latter ones before earlier ones. In this way, the // indices of the rows doesn't change after the deletes. indicesToRemove.Sort(); indicesToRemove.Reverse(); foreach (int i in indicesToRemove) this.listView.SelectedIndices.Remove(i); // Remove the objects from the unfiltered list foreach (object modelObject in modelObjects) this.fullObjectList.Remove(modelObject); this.FilterObjects(); this.RebuildIndexMap(); } /// /// /// /// public override void SetObjects(IEnumerable collection) { ArrayList newObjects = ObjectListView.EnumerableToArray(collection, true); this.fullObjectList = newObjects; this.FilterObjects(); this.RebuildIndexMap(); } /// /// Update/replace the nth object with the given object /// /// /// public override void UpdateObject(int index, object modelObject) { if (index < 0 || index >= this.filteredObjectList.Count) return; int i = this.fullObjectList.IndexOf(this.filteredObjectList[index]); if (i < 0) return; if (ReferenceEquals(this.fullObjectList[i], modelObject)) return; this.fullObjectList[i] = modelObject; this.filteredObjectList[index] = modelObject; this.objectsToIndexMap[modelObject] = index; } private ArrayList fullObjectList = new ArrayList(); private ArrayList filteredObjectList = new ArrayList(); private IModelFilter modelFilter; private IListFilter listFilter; #endregion #region IFilterableDataSource Members /// /// Apply the given filters to this data source. One or both may be null. /// /// /// public override void ApplyFilters(IModelFilter iModelFilter, IListFilter iListFilter) { this.modelFilter = iModelFilter; this.listFilter = iListFilter; this.SetObjects(this.fullObjectList); } #endregion #region Implementation /// /// Gets the full list of objects being used for this fast list. /// This list is unfiltered. /// public ArrayList ObjectList { get { return fullObjectList; } } /// /// Gets the list of objects from ObjectList which survive any installed filters. /// public ArrayList FilteredObjectList { get { return filteredObjectList; } } /// /// Rebuild the map that remembers which model object is displayed at which line /// protected void RebuildIndexMap() { this.objectsToIndexMap.Clear(); for (int i = 0; i < this.filteredObjectList.Count; i++) this.objectsToIndexMap[this.filteredObjectList[i]] = i; } readonly Dictionary objectsToIndexMap = new Dictionary(); /// /// Build our filtered list from our full list. /// protected void FilterObjects() { // If this list isn't filtered, we don't need to do anything else if (!this.listView.UseFiltering) { this.filteredObjectList = new ArrayList(this.fullObjectList); return; } // Tell the world to filter the objects. If they do so, don't do anything else // ReSharper disable PossibleMultipleEnumeration FilterEventArgs args = new FilterEventArgs(this.fullObjectList); this.listView.OnFilter(args); if (args.FilteredObjects != null) { this.filteredObjectList = ObjectListView.EnumerableToArray(args.FilteredObjects, false); return; } IEnumerable objects = (this.listFilter == null) ? this.fullObjectList : this.listFilter.Filter(this.fullObjectList); // Apply the object filter if there is one if (this.modelFilter == null) { this.filteredObjectList = ObjectListView.EnumerableToArray(objects, false); } else { this.filteredObjectList = new ArrayList(); foreach (object model in objects) { if (this.modelFilter.Filter(model)) this.filteredObjectList.Add(model); } } } #endregion } }