/* * VirtualObjectListView - A virtual listview delays fetching model objects until they are actually displayed. * * Author: Phillip Piper * Date: 27/09/2008 9:15 AM * * Change log: * v2.8 * 2014-09-26 JPP - Correct an incorrect use of checkStateMap when setting CheckedObjects * and a CheckStateGetter is installed * v2.6 * 2012-06-13 JPP - Corrected several bugs related to groups on virtual lists. * - Added EnsureNthGroupVisible() since EnsureGroupVisible() can't work on virtual lists. * v2.5.1 * 2012-05-04 JPP - Avoid bug/feature in ListView.VirtalListSize setter that causes flickering * when the size of the list changes. * 2012-04-24 JPP - Fixed bug that occurred when adding/removing item while the view was grouped. * v2.5 * 2011-05-31 JPP - Setting CheckedObjects is more efficient on large collections * 2011-04-05 JPP - CheckedObjects now only returns objects that are currently in the list. * ClearObjects() now resets all check state info. * 2011-03-31 JPP - Filtering on grouped virtual lists no longer behaves strangely. * 2011-03-17 JPP - Virtual lists can (finally) set CheckBoxes back to false if it has been set to true. * (this is a little hacky and may not work reliably). * - GetNextItem() and GetPreviousItem() now work on grouped virtual lists. * 2011-03-08 JPP - BREAKING CHANGE: 'DataSource' was renamed to 'VirtualListDataSource'. This was necessary * to allow FastDataListView which is both a DataListView AND a VirtualListView -- * which both used a 'DataSource' property :( * v2.4 * 2010-04-01 JPP - Support filtering * v2.3 * 2009-08-28 JPP - BIG CHANGE. Virtual lists can now have groups! * - Objects property now uses "yield return" -- much more efficient for big lists * 2009-08-07 JPP - Use new scheme for formatting rows/cells * v2.2.1 * 2009-07-24 JPP - Added specialised version of RefreshSelectedObjects() which works efficiently with virtual lists * (thanks to chriss85 for finding this bug) * 2009-07-03 JPP - Standardized code format * v2.2 * 2009-04-06 JPP - ClearObjects() now works again * v2.1 * 2009-02-24 JPP - Removed redundant OnMouseDown() since checkbox * handling is now handled in the base class * 2009-01-07 JPP - Made all public and protected methods virtual * 2008-12-07 JPP - Trigger Before/AfterSearching events * 2008-11-15 JPP - Fixed some caching issues * 2008-11-05 JPP - Rewrote handling of check boxes * 2008-10-28 JPP - Handle SetSelectedObjects(null) * 2008-10-02 JPP - MAJOR CHANGE: Use IVirtualListDataSource * 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@bigfoot.com. */ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Reflection; using System.Windows.Forms; using System.Runtime.InteropServices; namespace BrightIdeasSoftware { /// /// A virtual object list view operates in virtual mode, that is, it only gets model objects for /// a row when it is needed. This gives it the ability to handle very large numbers of rows with /// minimal resources. /// /// A listview is not a great user interface for a large number of items. But if you've /// ever wanted to have a list with 10 million items, go ahead, knock yourself out. /// Virtual lists can never iterate their contents. That would defeat the whole purpose. /// Animated GIFs should not be used in virtual lists. Animated GIFs require some state /// information to be stored for each animation, but virtual lists specifically do not keep any state information. /// In any case, you really do not want to keep state information for 10 million animations! /// /// Although it isn't documented, .NET virtual lists cannot have checkboxes. This class codes around this limitation, /// but you must use the functions provided by ObjectListView: CheckedObjects, CheckObject(), UncheckObject() and their friends. /// If you use the normal check box properties (CheckedItems or CheckedIndicies), they will throw an exception, since the /// list is in virtual mode, and .NET "knows" it can't handle checkboxes in virtual mode. /// /// Due to the limits of the underlying Windows control, virtual lists do not trigger ItemCheck/ItemChecked events. /// Use a CheckStatePutter instead. /// To enable grouping, you must provide an implmentation of IVirtualGroups interface, via the GroupingStrategy property. /// Similarly, to enable filtering on the list, your VirtualListDataSource must also implement the IFilterableDataSource interface. /// public class VirtualObjectListView : ObjectListView { /// /// Create a VirtualObjectListView /// public VirtualObjectListView() : base() { this.VirtualMode = true; // Virtual lists have to be virtual -- no prizes for guessing that :) this.CacheVirtualItems += new CacheVirtualItemsEventHandler(this.HandleCacheVirtualItems); this.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(this.HandleRetrieveVirtualItem); this.SearchForVirtualItem += new SearchForVirtualItemEventHandler(this.HandleSearchForVirtualItem); // At the moment, we don't need to handle this event. But we'll keep this comment to remind us about it. //this.VirtualItemsSelectionRangeChanged += new ListViewVirtualItemsSelectionRangeChangedEventHandler(VirtualObjectListView_VirtualItemsSelectionRangeChanged); this.VirtualListDataSource = new VirtualListVersion1DataSource(this); // Virtual lists have to manage their own check state, since the normal ListView control // doesn't even allow checkboxes on virtual lists this.PersistentCheckBoxes = true; } #region Public Properties /// /// Gets whether or not this listview is capabale of showing groups /// [Browsable(false)] public override bool CanShowGroups { get { // Virtual lists need Vista and a grouping strategy to show groups return (ObjectListView.IsVistaOrLater && this.GroupingStrategy != null); } } /// /// Gets or sets whether this ObjectListView will show checkboxes in the primary column /// /// Due to code in the base ListView class, turning off CheckBoxes on a virtual /// list always throws an InvalidOperationException. This implementation codes around /// that limitation. [Category("Appearance"), Description("Should the list view show checkboxes?"), DefaultValue(false)] new public bool CheckBoxes { get { return base.CheckBoxes; } set { try { base.CheckBoxes = value; } catch (InvalidOperationException) { this.StateImageList = null; this.VirtualMode = false; base.CheckBoxes = value; this.VirtualMode = true; this.ShowGroups = this.ShowGroups; this.BuildList(true); } } } /// /// Get or set the collection of model objects that are checked. /// When setting this property, any row whose model object isn't /// in the given collection will be unchecked. Setting to null is /// equivilent to unchecking all. /// /// /// /// This property returns a simple collection. Changes made to the returned /// collection do NOT affect the list. This is different to the behaviour of /// CheckedIndicies collection. /// /// /// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects. /// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus /// the number of objects to be checked. /// /// /// If the ListView is not currently showing CheckBoxes, this property does nothing. It does /// not remember any check box settings made. /// /// /// This class optimizes the mangement of CheckStates so that it will work efficiently even on /// large lists of item. However, those optimizations are impossible if you install a CheckStateGetter. /// Witha CheckStateGetter installed, the performance of this method is O(n) where n is the size /// of the list. This could be painfully slow. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override IList CheckedObjects { get { // If we aren't should checkboxes, then no objects can be checked if (!this.CheckBoxes) return new ArrayList(); // If the data source has somehow vanished, we can't do anything if (this.VirtualListDataSource == null) return new ArrayList(); // If a custom check state getter is install, we can't use our check state management // We have to use the (slower) base version. if (this.CheckStateGetter != null) return base.CheckedObjects; // Collect items that are checked AND that still exist in the list. ArrayList objects = new ArrayList(); foreach (KeyValuePair kvp in this.CheckStateMap) { if (kvp.Value == CheckState.Checked && (!this.CheckedObjectsMustStillExistInList || this.VirtualListDataSource.GetObjectIndex(kvp.Key) >= 0)) objects.Add(kvp.Key); } return objects; } set { if (!this.CheckBoxes) return; // If a custom check state getter is install, we can't use our check state management // We have to use the (slower) base version. if (this.CheckStateGetter != null) { base.CheckedObjects = value; return; } Stopwatch sw = Stopwatch.StartNew(); // Set up an efficient way of testing for the presence of a particular model Hashtable table = new Hashtable(this.GetItemCount()); if (value != null) { foreach (object x in value) table[x] = true; } this.BeginUpdate(); // Uncheck anything that is no longer checked Object[] keys = new Object[this.CheckStateMap.Count]; this.CheckStateMap.Keys.CopyTo(keys, 0); foreach (Object key in keys) { if (!table.Contains(key)) this.SetObjectCheckedness(key, CheckState.Unchecked); } // Check all the new checked objects foreach (Object x in table.Keys) this.SetObjectCheckedness(x, CheckState.Checked); this.EndUpdate(); Debug.WriteLine(String.Format("PERF - Setting virtual CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); } } /// /// Gets or sets whether or not an object will be included in the CheckedObjects /// collection, even if it is not present in the control at the moment /// /// /// This property is an implementation detail and should not be altered. /// protected internal bool CheckedObjectsMustStillExistInList { get { return checkedObjectsMustStillExistInList; } set { checkedObjectsMustStillExistInList = value; } } private bool checkedObjectsMustStillExistInList = true; /// /// Gets the collection of objects that survive any filtering that may be in place. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override IEnumerable FilteredObjects { get { for (int i = 0; i < this.GetItemCount(); i++) yield return this.GetModelObject(i); } } /// /// Gets or sets the strategy that will be used to create groups /// /// /// This must be provided for a virtual list to show groups. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IVirtualGroups GroupingStrategy { get { return this.groupingStrategy; } set { this.groupingStrategy = value; } } private IVirtualGroups groupingStrategy; /// /// Gets whether or not the current list is filtering its contents /// /// /// This is only possible if our underlying data source supports filtering. /// public override bool IsFiltering { get { return base.IsFiltering && (this.VirtualListDataSource is IFilterableDataSource); } } /// /// Get/set the collection of objects that this list will show /// /// /// /// The contents of the control will be updated immediately after setting this property. /// /// Setting this property preserves selection, if possible. Use SetObjects() if /// you do not want to preserve the selection. Preserving selection is the slowest part of this /// code -- performance is O(n) where n is the number of selected rows. /// This method is not thread safe. /// The property DOES work on virtual lists, but if you try to iterate through a list /// of 10 million objects, it may take some time :) /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override IEnumerable Objects { get { try { // If we are filtering, we have to temporarily disable filtering so we get // the whole collection if (this.IsFiltering) ((IFilterableDataSource)this.VirtualListDataSource).ApplyFilters(null, null); return this.FilteredObjects; } finally { if (this.IsFiltering) ((IFilterableDataSource)this.VirtualListDataSource).ApplyFilters(this.ModelFilter, this.ListFilter); } } set { base.Objects = value; } } /// /// This delegate is used to fetch a rowObject, given it's index within the list /// /// Only use this property if you are not using a VirtualListDataSource. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual RowGetterDelegate RowGetter { get { return ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter; } set { ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter = value; } } /// /// Should this list show its items in groups? /// [Category("Appearance"), Description("Should the list view show items in groups?"), DefaultValue(true)] override public bool ShowGroups { get { // Pre-Vista, virtual lists cannot show groups return ObjectListView.IsVistaOrLater && this.showGroups; } set { this.showGroups = value; if (this.Created && !value) this.DisableVirtualGroups(); } } private bool showGroups; /// /// Get/set the data source that is behind this virtual list /// /// Setting this will cause the list to redraw. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual IVirtualListDataSource VirtualListDataSource { get { return this.virtualListDataSource; } set { this.virtualListDataSource = value; this.CustomSorter = delegate(OLVColumn column, SortOrder sortOrder) { this.ClearCachedInfo(); this.virtualListDataSource.Sort(column, sortOrder); }; this.BuildList(false); } } private IVirtualListDataSource virtualListDataSource; /// /// Gets or sets the number of rows in this virtual list. /// /// /// There is an annoying feature/bug in the .NET ListView class. /// When you change the VirtualListSize property, it always scrolls so /// that the focused item is the top item. This is annoying since it makes /// the virtual list seem to flicker as the control scrolls to show the focused /// item and then scrolls back to where ObjectListView wants it to be. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] protected new virtual int VirtualListSize { get { return base.VirtualListSize; } set { if (value == this.VirtualListSize || value < 0) return; // Get around the 'private' marker on 'virtualListSize' field using reflection if (virtualListSizeFieldInfo == null) { virtualListSizeFieldInfo = typeof(ListView).GetField("virtualListSize", BindingFlags.NonPublic | BindingFlags.Instance); System.Diagnostics.Debug.Assert(virtualListSizeFieldInfo != null); } // Set the base class private field so that it keeps on working virtualListSizeFieldInfo.SetValue(this, value); // Send a raw message to change the virtual list size *without* changing the scroll position if (this.IsHandleCreated && !this.DesignMode) NativeMethods.SetItemCount(this, value); } } static private FieldInfo virtualListSizeFieldInfo; #endregion #region OLV accessing /// /// Return the number of items in the list /// /// the number of items in the list public override int GetItemCount() { return this.VirtualListSize; } /// /// Return the model object at the given index /// /// Index of the model object to be returned /// A model object public override object GetModelObject(int index) { if (this.VirtualListDataSource != null && index >= 0 && index < this.GetItemCount()) return this.VirtualListDataSource.GetNthObject(index); else return null; } /// /// Find the given model object within the listview and return its index /// /// The model object to be found /// The index of the object. -1 means the object was not present public override int IndexOf(Object modelObject) { if (this.VirtualListDataSource == null || modelObject == null) return -1; return this.VirtualListDataSource.GetObjectIndex(modelObject); } /// /// Return the OLVListItem that displays the given model object /// /// The modelObject whose item is to be found /// The OLVListItem that displays the model, or null /// This method has O(n) performance. public override OLVListItem ModelToItem(object modelObject) { if (this.VirtualListDataSource == null || modelObject == null) return null; int index = this.VirtualListDataSource.GetObjectIndex(modelObject); return index >= 0 ? this.GetItem(index) : null; } #endregion #region Object manipulation /// /// Add the given collection of model objects to this control. /// /// A collection of model objects /// /// The added objects will appear in their correct sort position, if sorting /// is active. Otherwise, they will appear at the end of the list. /// No check is performed to see if any of the objects are already in the ListView. /// Null objects are silently ignored. /// public override void AddObjects(ICollection modelObjects) { if (this.VirtualListDataSource == null) return; // Give the world a chance to cancel or change the added objects ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); this.OnItemsAdding(args); if (args.Canceled) return; try { this.BeginUpdate(); this.VirtualListDataSource.AddObjects(args.ObjectsToAdd); this.BuildList(); } finally { this.EndUpdate(); } } /// /// Remove all items from this list /// /// This method can safely be called from background threads. public override void ClearObjects() { if (this.InvokeRequired) this.Invoke(new MethodInvoker(this.ClearObjects)); else { this.CheckStateMap.Clear(); this.SetObjects(new ArrayList()); } } /// /// Scroll the listview so that the given group is at the top. /// /// The index of the group to be revealed /// /// If the group is already visible, the list will still be scrolled to move /// the group to the top, if that is possible. /// /// This only works when the list is showing groups (obviously). /// public virtual void EnsureNthGroupVisible(int groupIndex) { if (!this.ShowGroups) return; if (groupIndex <= 0 || groupIndex >= this.OLVGroups.Count) { // There is no easy way to scroll back to the beginning of the list int delta = 0 - NativeMethods.GetScrollPosition(this, false); NativeMethods.Scroll(this, 0, delta); } else { // Find the display rectangle of the last item in the previous group OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; int lastItemInGroup = this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1); Rectangle r = this.GetItemRect(lastItemInGroup); // Scroll so that the last item of the previous group is just out of sight, // which will make the desired group header visible. int delta = r.Y + r.Height / 2; NativeMethods.Scroll(this, 0, delta); } } /// /// Update the rows that are showing the given objects /// /// This method does not resort the items. public override void RefreshObjects(IList modelObjects) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); return; } // Without a data source, we can't do this. if (this.VirtualListDataSource == null) return; try { this.BeginUpdate(); this.ClearCachedInfo(); foreach (object modelObject in modelObjects) { int index = this.VirtualListDataSource.GetObjectIndex(modelObject); if (index >= 0) { this.VirtualListDataSource.UpdateObject(index, modelObject); this.RedrawItems(index, index, true); } } } finally { this.EndUpdate(); } } /// /// Update the rows that are selected /// /// This method does not resort or regroup the view. public override void RefreshSelectedObjects() { foreach (int index in this.SelectedIndices) this.RedrawItems(index, index, true); } /// /// Remove all of the given objects from the control /// /// Collection of objects to be removed /// /// Nulls and model objects that are not in the ListView are silently ignored. /// Due to problems in the underlying ListView, if you remove all the objects from /// the control using this method and the list scroll vertically when you do so, /// then when you subsequenially add more objects to the control, /// the vertical scroll bar will become confused and the control will draw one or more /// blank lines at the top of the list. /// public override void RemoveObjects(ICollection modelObjects) { if (this.VirtualListDataSource == null) return; // Give the world a chance to cancel or change the removed objects ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); this.OnItemsRemoving(args); if (args.Canceled) return; try { this.BeginUpdate(); this.VirtualListDataSource.RemoveObjects(args.ObjectsToRemove); this.BuildList(); this.UnsubscribeNotifications(args.ObjectsToRemove); } finally { this.EndUpdate(); } } /// /// Select the row that is displaying the given model object. All other rows are deselected. /// /// Model object to select /// Should the object be focused as well? public override void SelectObject(object modelObject, bool setFocus) { // Without a data source, we can't do this. if (this.VirtualListDataSource == null) return; // Check that the object is in the list (plus not all data sources can locate objects) int index = this.VirtualListDataSource.GetObjectIndex(modelObject); if (index < 0 || index >= this.VirtualListSize) return; // If the given model is already selected, don't do anything else (prevents an flicker) if (this.SelectedIndices.Count == 1 && this.SelectedIndices[0] == index) return; // Finally, select the row this.SelectedIndices.Clear(); this.SelectedIndices.Add(index); if (setFocus) this.SelectedItem.Focused = true; } /// /// Select the rows that is displaying any of the given model object. All other rows are deselected. /// /// A collection of model objects /// This method has O(n) performance where n is the number of model objects passed. /// Do not use this to select all the rows in the list -- use SelectAll() for that. public override void SelectObjects(IList modelObjects) { // Without a data source, we can't do this. if (this.VirtualListDataSource == null) return; this.SelectedIndices.Clear(); if (modelObjects == null) return; foreach (object modelObject in modelObjects) { int index = this.VirtualListDataSource.GetObjectIndex(modelObject); if (index >= 0 && index < this.VirtualListSize) this.SelectedIndices.Add(index); } } /// /// Set the collection of objects that this control will show. /// /// /// Should the state of the list be preserved as far as is possible. public override void SetObjects(IEnumerable collection, bool preserveState) { if (this.InvokeRequired) { this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); return; } if (this.VirtualListDataSource == null) return; // Give the world a chance to cancel or change the assigned collection ItemsChangingEventArgs args = new ItemsChangingEventArgs(null, collection); this.OnItemsChanging(args); if (args.Canceled) return; this.BeginUpdate(); try { this.VirtualListDataSource.SetObjects(args.NewObjects); this.BuildList(); this.UpdateNotificationSubscriptions(args.NewObjects); } finally { this.EndUpdate(); } } #endregion #region Check boxes // // /// // /// Check all rows // /// // /// The performance of this method is O(n) where n is the number of rows in the control. // public override void CheckAll() // { // if (!this.CheckBoxes) // return; // // Stopwatch sw = Stopwatch.StartNew(); // // this.BeginUpdate(); // // foreach (Object x in this.Objects) // this.SetObjectCheckedness(x, CheckState.Checked); // // this.EndUpdate(); // // Debug.WriteLine(String.Format("PERF - CheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); // // } // // /// // /// Uncheck all rows // /// // /// The performance of this method is O(n) where n is the number of rows in the control. // public override void UncheckAll() // { // if (!this.CheckBoxes) // return; // // Stopwatch sw = Stopwatch.StartNew(); // // this.BeginUpdate(); // // foreach (Object x in this.Objects) // this.SetObjectCheckedness(x, CheckState.Unchecked); // // this.EndUpdate(); // // Debug.WriteLine(String.Format("PERF - UncheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); // } /// /// Get the checkedness of an object from the model. Returning null means the /// model does know and the value from the control will be used. /// /// /// protected override CheckState? GetCheckState(object modelObject) { if (this.CheckStateGetter != null) return base.GetCheckState(modelObject); CheckState state; if (modelObject != null && this.CheckStateMap.TryGetValue(modelObject, out state)) return state; return CheckState.Unchecked; } #endregion #region Implementation /// /// Rebuild the list with its current contents. /// /// /// Invalidate any cached information when we rebuild the list. /// public override void BuildList(bool shouldPreserveSelection) { this.UpdateVirtualListSize(); this.ClearCachedInfo(); if (this.ShowGroups) this.BuildGroups(); else this.Sort(); this.Invalidate(); } /// /// Clear any cached info this list may have been using /// public override void ClearCachedInfo() { this.lastRetrieveVirtualItemIndex = -1; } /// /// Do the work of creating groups for this control /// /// protected override void CreateGroups(IEnumerable groups) { // In a virtual list, we cannot touch the Groups property. // It was obviously not written for virtual list and often throws exceptions. NativeMethods.ClearGroups(this); this.EnableVirtualGroups(); foreach (OLVGroup group in groups) { System.Diagnostics.Debug.Assert(group.Items.Count == 0, "Groups in virtual lists cannot set Items. Use VirtualItemCount instead."); System.Diagnostics.Debug.Assert(group.VirtualItemCount > 0, "VirtualItemCount must be greater than 0."); group.InsertGroupNewStyle(this); } } /// /// Do the plumbing to disable groups on a virtual list /// protected void DisableVirtualGroups() { NativeMethods.ClearGroups(this); //System.Diagnostics.Debug.WriteLine(err); const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 0, 0); //System.Diagnostics.Debug.WriteLine(x); const int LVM_SETOWNERDATACALLBACK = 0x10BB; IntPtr x2 = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, 0, 0); //System.Diagnostics.Debug.WriteLine(x2); } /// /// Do the plumbing to enable groups on a virtual list /// protected void EnableVirtualGroups() { // We need to implement the IOwnerDataCallback interface if (this.ownerDataCallbackImpl == null) this.ownerDataCallbackImpl = new OwnerDataCallbackImpl(this); const int LVM_SETOWNERDATACALLBACK = 0x10BB; IntPtr ptr = Marshal.GetComInterfaceForObject(ownerDataCallbackImpl, typeof(IOwnerDataCallback)); IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, ptr, 0); //System.Diagnostics.Debug.WriteLine(x); Marshal.Release(ptr); const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 1, 0); //System.Diagnostics.Debug.WriteLine(x); } private OwnerDataCallbackImpl ownerDataCallbackImpl; /// /// Return the position of the given itemIndex in the list as it currently shown to the user. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// /// public override int GetDisplayOrderOfItemIndex(int itemIndex) { if (!this.ShowGroups) return itemIndex; int groupIndex = this.GroupingStrategy.GetGroup(itemIndex); int displayIndex = 0; for (int i = 0; i < groupIndex - 1; i++) displayIndex += this.OLVGroups[i].VirtualItemCount; displayIndex += this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemIndex); return displayIndex; } /// /// Return the last item in the order they are shown to the user. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// public override OLVListItem GetLastItemInDisplayOrder() { if (!this.ShowGroups) return base.GetLastItemInDisplayOrder(); if (this.OLVGroups.Count > 0) { OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; if (lastGroup.VirtualItemCount > 0) return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); } return null; } /// /// Return the n'th item (0-based) in the order they are shown to the user. /// If the control is not grouped, the display order is the same as the /// sorted list order. But if the list is grouped, the display order is different. /// /// /// public override OLVListItem GetNthItemInDisplayOrder(int n) { if (!this.ShowGroups || this.OLVGroups == null || this.OLVGroups.Count == 0) return this.GetItem(n); foreach (OLVGroup group in this.OLVGroups) { if (n < group.VirtualItemCount) return this.GetItem(this.GroupingStrategy.GetGroupMember(group, n)); n -= group.VirtualItemCount; } return null; } /// /// Return the ListViewItem that appears immediately after the given item. /// If the given item is null, the first item in the list will be returned. /// Return null if the given item is the last item. /// /// The item that is before the item that is returned, or null /// A OLVListItem public override OLVListItem GetNextItem(OLVListItem itemToFind) { if (!this.ShowGroups) return base.GetNextItem(itemToFind); // Sanity if (this.OLVGroups == null || this.OLVGroups.Count == 0) return null; // If the given item is null, return the first member of the first group if (itemToFind == null) { return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[0], 0)); } // Find where this item occurs (which group and where in that group) int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); // If it's not the last member, just return the next member if (indexWithinGroup < this.OLVGroups[groupIndex].VirtualItemCount - 1) return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup + 1)); // The item is the last member of its group. Return the first member of the next group // (unless there isn't a next group) if (groupIndex < this.OLVGroups.Count - 1) return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex + 1], 0)); return null; } /// /// Return the ListViewItem that appears immediately before the given item. /// If the given item is null, the last item in the list will be returned. /// Return null if the given item is the first item. /// /// The item that is before the item that is returned /// A ListViewItem public override OLVListItem GetPreviousItem(OLVListItem itemToFind) { if (!this.ShowGroups) return base.GetPreviousItem(itemToFind); // Sanity if (this.OLVGroups == null || this.OLVGroups.Count == 0) return null; // If the given items is null, return the last member of the last group if (itemToFind == null) { OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); } // Find where this item occurs (which group and where in that group) int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); // If it's not the first member of the group, just return the previous member if (indexWithinGroup > 0) return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup - 1)); // The item is the first member of its group. Return the last member of the previous group // (if there is one) if (groupIndex > 0) { OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; return this.GetItem(this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1)); } return null; } /// /// Make a list of groups that should be shown according to the given parameters /// /// /// protected override IList MakeGroups(GroupingParameters parms) { if (this.GroupingStrategy == null) return new List(); else return this.GroupingStrategy.GetGroups(parms); } /// /// Create a OLVListItem for given row index /// /// The index of the row that is needed /// An OLVListItem public virtual OLVListItem MakeListViewItem(int itemIndex) { OLVListItem olvi = new OLVListItem(this.GetModelObject(itemIndex)); this.FillInValues(olvi, olvi.RowObject); this.PostProcessOneRow(itemIndex, this.GetDisplayOrderOfItemIndex(itemIndex), olvi); if (this.HotRowIndex == itemIndex) this.UpdateHotRow(olvi); return olvi; } /// /// On virtual lists, this cannot work. /// protected override void PostProcessRows() { } /// /// Record the change of checkstate for the given object in the model. /// This does not update the UI -- only the model /// /// /// /// The check state that was recorded and that should be used to update /// the control. protected override CheckState PutCheckState(object modelObject, CheckState state) { state = base.PutCheckState(modelObject, state); this.CheckStateMap[modelObject] = state; return state; } /// /// Refresh the given item in the list /// /// The item to refresh public override void RefreshItem(OLVListItem olvi) { this.ClearCachedInfo(); this.RedrawItems(olvi.Index, olvi.Index, true); } /// /// Change the size of the list /// /// protected virtual void SetVirtualListSize(int newSize) { if (newSize < 0 || this.VirtualListSize == newSize) return; int oldSize = this.VirtualListSize; this.ClearCachedInfo(); // There is a bug in .NET when a virtual ListView is cleared // (i.e. VirtuaListSize set to 0) AND it is scrolled vertically: the scroll position // is wrong when the list is next populated. To avoid this, before // clearing a virtual list, we make sure the list is scrolled to the top. // [6 weeks later] Damn this is a pain! There are cases where this can also throw exceptions! try { if (newSize == 0 && this.TopItemIndex > 0) this.TopItemIndex = 0; } catch (Exception) { // Ignore any failures } // In strange cases, this can throw the exceptions too. The best we can do is ignore them :( try { this.VirtualListSize = newSize; } catch (ArgumentOutOfRangeException) { // pass } catch (NullReferenceException) { // pass } // Tell the world that the size of the list has changed this.OnItemsChanged(new ItemsChangedEventArgs(oldSize, this.VirtualListSize)); } /// /// Take ownership of the 'objects' collection. This separates our collection from the source. /// /// /// /// This method /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject /// calls will modify our collection and not the original colleciton. /// /// /// VirtualObjectListViews always own their collections, so this is a no-op. /// /// protected override void TakeOwnershipOfObjects() { } /// /// Change the state of the control to reflect changes in filtering /// protected override void UpdateFiltering() { IFilterableDataSource filterable = this.VirtualListDataSource as IFilterableDataSource; if (filterable == null) return; this.BeginUpdate(); try { int originalSize = this.VirtualListSize; filterable.ApplyFilters(this.ModelFilter, this.ListFilter); this.BuildList(); //// If the filtering actually did something, rebuild the groups if they are being shown //if (originalSize != this.VirtualListSize && this.ShowGroups) // this.BuildGroups(); } finally { this.EndUpdate(); } } /// /// Change the size of the virtual list so that it matches its data source /// public virtual void UpdateVirtualListSize() { if (this.VirtualListDataSource != null) this.SetVirtualListSize(this.VirtualListDataSource.GetObjectCount()); } #endregion #region Event handlers /// /// Handle the CacheVirtualItems event /// /// /// protected virtual void HandleCacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { if (this.VirtualListDataSource != null) this.VirtualListDataSource.PrepareCache(e.StartIndex, e.EndIndex); } /// /// Handle a RetrieveVirtualItem /// /// /// protected virtual void HandleRetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { // .NET 2.0 seems to generate a lot of these events. Before drawing *each* sub-item, // this event is triggered 4-8 times for the same index. So we save lots of CPU time // by caching the last result. //System.Diagnostics.Debug.WriteLine(String.Format("HandleRetrieveVirtualItem({0})", e.ItemIndex)); if (this.lastRetrieveVirtualItemIndex != e.ItemIndex) { this.lastRetrieveVirtualItemIndex = e.ItemIndex; this.lastRetrieveVirtualItem = this.MakeListViewItem(e.ItemIndex); } e.Item = this.lastRetrieveVirtualItem; } /// /// Handle the SearchForVirtualList event, which is called when the user types into a virtual list /// /// /// protected virtual void HandleSearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e) { // The event has e.IsPrefixSearch, but as far as I can tell, this is always false (maybe that's different under Vista) // So we ignore IsPrefixSearch and IsTextSearch and always to a case insensitve prefix match. // We can't do anything if we don't have a data source if (this.VirtualListDataSource == null) return; // Where should we start searching? If the last row is focused, the SearchForVirtualItemEvent starts searching // from the next row, which is actually an invalidate index -- so we make sure we never go past the last object. int start = Math.Min(e.StartIndex, this.VirtualListDataSource.GetObjectCount() - 1); // Give the world a chance to fiddle with or completely avoid the searching process BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(e.Text, start); this.OnBeforeSearching(args); if (args.Canceled) return; // Do the search int i = this.FindMatchingRow(args.StringToFind, args.StartSearchFrom, e.Direction); // Tell the world that a search has occurred AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(args.StringToFind, i); this.OnAfterSearching(args2); // If we found a match, tell the event if (i != -1) e.Index = i; } /// /// Find the first row in the given range of rows that prefix matches the string value of the given column. /// /// /// /// /// /// The index of the matched row, or -1 protected override int FindMatchInRange(string text, int first, int last, OLVColumn column) { return this.VirtualListDataSource.SearchText(text, first, last, column); } #endregion #region Variable declaractions private OLVListItem lastRetrieveVirtualItem; private int lastRetrieveVirtualItemIndex = -1; #endregion } }