diff --git a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs index 127a3b00..a973b63c 100644 --- a/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs +++ b/Source/LibationAvalonia/ViewModels/ProductsDisplayViewModel.cs @@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels /// Backing list of all grid entries private readonly AvaloniaList SOURCE = new(); /// Grid entries included in the filter set. If null, all grid entries are shown - private List FilteredInGridEntries; + private HashSet FilteredInGridEntries; public string FilterString { get; private set; } public DataGridCollectionView GridEntries { get; private set; } @@ -117,7 +117,7 @@ namespace LibationAvalonia.ViewModels } //Create the filtered-in list before adding entries to avoid a refresh - FilteredInGridEntries = QueryResults(geList.Union(geList.OfType().SelectMany(s => s.Children)), FilterString); + FilteredInGridEntries = geList.Union(geList.OfType().SelectMany(s => s.Children)).FilterEntries(FilterString); SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded)); //Add all children beneath their parent @@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels if (SOURCE.Count == 0) return; - FilteredInGridEntries = QueryResults(SOURCE, searchString); + FilteredInGridEntries = SOURCE.FilterEntries(searchString); await refreshGrid(); } @@ -318,23 +318,9 @@ namespace LibationAvalonia.ViewModels return FilteredInGridEntries.Contains(item); } - private static List QueryResults(IEnumerable entries, string searchString) - { - if (string.IsNullOrEmpty(searchString)) return null; - - var searchResultSet = SearchEngineCommands.Search(searchString); - - var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe); - - //Find all series containing children that match the search criteria - var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any()); - - return booksFilteredIn.Concat(seriesFilteredIn).ToList(); - } - private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e) { - var filterResults = QueryResults(SOURCE, FilterString); + var filterResults = SOURCE.FilterEntries(FilterString); if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count) { diff --git a/Source/LibationAvalonia/ViewModels/RowComparer.cs b/Source/LibationAvalonia/ViewModels/RowComparer.cs index 743a610f..ef3170d8 100644 --- a/Source/LibationAvalonia/ViewModels/RowComparer.cs +++ b/Source/LibationAvalonia/ViewModels/RowComparer.cs @@ -1,25 +1,17 @@ using Avalonia.Controls; using LibationUiBase.GridView; -using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.Reflection; namespace LibationAvalonia.ViewModels { - /// - /// This compare class ensures that all top-level grid entries (standalone books or series parents) - /// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain - /// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex - /// properties when 2 items compare equal. - /// - internal class RowComparer : IComparer, IComparer, IComparer + internal class RowComparer : RowComparerBase { private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance); - public DataGridColumn Column { get; init; } - public string PropertyName { get; private set; } + private DataGridColumn Column { get; init; } + public override string PropertyName { get; set; } public RowComparer(DataGridColumn column) { @@ -27,72 +19,8 @@ namespace LibationAvalonia.ViewModels PropertyName = Column.SortMemberPath; } - public int Compare(object x, object y) - { - if (x is null && y is not null) return -1; - if (x is not null && y is null) return 1; - if (x is null && y is null) return 0; - - var geA = (IGridEntry)x; - var geB = (IGridEntry)y; - - var sortDirection = GetSortOrder(); - - ISeriesEntry parentA = null; - ISeriesEntry parentB = null; - - if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA) - parentA = seA; - if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB) - parentB = seB; - - //both a and b are top-level grid entries - if (parentA is null && parentB is null) - return InternalCompare(geA, geB); - - //a is top-level, b is a child - if (parentA is null && parentB is not null) - { - // b is a child of a, parent is always first - if (parentB == geA) - return sortDirection is ListSortDirection.Ascending ? -1 : 1; - else - return InternalCompare(geA, parentB); - } - - //a is a child, b is a top-level - if (parentA is not null && parentB is null) - { - // a is a child of b, parent is always first - if (parentA == geB) - return sortDirection is ListSortDirection.Ascending ? 1 : -1; - else - return InternalCompare(parentA, geB); - } - - //both are children of the same series - if (parentA == parentB) - return InternalCompare(geA, geB); - - //a and b are children of different series. - return InternalCompare(parentA, parentB); - } - //Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection - private ListSortDirection? GetSortOrder() + protected override ListSortDirection? GetSortOrder() => CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?; - - private int InternalCompare(IGridEntry x, IGridEntry y) - { - var val1 = x.GetMemberValue(PropertyName); - var val2 = y.GetMemberValue(PropertyName); - - return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ; - } - - public int Compare(IGridEntry x, IGridEntry y) - { - return Compare((object)x, y); - } } } diff --git a/Source/LibationUiBase/GridView/QueryExtensions.cs b/Source/LibationUiBase/GridView/QueryExtensions.cs index 9f899212..827d791b 100644 --- a/Source/LibationUiBase/GridView/QueryExtensions.cs +++ b/Source/LibationUiBase/GridView/QueryExtensions.cs @@ -1,4 +1,5 @@ -using DataLayer; +using ApplicationServices; +using DataLayer; using System; using System.Collections.Generic; using System.Linq; @@ -39,6 +40,20 @@ namespace LibationUiBase.GridView return null; } } + + public static HashSet? FilterEntries(this IEnumerable entries, string searchString) + { + if (string.IsNullOrEmpty(searchString)) return null; + + var searchResultSet = SearchEngineCommands.Search(searchString); + + var booksFilteredIn = entries.BookEntries().Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe); + + //Find all series containing children that match the search criteria + var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(searchResultSet.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any()); + + return booksFilteredIn.Concat(seriesFilteredIn).ToHashSet(); + } } #nullable disable } diff --git a/Source/LibationUiBase/GridView/RowComparerBase.cs b/Source/LibationUiBase/GridView/RowComparerBase.cs new file mode 100644 index 00000000..6dbad15c --- /dev/null +++ b/Source/LibationUiBase/GridView/RowComparerBase.cs @@ -0,0 +1,82 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace LibationUiBase.GridView +{ + /// + /// This compare class ensures that all top-level grid entries (standalone books or series parents) + /// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain + /// sorted by series index, ascending. + /// + public abstract class RowComparerBase : IComparer, IComparer, IComparer + { + public abstract string PropertyName { get; set; } + + public int Compare(object x, object y) + { + if (x is null && y is not null) return -1; + if (x is not null && y is null) return 1; + if (x is null && y is null) return 0; + + var geA = (IGridEntry)x; + var geB = (IGridEntry)y; + + var sortDirection = GetSortOrder(); + + ISeriesEntry parentA = null; + ISeriesEntry parentB = null; + + if (geA is ILibraryBookEntry lbA && lbA.Parent is ISeriesEntry seA) + parentA = seA; + if (geB is ILibraryBookEntry lbB && lbB.Parent is ISeriesEntry seB) + parentB = seB; + + //both a and b are top-level grid entries + if (parentA is null && parentB is null) + return InternalCompare(geA, geB); + + //a is top-level, b is a child + if (parentA is null && parentB is not null) + { + // b is a child of a, parent is always first + if (parentB == geA) + return sortDirection is ListSortDirection.Ascending ? -1 : 1; + else + return InternalCompare(geA, parentB); + } + + //a is a child, b is a top-level + if (parentA is not null && parentB is null) + { + // a is a child of b, parent is always first + if (parentA == geB) + return sortDirection is ListSortDirection.Ascending ? 1 : -1; + else + return InternalCompare(parentA, geB); + } + + //both are children of the same series + if (parentA == parentB) + return InternalCompare(geA, geB); + + //a and b are children of different series. + return InternalCompare(parentA, parentB); + } + + protected abstract ListSortDirection? GetSortOrder(); + + private int InternalCompare(IGridEntry x, IGridEntry y) + { + var val1 = x.GetMemberValue(PropertyName); + var val2 = y.GetMemberValue(PropertyName); + + return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); ; + } + + public int Compare(IGridEntry x, IGridEntry y) + { + return Compare((object)x, y); + } + } +}