forked from geomatics-io/ObjectListView
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVirtualObjectListView.cs
More file actions
1255 lines (1101 loc) · 55.6 KB
/
Copy pathVirtualObjectListView.cs
File metadata and controls
1255 lines (1101 loc) · 55.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* 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:
* 2015-06-14 JPP - Moved handling of CheckBoxes on virtual lists into base class (ObjectListView).
* This allows the property to be set correctly, even when set via an upcast reference.
* 2015-03-25 JPP - Subscribe to change notifications when objects are added
* 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 <http://www.gnu.org/licenses/>.
*
* 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.Diagnostics;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace BrightIdeasSoftware
{
/// <summary>
/// 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.
/// </summary>
/// <remarks><para>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.</para>
/// <para>Virtual lists can never iterate their contents. That would defeat the whole purpose.</para>
/// <para>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!</para>
/// <para>
/// 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.
/// </para>
/// <para>Due to the limits of the underlying Windows control, virtual lists do not trigger ItemCheck/ItemChecked events.
/// Use a CheckStatePutter instead.</para>
/// <para>To enable grouping, you must provide an implementation of IVirtualGroups interface, via the GroupingStrategy property.</para>
/// <para>Similarly, to enable filtering on the list, your VirtualListDataSource must also implement the IFilterableDataSource interface.</para>
/// </remarks>
public class VirtualObjectListView : ObjectListView
{
/// <summary>
/// Create a VirtualObjectListView
/// </summary>
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(this.HandleVirtualItemsSelectionRangeChanged);
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
/// <summary>
/// Gets whether or not this listview is capable of showing groups
/// </summary>
[Browsable(false)]
public override bool CanShowGroups {
get {
// Virtual lists need Vista and a grouping strategy to show groups
return (ObjectListView.IsVistaOrLater && this.GroupingStrategy != null);
}
}
/// <summary>
/// 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
/// equivalent to unchecking all.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// If the ListView is not currently showing CheckBoxes, this property does nothing. It does
/// not remember any check box settings made.
/// </para>
/// <para>
/// This class optimizes the management of CheckStates so that it will work efficiently even on
/// large lists of item. However, those optimizations are impossible if you install a CheckStateGetter.
/// With a CheckStateGetter installed, the performance of this method is O(n) where n is the size
/// of the list. This could be painfully slow.</para>
/// </remarks>
[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<Object, CheckState> 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()));
}
}
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// This property is an implementation detail and should not be altered.
/// </remarks>
protected internal bool CheckedObjectsMustStillExistInList {
get { return checkedObjectsMustStillExistInList; }
set { checkedObjectsMustStillExistInList = value; }
}
private bool checkedObjectsMustStillExistInList = true;
/// <summary>
/// Gets the collection of objects that survive any filtering that may be in place.
/// </summary>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override IEnumerable FilteredObjects {
get {
for (int i = 0; i < this.GetItemCount(); i++)
yield return this.GetModelObject(i);
}
}
/// <summary>
/// Gets or sets the strategy that will be used to create groups
/// </summary>
/// <remarks>
/// This must be provided for a virtual list to show groups.
/// </remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IVirtualGroups GroupingStrategy {
get { return this.groupingStrategy; }
set { this.groupingStrategy = value; }
}
private IVirtualGroups groupingStrategy;
/// <summary>
/// Gets whether or not the current list is filtering its contents
/// </summary>
/// <remarks>
/// This is only possible if our underlying data source supports filtering.
/// </remarks>
public override bool IsFiltering {
get {
return base.IsFiltering && (this.VirtualListDataSource is IFilterableDataSource);
}
}
/// <summary>
/// Get/set the collection of objects that this list will show
/// </summary>
/// <remarks>
/// <para>
/// The contents of the control will be updated immediately after setting this property.
/// </para>
/// <para>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.</para>
/// <para>This method is not thread safe.</para>
/// <para>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 :)</para>
/// </remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override IEnumerable Objects {
get {
IFilterableDataSource filterable = this.VirtualListDataSource as IFilterableDataSource;
try {
// If we are filtering, we have to temporarily disable filtering so we get
// the whole collection
if (filterable != null && this.UseFiltering)
filterable.ApplyFilters(null, null);
return this.FilteredObjects;
} finally {
if (filterable != null && this.UseFiltering)
filterable.ApplyFilters(this.ModelFilter, this.ListFilter);
}
}
set { base.Objects = value; }
}
/// <summary>
/// This delegate is used to fetch a rowObject, given it's index within the list
/// </summary>
/// <remarks>Only use this property if you are not using a VirtualListDataSource.</remarks>
[Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public virtual RowGetterDelegate RowGetter {
get { return ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter; }
set { ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter = value; }
}
/// <summary>
/// Should this list show its items in groups?
/// </summary>
[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;
/// <summary>
/// Get/set the data source that is behind this virtual list
/// </summary>
/// <remarks>Setting this will cause the list to redraw.</remarks>
[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;
/// <summary>
/// Gets or sets the number of rows in this virtual list.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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
/// <summary>
/// Return the number of items in the list
/// </summary>
/// <returns>the number of items in the list</returns>
public override int GetItemCount() {
return this.VirtualListSize;
}
/// <summary>
/// Return the model object at the given index
/// </summary>
/// <param name="index">Index of the model object to be returned</param>
/// <returns>A model object</returns>
public override object GetModelObject(int index) {
if (this.VirtualListDataSource != null && index >= 0 && index < this.GetItemCount())
return this.VirtualListDataSource.GetNthObject(index);
else
return null;
}
/// <summary>
/// Find the given model object within the listview and return its index
/// </summary>
/// <param name="modelObject">The model object to be found</param>
/// <returns>The index of the object. -1 means the object was not present</returns>
public override int IndexOf(Object modelObject) {
if (this.VirtualListDataSource == null || modelObject == null)
return -1;
return this.VirtualListDataSource.GetObjectIndex(modelObject);
}
/// <summary>
/// Return the OLVListItem that displays the given model object
/// </summary>
/// <param name="modelObject">The modelObject whose item is to be found</param>
/// <returns>The OLVListItem that displays the model, or null</returns>
/// <remarks>This method has O(n) performance.</remarks>
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
/// <summary>
/// Add the given collection of model objects to this control.
/// </summary>
/// <param name="modelObjects">A collection of model objects</param>
/// <remarks>
/// <para>The added objects will appear in their correct sort position, if sorting
/// is active. Otherwise, they will appear at the end of the list.</para>
/// <para>No check is performed to see if any of the objects are already in the ListView.</para>
/// <para>Null objects are silently ignored.</para>
/// </remarks>
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();
this.SubscribeNotifications(args.ObjectsToAdd);
}
finally
{
this.EndUpdate();
}
}
/// <summary>
/// Remove all items from this list
/// </summary>
/// <remark>This method can safely be called from background threads.</remark>
public override void ClearObjects() {
if (this.InvokeRequired)
this.Invoke(new MethodInvoker(this.ClearObjects));
else {
this.CheckStateMap.Clear();
this.SetObjects(new ArrayList());
}
}
/// <summary>
/// Scroll the listview so that the given group is at the top.
/// </summary>
/// <param name="groupIndex">The index of the group to be revealed</param>
/// <remarks><para>
/// If the group is already visible, the list will still be scrolled to move
/// the group to the top, if that is possible.
/// </para>
/// <para>This only works when the list is showing groups (obviously).</para>
/// </remarks>
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);
}
}
/// <summary>
/// Inserts the given collection of model objects to this control at hte given location
/// </summary>
/// <param name="index">The index where the new objects will be inserted</param>
/// <param name="modelObjects">A collection of model objects</param>
/// <remarks>
/// <para>The added objects will appear in their correct sort position, if sorting
/// is active. Otherwise, they will appear at the given position of the list.</para>
/// <para>No check is performed to see if any of the objects are already in the ListView.</para>
/// <para>Null objects are silently ignored.</para>
/// </remarks>
public override void InsertObjects(int index, ICollection modelObjects)
{
if (this.VirtualListDataSource == null)
return;
// Give the world a chance to cancel or change the added objects
ItemsAddingEventArgs args = new ItemsAddingEventArgs(index, modelObjects);
this.OnItemsAdding(args);
if (args.Canceled)
return;
try
{
this.BeginUpdate();
this.VirtualListDataSource.InsertObjects(index, args.ObjectsToAdd);
this.BuildList();
this.SubscribeNotifications(args.ObjectsToAdd);
}
finally
{
this.EndUpdate();
}
}
/// <summary>
/// Update the rows that are showing the given objects
/// </summary>
/// <remarks>This method does not resort the items.</remarks>
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();
}
}
/// <summary>
/// Update the rows that are selected
/// </summary>
/// <remarks>This method does not resort or regroup the view.</remarks>
public override void RefreshSelectedObjects() {
foreach (int index in this.SelectedIndices)
this.RedrawItems(index, index, true);
}
/// <summary>
/// Remove all of the given objects from the control
/// </summary>
/// <param name="modelObjects">Collection of objects to be removed</param>
/// <remarks>
/// <para>Nulls and model objects that are not in the ListView are silently ignored.</para>
/// <para>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 subsequently 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. </para>
/// </remarks>
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();
}
}
/// <summary>
/// Select the row that is displaying the given model object. All other rows are deselected.
/// </summary>
/// <param name="modelObject">Model object to select</param>
/// <param name="setFocus">Should the object be focused as well?</param>
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 != null)
this.SelectedItem.Focused = true;
}
/// <summary>
/// Select the rows that is displaying any of the given model object. All other rows are deselected.
/// </summary>
/// <param name="modelObjects">A collection of model objects</param>
/// <remarks>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.</remarks>
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);
}
}
/// <summary>
/// Set the collection of objects that this control will show.
/// </summary>
/// <param name="collection"></param>
/// <param name="preserveState">Should the state of the list be preserved as far as is possible.</param>
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
//
// /// <summary>
// /// Check all rows
// /// </summary>
// /// <remarks>The performance of this method is O(n) where n is the number of rows in the control.</remarks>
// 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()));
//
// }
//
// /// <summary>
// /// Uncheck all rows
// /// </summary>
// /// <remarks>The performance of this method is O(n) where n is the number of rows in the control.</remarks>
// 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()));
// }
/// <summary>
/// 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.
/// </summary>
/// <param name="modelObject"></param>
/// <returns></returns>
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
/// <summary>
/// Rebuild the list with its current contents.
/// </summary>
/// <remarks>
/// Invalidate any cached information when we rebuild the list.
/// </remarks>
public override void BuildList(bool shouldPreserveSelection) {
this.UpdateVirtualListSize();
this.ClearCachedInfo();
if (this.ShowGroups)
this.BuildGroups();
else
this.Sort();
this.Invalidate();
}
/// <summary>
/// Clear any cached info this list may have been using
/// </summary>
public override void ClearCachedInfo() {
this.lastRetrieveVirtualItemIndex = -1;
}
/// <summary>
/// Do the work of creating groups for this control
/// </summary>
/// <param name="groups"></param>
protected override void CreateGroups(IEnumerable<OLVGroup> 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);
}
}
/// <summary>
/// Do the plumbing to disable groups on a virtual list
/// </summary>
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);
}
/// <summary>
/// Do the plumbing to enable groups on a virtual list
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
/// <param name="itemIndex"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="itemToFind">The item that is before the item that is returned, or null</param>
/// <returns>A OLVListItem</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="itemToFind">The item that is before the item that is returned</param>
/// <returns>A ListViewItem</returns>
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