/*
* 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
}
}