Added a sortable variant of the BindingList class (useful in getting DataGridViews to be not quite so silly)

git-svn-id: file:///srv/devel/repo-conversion/nusu@342 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2022-11-03 20:04:45 +00:00
parent 0e49d46eab
commit 5cba0ed266
5 changed files with 383 additions and 24 deletions

View File

@ -194,6 +194,10 @@
<Compile Include="Source\Collections\ReverseComparer.Test.cs"> <Compile Include="Source\Collections\ReverseComparer.Test.cs">
<DependentUpon>ReverseComparer.cs</DependentUpon> <DependentUpon>ReverseComparer.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="Source\Collections\SortableBindingList.cs" />
<Compile Include="Source\Collections\SortableBindingList.Test.cs">
<DependentUpon>SortableBindingList.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\TransformingReadOnlyCollection.cs" /> <Compile Include="Source\Collections\TransformingReadOnlyCollection.cs" />
<Compile Include="Source\Collections\TransformingReadOnlyCollection.Interfaces.cs"> <Compile Include="Source\Collections\TransformingReadOnlyCollection.Interfaces.cs">
<DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon> <DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon>

View File

@ -32,6 +32,20 @@ namespace Nuclex.Support.Collections {
[TestFixture] [TestFixture]
internal class IListExtensionsTest { internal class IListExtensionsTest {
/// <summary>Tests whether the insertion sort algorithm sorts a list correctly</summary>
[Test]
public void InsertionSortCanSortWholeList() {
var testList = new List<int>(capacity: 5) { 1, 5, 2, 4, 3 };
var testListAsIList = (IList<int>)testList;
testListAsIList.InsertionSort();
CollectionAssert.AreEqual(
new List<int>(capacity: 5) { 1, 2, 3, 4, 5 },
testList
);
}
/// <summary>Tests whether the insertion sort algorithm works on big lists</summary> /// <summary>Tests whether the insertion sort algorithm works on big lists</summary>
[Test] [Test]
public void InsertionSortCanSortBigList() { public void InsertionSortCanSortBigList() {
@ -53,21 +67,7 @@ namespace Nuclex.Support.Collections {
} }
} }
/// <summary>Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected</summary> /// <summary>Tests whether the insertion sort algorithm respects custom boundaries</summary>
[Test]
public void InsertionSortCanSortWholeList() {
var testList = new List<int>(capacity: 5) { 1, 5, 2, 4, 3 };
var testListAsIList = (IList<int>)testList;
testListAsIList.InsertionSort();
CollectionAssert.AreEqual(
new List<int>(capacity: 5) { 1, 2, 3, 4, 5 },
testList
);
}
/// <summary>Tests whether the 'Text' property works as expected</summary>
[Test] [Test]
public void InsertionSortCanSortListSegment() { public void InsertionSortCanSortListSegment() {
var testList = new List<int>(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; var testList = new List<int>(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 };
@ -81,6 +81,20 @@ namespace Nuclex.Support.Collections {
); );
} }
/// <summary>Tests whether the quicksort algorithm sorts a list correctly</summary>
[Test]
public void QuickSortCanSortWholeList() {
var testList = new List<int>(capacity: 5) { 1, 5, 2, 4, 3 };
var testListAsIList = (IList<int>)testList;
testListAsIList.QuickSort();
CollectionAssert.AreEqual(
new List<int>(capacity: 5) { 1, 2, 3, 4, 5 },
testList
);
}
/// <summary>Tests whether the quicksort algorithm works on big lists</summary> /// <summary>Tests whether the quicksort algorithm works on big lists</summary>
[Test] [Test]
public void QuickSortCanSortBigList() { public void QuickSortCanSortBigList() {
@ -102,21 +116,20 @@ namespace Nuclex.Support.Collections {
} }
} }
/// <summary>Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected</summary> /// <summary>Tests whether the quicksort algorithm respects custom boundaries</summary>
[Test] [Test]
public void QuickSortCanSortWholeList() { public void QuickSortCanSortListSegment() {
var testList = new List<int>(capacity: 5) { 1, 5, 2, 4, 3 }; var testList = new List<int>(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 };
var testListAsIList = (IList<int>)testList; var testListAsIList = (IList<int>)testList;
testListAsIList.QuickSort(); testListAsIList.QuickSort(1, 5, Comparer<int>.Default);
CollectionAssert.AreEqual( CollectionAssert.AreEqual(
new List<int>(capacity: 5) { 1, 2, 3, 4, 5 }, new List<int>(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 },
testList testList
); );
} }
} }
} // namespace Nuclex.Support.Collections } // namespace Nuclex.Support.Collections

View File

@ -19,7 +19,6 @@ License along with this library
#endregion #endregion
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
namespace Nuclex.Support.Collections { namespace Nuclex.Support.Collections {

View File

@ -0,0 +1,120 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library 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
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
#if UNITTEST
using NUnit.Framework;
namespace Nuclex.Support.Collections {
/// <summary>Unit Test for the SortableBindingList class</summary>
[TestFixture]
internal class SortableBindingListTest {
#region class TestRecord
/// <summary>Dummy record used to test the sortable binding list</summary>
private class TestRecord {
/// <summary>A property of type integer</summary>
public int IntegerValue { get; set; }
/// <summary>A property of type string</summary>
public string StringValue { get; set; }
/// <summary>A property of type float</summary>
public float FloatValue { get; set; }
}
#endregion // class TestRecord
/// <summary>Verifies that the sortable binding list is default constructible</summary>
[Test]
public void HasDefaultConstructor() {
Assert.DoesNotThrow(
delegate () { new SortableBindingList<TestRecord>(); }
);
}
/// <summary>
/// Tests whether the sortable binding list can copy an existing list
/// when being constructed
/// </summary>
[Test]
public void HasEnumerableConstructor() {
var items = new List<TestRecord>() {
new TestRecord() { IntegerValue = 123 },
new TestRecord() { IntegerValue = 456 }
};
var testList = new SortableBindingList<TestRecord>(items);
Assert.AreEqual(2, testList.Count);
Assert.AreSame(items[0], testList[0]);
Assert.AreSame(items[1], testList[1]);
}
/// <summary>Verifies that the sortable binding list supports sorting</summary>
[Test]
public void SupportsSorting() {
var testList = new SortableBindingList<TestRecord>();
IBindingList testListAsBindingList = testList;
Assert.IsTrue(testListAsBindingList.SupportsSorting);
}
/// <summary>
/// Tests whether the sortable binding list can sort its elements by different properties
/// </summary>
[Test]
public void CanSortItems() {
var items = new List<TestRecord>() {
new TestRecord() { IntegerValue = 456 },
new TestRecord() { IntegerValue = 789 },
new TestRecord() { IntegerValue = 123 }
};
var testList = new SortableBindingList<TestRecord>(items);
IBindingList testListAsBindingList = testList;
PropertyDescriptor integerValuePropertyDescriptor = (
TypeDescriptor.GetProperties(typeof(TestRecord))[nameof(TestRecord.IntegerValue)]
);
testListAsBindingList.ApplySort(
integerValuePropertyDescriptor, ListSortDirection.Ascending
);
Assert.AreEqual(3, testList.Count);
Assert.AreEqual(123, testList[0].IntegerValue);
Assert.AreEqual(456, testList[1].IntegerValue);
Assert.AreEqual(789, testList[2].IntegerValue);
}
}
} // namespace Nuclex.Support.Collections
#endif // UNITTEST

View File

@ -0,0 +1,223 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library 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
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
namespace Nuclex.Support.Collections {
/// <summary>Variant of BindingList that supports sorting</summary>
/// <typeparam name="TElement">Type of items the binding list will contain</typeparam>
internal class SortableBindingList<TElement> : BindingList<TElement> {
#region class PropertyComparer
/// <summary>Compares two elements based on a single preselected property</summary>
private class PropertyComparer : IComparer<TElement> {
/// <summary>Initializes a new property comparer for the specified property</summary>
/// <param name="property">Property based on which elements should be compared</param>
/// <param name="direction">Direction in which elements should be sorted</param>
public PropertyComparer(PropertyDescriptor property, ListSortDirection direction) {
this.propertyDescriptor = property;
Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType);
this.comparer = (IComparer)comparerForPropertyType.InvokeMember(
"Default",
BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public,
null, // binder
null, // target (none since method is static)
null // argument array
);
SetListSortDirection(direction);
}
/// <summary>Compares two elements based on the comparer's chosen property</summary>
/// <param name="first">First element for the comparison</param>
/// <param name="second">Second element for the comparison</param>
/// <returns>The relationship of the two elements to each other</returns>
public int Compare(TElement first, TElement second) {
return this.comparer.Compare(
this.propertyDescriptor.GetValue(first),
this.propertyDescriptor.GetValue(second)
) * this.reverse;
}
/// <summary>Selects the property based on which elements should be compared</summary>
/// <param name="descriptor">Descriptor for the property to use for comparison</param>
private void SetPropertyDescriptor(PropertyDescriptor descriptor) {
this.propertyDescriptor = descriptor;
}
/// <summary>Changes the sort direction</summary>
/// <param name="direction">New sort direction</param>
private void SetListSortDirection(ListSortDirection direction) {
this.reverse = direction == ListSortDirection.Ascending ? 1 : -1;
}
/// <summary>Updtes the sorted proeprty and the sort direction</summary>
/// <param name="descriptor">Property based on which elements will be sorted</param>
/// <param name="direction">Direction in which elements will be sorted</param>
public void SetPropertyAndDirection(
PropertyDescriptor descriptor, ListSortDirection direction
) {
SetPropertyDescriptor(descriptor);
SetListSortDirection(direction);
}
/// <summary>The default comparer for the type of the chosen property</summary>
private readonly IComparer comparer;
/// <summary>Descriptor for the chosen property</summary>
private PropertyDescriptor propertyDescriptor;
/// <summary>
/// Either positive or negative 1 to change the sign of the comparison result
/// </summary>
private int reverse;
}
#endregion // class PropertyComparer
/// <summary>Initializes a new BindingList with support for sorting</summary>
public SortableBindingList() : base(new List<TElement>()) {
this.comparers = new Dictionary<Type, PropertyComparer>();
}
/// <summary>
/// Initializes a sortable BindingList, copying the contents of an existing list
/// </summary>
/// <param name="enumeration">Existing list whose contents will be shallo-wcopied</param>
public SortableBindingList(IEnumerable<TElement> enumeration) :
base(new List<TElement>(enumeration)) {
this.comparers = new Dictionary<Type, PropertyComparer>();
}
/// <summary>
/// Used by BindingList implementation to check whether sorting is supported
/// </summary>
protected override bool SupportsSortingCore {
get { return true; }
}
/// <summary>
/// Used by BindingList implementation to check whether the list is currently sorted
/// </summary>
protected override bool IsSortedCore {
get { return this.isSorted; }
}
/// <summary>
/// Used by BindingList implementation to track the property the list is sorted by
/// </summary>
protected override PropertyDescriptor SortPropertyCore {
get { return this.propertyDescriptor; }
}
/// <summary>
/// Used by BindingList implementation to track the direction in which the list is sortd
/// </summary>
protected override ListSortDirection SortDirectionCore {
get { return this.listSortDirection; }
}
/// <summary>
/// Used by BindingList implementation to check whether the list supports searching
/// </summary>
protected override bool SupportsSearchingCore {
get { return true; }
}
/// <summary>
/// Used by BindingList implementation to sort the elements in the backing collection
/// </summary>
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) {
// Obtain a property comparer that sorts on the attributes the SortableBindingList
// has been configured for its sort order
PropertyComparer comparer;
{
Type propertyType = property.PropertyType;
if(!this.comparers.TryGetValue(propertyType, out comparer)) {
comparer = new PropertyComparer(property, direction);
this.comparers.Add(propertyType, comparer);
}
// Direction may need to be updated
comparer.SetPropertyAndDirection(property, direction);
}
// Check to see if our base class is using a standard List<> in which case
// we'll sneakily use the downcast to call the List<>.Sort() method, otherwise
// there's still our own quicksort implementation for IList<>.
List<TElement> itemsAsList = this.Items as List<TElement>;
if(itemsAsList != null) {
itemsAsList.Sort(comparer);
} else {
this.Items.QuickSort(0, this.Items.Count, comparer); // from IListExtensions
}
this.propertyDescriptor = property;
this.listSortDirection = direction;
this.isSorted = true;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
/// <summary>Used by BindingList implementation to undo any sorting that took place</summary>
protected override void RemoveSortCore() {
this.isSorted = false;
this.propertyDescriptor = base.SortPropertyCore;
this.listSortDirection = base.SortDirectionCore;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
/// <summary>
/// Used by BindingList implementation to run a search on any of the element's properties
/// </summary>
protected override int FindCore(PropertyDescriptor property, object key) {
int count = this.Count;
for(int index = 0; index < count; ++index) {
TElement element = this[index];
if(property.GetValue(element).Equals(key)) {
return index;
}
}
return -1;
}
/// <summary>Cached property comparers, created for each element property as needed</summary>
private readonly Dictionary<Type, PropertyComparer> comparers;
/// <summary>Whether the binding list is currently sorted</summary>
private bool isSorted;
/// <summary>Direction in which the binding list is currently sorted</summary>
private ListSortDirection listSortDirection;
/// <summary>Descriptor for the property by which the binding list is currently sorted</summary>
private PropertyDescriptor propertyDescriptor;
}
} // namespace Nuclex.Support.Collections