diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index 65713bc..85e2d5b 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -194,6 +194,10 @@ ReverseComparer.cs + + + SortableBindingList.cs + TransformingReadOnlyCollection.cs diff --git a/Source/Collections/IListExtensions.Test.cs b/Source/Collections/IListExtensions.Test.cs index a6c70e6..7bd8240 100644 --- a/Source/Collections/IListExtensions.Test.cs +++ b/Source/Collections/IListExtensions.Test.cs @@ -32,6 +32,20 @@ namespace Nuclex.Support.Collections { [TestFixture] internal class IListExtensionsTest { + /// Tests whether the insertion sort algorithm sorts a list correctly + [Test] + public void InsertionSortCanSortWholeList() { + var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; + var testListAsIList = (IList)testList; + + testListAsIList.InsertionSort(); + + CollectionAssert.AreEqual( + new List(capacity: 5) { 1, 2, 3, 4, 5 }, + testList + ); + } + /// Tests whether the insertion sort algorithm works on big lists [Test] public void InsertionSortCanSortBigList() { @@ -53,26 +67,12 @@ namespace Nuclex.Support.Collections { } } - /// Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected - [Test] - public void InsertionSortCanSortWholeList() { - var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; - var testListAsIList = (IList)testList; - - testListAsIList.InsertionSort(); - - CollectionAssert.AreEqual( - new List(capacity: 5) { 1, 2, 3, 4, 5 }, - testList - ); - } - - /// Tests whether the 'Text' property works as expected + /// Tests whether the insertion sort algorithm respects custom boundaries [Test] public void InsertionSortCanSortListSegment() { var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; var testListAsIList = (IList)testList; - + testListAsIList.InsertionSort(1, 5, Comparer.Default); CollectionAssert.AreEqual( @@ -81,6 +81,20 @@ namespace Nuclex.Support.Collections { ); } + /// Tests whether the quicksort algorithm sorts a list correctly + [Test] + public void QuickSortCanSortWholeList() { + var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; + var testListAsIList = (IList)testList; + + testListAsIList.QuickSort(); + + CollectionAssert.AreEqual( + new List(capacity: 5) { 1, 2, 3, 4, 5 }, + testList + ); + } + /// Tests whether the quicksort algorithm works on big lists [Test] public void QuickSortCanSortBigList() { @@ -102,21 +116,20 @@ namespace Nuclex.Support.Collections { } } - /// Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected + /// Tests whether the quicksort algorithm respects custom boundaries [Test] - public void QuickSortCanSortWholeList() { - var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; + public void QuickSortCanSortListSegment() { + var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; var testListAsIList = (IList)testList; - - testListAsIList.QuickSort(); + + testListAsIList.QuickSort(1, 5, Comparer.Default); CollectionAssert.AreEqual( - new List(capacity: 5) { 1, 2, 3, 4, 5 }, + new List(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 }, testList ); } - } } // namespace Nuclex.Support.Collections diff --git a/Source/Collections/IListExtensions.cs b/Source/Collections/IListExtensions.cs index 8f79aa3..5cbe824 100644 --- a/Source/Collections/IListExtensions.cs +++ b/Source/Collections/IListExtensions.cs @@ -19,7 +19,6 @@ License along with this library #endregion using System; -using System.Collections; using System.Collections.Generic; namespace Nuclex.Support.Collections { diff --git a/Source/Collections/SortableBindingList.Test.cs b/Source/Collections/SortableBindingList.Test.cs new file mode 100644 index 0000000..0ebab8c --- /dev/null +++ b/Source/Collections/SortableBindingList.Test.cs @@ -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 { + + /// Unit Test for the SortableBindingList class + [TestFixture] + internal class SortableBindingListTest { + + #region class TestRecord + + /// Dummy record used to test the sortable binding list + private class TestRecord { + + /// A property of type integer + public int IntegerValue { get; set; } + + /// A property of type string + public string StringValue { get; set; } + + /// A property of type float + public float FloatValue { get; set; } + + } + + #endregion // class TestRecord + + /// Verifies that the sortable binding list is default constructible + [Test] + public void HasDefaultConstructor() { + Assert.DoesNotThrow( + delegate () { new SortableBindingList(); } + ); + } + + /// + /// Tests whether the sortable binding list can copy an existing list + /// when being constructed + /// + [Test] + public void HasEnumerableConstructor() { + var items = new List() { + new TestRecord() { IntegerValue = 123 }, + new TestRecord() { IntegerValue = 456 } + }; + + var testList = new SortableBindingList(items); + + Assert.AreEqual(2, testList.Count); + Assert.AreSame(items[0], testList[0]); + Assert.AreSame(items[1], testList[1]); + } + + /// Verifies that the sortable binding list supports sorting + [Test] + public void SupportsSorting() { + var testList = new SortableBindingList(); + IBindingList testListAsBindingList = testList; + + Assert.IsTrue(testListAsBindingList.SupportsSorting); + } + + /// + /// Tests whether the sortable binding list can sort its elements by different properties + /// + [Test] + public void CanSortItems() { + var items = new List() { + new TestRecord() { IntegerValue = 456 }, + new TestRecord() { IntegerValue = 789 }, + new TestRecord() { IntegerValue = 123 } + }; + + var testList = new SortableBindingList(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 diff --git a/Source/Collections/SortableBindingList.cs b/Source/Collections/SortableBindingList.cs new file mode 100644 index 0000000..1ad125e --- /dev/null +++ b/Source/Collections/SortableBindingList.cs @@ -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 { + + /// Variant of BindingList that supports sorting + /// Type of items the binding list will contain + internal class SortableBindingList : BindingList { + + #region class PropertyComparer + + /// Compares two elements based on a single preselected property + private class PropertyComparer : IComparer { + + /// Initializes a new property comparer for the specified property + /// Property based on which elements should be compared + /// Direction in which elements should be sorted + 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); + } + + /// Compares two elements based on the comparer's chosen property + /// First element for the comparison + /// Second element for the comparison + /// The relationship of the two elements to each other + public int Compare(TElement first, TElement second) { + return this.comparer.Compare( + this.propertyDescriptor.GetValue(first), + this.propertyDescriptor.GetValue(second) + ) * this.reverse; + } + + /// Selects the property based on which elements should be compared + /// Descriptor for the property to use for comparison + private void SetPropertyDescriptor(PropertyDescriptor descriptor) { + this.propertyDescriptor = descriptor; + } + + /// Changes the sort direction + /// New sort direction + private void SetListSortDirection(ListSortDirection direction) { + this.reverse = direction == ListSortDirection.Ascending ? 1 : -1; + } + + /// Updtes the sorted proeprty and the sort direction + /// Property based on which elements will be sorted + /// Direction in which elements will be sorted + public void SetPropertyAndDirection( + PropertyDescriptor descriptor, ListSortDirection direction + ) { + SetPropertyDescriptor(descriptor); + SetListSortDirection(direction); + } + + /// The default comparer for the type of the chosen property + private readonly IComparer comparer; + /// Descriptor for the chosen property + private PropertyDescriptor propertyDescriptor; + /// + /// Either positive or negative 1 to change the sign of the comparison result + /// + private int reverse; + + } + + #endregion // class PropertyComparer + + /// Initializes a new BindingList with support for sorting + public SortableBindingList() : base(new List()) { + this.comparers = new Dictionary(); + } + + /// + /// Initializes a sortable BindingList, copying the contents of an existing list + /// + /// Existing list whose contents will be shallo-wcopied + public SortableBindingList(IEnumerable enumeration) : + base(new List(enumeration)) { + this.comparers = new Dictionary(); + } + + /// + /// Used by BindingList implementation to check whether sorting is supported + /// + protected override bool SupportsSortingCore { + get { return true; } + } + + /// + /// Used by BindingList implementation to check whether the list is currently sorted + /// + protected override bool IsSortedCore { + get { return this.isSorted; } + } + + /// + /// Used by BindingList implementation to track the property the list is sorted by + /// + protected override PropertyDescriptor SortPropertyCore { + get { return this.propertyDescriptor; } + } + + /// + /// Used by BindingList implementation to track the direction in which the list is sortd + /// + protected override ListSortDirection SortDirectionCore { + get { return this.listSortDirection; } + } + + /// + /// Used by BindingList implementation to check whether the list supports searching + /// + protected override bool SupportsSearchingCore { + get { return true; } + } + + /// + /// Used by BindingList implementation to sort the elements in the backing collection + /// + 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 itemsAsList = this.Items as List; + 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)); + } + + /// Used by BindingList implementation to undo any sorting that took place + protected override void RemoveSortCore() { + this.isSorted = false; + this.propertyDescriptor = base.SortPropertyCore; + this.listSortDirection = base.SortDirectionCore; + + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + } + + /// + /// Used by BindingList implementation to run a search on any of the element's properties + /// + 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; + } + + /// Cached property comparers, created for each element property as needed + private readonly Dictionary comparers; + /// Whether the binding list is currently sorted + private bool isSorted; + /// Direction in which the binding list is currently sorted + private ListSortDirection listSortDirection; + /// Descriptor for the property by which the binding list is currently sorted + private PropertyDescriptor propertyDescriptor; + + } + +} // namespace Nuclex.Support.Collections