Merge pull request #579 from Mbucari/master

Bug fixes and more shared code moved to UI base
This commit is contained in:
rmcrackan 2023-04-10 22:47:24 -04:00 committed by GitHub
commit 1a95f2923b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 247 additions and 219 deletions

View File

@ -23,7 +23,7 @@ namespace LibationAvalonia.ViewModels
/// <summary>Backing list of all grid entries</summary> /// <summary>Backing list of all grid entries</summary>
private readonly AvaloniaList<IGridEntry> SOURCE = new(); private readonly AvaloniaList<IGridEntry> SOURCE = new();
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary> /// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private List<IGridEntry> FilteredInGridEntries; private HashSet<IGridEntry> FilteredInGridEntries;
public string FilterString { get; private set; } public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; private set; } public DataGridCollectionView GridEntries { get; private set; }
@ -117,8 +117,8 @@ namespace LibationAvalonia.ViewModels
} }
//Create the filtered-in list before adding entries to avoid a refresh //Create the filtered-in list before adding entries to avoid a refresh
FilteredInGridEntries = QueryResults(geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)), FilterString); FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
SOURCE.AddRange(geList.OrderByDescending(e => e.DateAdded)); SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
//Add all children beneath their parent //Add all children beneath their parent
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList()) foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
@ -301,7 +301,7 @@ namespace LibationAvalonia.ViewModels
if (SOURCE.Count == 0) if (SOURCE.Count == 0)
return; return;
FilteredInGridEntries = QueryResults(SOURCE, searchString); FilteredInGridEntries = SOURCE.FilterEntries(searchString);
await refreshGrid(); await refreshGrid();
} }
@ -318,25 +318,11 @@ namespace LibationAvalonia.ViewModels
return FilteredInGridEntries.Contains(item); return FilteredInGridEntries.Contains(item);
} }
private static List<IGridEntry> QueryResults(IEnumerable<IGridEntry> 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) 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) if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
{ {
FilteredInGridEntries = filterResults; FilteredInGridEntries = filterResults;
await refreshGrid(); await refreshGrid();

View File

@ -1,98 +1,28 @@
using Avalonia.Controls; using Avalonia.Controls;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Reflection; using System.Reflection;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
/// <summary> internal class RowComparer : RowComparerBase
/// 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.
/// </summary>
internal class RowComparer : IComparer, IComparer<IGridEntry>, IComparer<object>
{ {
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance); 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); private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; } private DataGridColumn Column { get; init; }
public string PropertyName { get; private set; } public override string PropertyName { get; set; }
public RowComparer(DataGridColumn column) public RowComparer(DataGridColumn column)
{ {
Column = column; Column = column;
PropertyName = Column.SortMemberPath; PropertyName = Column?.SortMemberPath ?? nameof(IGridEntry.DateAdded);
}
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 //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?; => Column is null ? ListSortDirection.Descending
: CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) is ListSortDirection lsd ? lsd
private int InternalCompare(IGridEntry x, IGridEntry y) : ListSortDirection.Descending;
{
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);
}
} }
} }

View File

@ -58,7 +58,7 @@ namespace LibationUiBase.GridView
public abstract object BackgroundBrush { get; } public abstract object BackgroundBrush { get; }
public object ButtonImage => GetLiberateIcon(); public object ButtonImage => GetLiberateIcon();
public string ToolTip => GetTooltip(); public string ToolTip => GetTooltip();
protected internal Book Book { get; internal set; } private Book Book { get; }
private DateTime lastBookUpdate; private DateTime lastBookUpdate;
private LiberatedStatus bookStatus; private LiberatedStatus bookStatus;

View File

@ -154,9 +154,13 @@ namespace LibationUiBase.GridView
if (udi.Book.AudibleProductId != Book.AudibleProductId) if (udi.Book.AudibleProductId != Book.AudibleProductId)
return; return;
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus), if (udi.Book != LibraryBook.Book)
//EntryStatu's Book instance will not have the current DB state. {
Liberate.Book = udi.Book; //If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
//Liberate.Book and LibraryBook.Book instances will not have the current DB state.
Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account)));
return;
}
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view. // UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs // - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs

View File

@ -1,4 +1,5 @@
using DataLayer; using ApplicationServices;
using DataLayer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -39,6 +40,26 @@ namespace LibationUiBase.GridView
return null; return null;
} }
} }
public static bool SearchSetsDiffer(this HashSet<IGridEntry>? searchSet, HashSet<IGridEntry>? otherSet)
=> searchSet is null != otherSet is null ||
(searchSet is not null &&
otherSet is not null &&
searchSet.Intersect(otherSet).Count() != searchSet.Count);
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> 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 #nullable disable
} }

View File

@ -0,0 +1,91 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
namespace LibationUiBase.GridView
{
/// <summary>
/// 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.
/// </summary>
public abstract class RowComparerBase : IComparer, IComparer<IGridEntry>, IComparer<object>
{
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)
{
//Podcast episodes usually all have the same PurchaseDate and DateAdded property:
//the date that the series was added to the library. So when sorting by PurchaseDate
//and DateAdded, compare SeriesOrder instead..
return PropertyName switch
{
nameof(IGridEntry.DateAdded) or nameof (IGridEntry.PurchaseDate) => geA.SeriesOrder.CompareTo(geB.SeriesOrder),
_ => 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);
}
}
}

View File

@ -47,7 +47,7 @@ namespace LibationUiBase.GridView
Children = children Children = children
.Select(c => new LibraryBookEntry<TStatus>(c, this)) .Select(c => new LibraryBookEntry<TStatus>(c, this))
.OrderBy(c => c.SeriesIndex) .OrderByDescending(c => c.SeriesOrder)
.ToList<ILibraryBookEntry>(); .ToList<ILibraryBookEntry>();
UpdateLibraryBook(parent); UpdateLibraryBook(parent);

View File

@ -1,6 +1,4 @@
using ApplicationServices; using ApplicationServices;
using Dinah.Core.DataBinding;
using LibationSearchEngine;
using LibationUiBase.GridView; using LibationUiBase.GridView;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -20,51 +18,62 @@ namespace LibationWinForms.GridView
* *
* Remove is overridden to ensure that removed items are removed from * Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list. * the base list (visible items) as well as the FilterRemoved list.
*
* Using BindingList.Add/Insert and BindingList.Remove will cause the
* BindingList to subscribe/unsibscribe to/from the item's PropertyChanged
* event. Adding or removing from the underlying list will not change the
* BindingList's subscription to that item.
*/ */
internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView internal class GridEntryBindingList : BindingList<IGridEntry>, IBindingListView
{ {
public GridEntryBindingList() : base(new List<IGridEntry>()) { }
public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration)) public GridEntryBindingList(IEnumerable<IGridEntry> enumeration) : base(new List<IGridEntry>(enumeration))
{ {
SearchEngineCommands.SearchEngineUpdated += (_,_) => ApplyFilter(FilterString); SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
ListChanged += GridEntryBindingList_ListChanged;
refreshEntries();
} }
/// <returns>All items in the list, including those filtered out.</returns> /// <returns>All items in the list, including those filtered out.</returns>
public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList(); public List<IGridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
/// <summary>All items that pass the current filter</summary> /// <summary>All items that pass the current filter</summary>
public IEnumerable<ILibraryBookEntry> GetFilteredInItems() public IEnumerable<ILibraryBookEntry> GetFilteredInItems()
=> SearchResults is null => FilteredInGridEntries?
? FilterRemoved
.OfType<ILibraryBookEntry>() .OfType<ILibraryBookEntry>()
.Union(Items.OfType<ILibraryBookEntry>()) ?? FilterRemoved
: FilterRemoved
.OfType<ILibraryBookEntry>() .OfType<ILibraryBookEntry>()
.Join(SearchResults.Docs, o => o.Book.AudibleProductId, i => i.ProductId, (o, _) => o)
.Union(Items.OfType<ILibraryBookEntry>()); .Union(Items.OfType<ILibraryBookEntry>());
public bool SupportsFiltering => true; public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); } public string Filter
{
get => FilterString;
set
{
FilterString = value;
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary> if (Items.Count + FilterRemoved.Count == 0)
public bool SuspendFilteringOnUpdate { get; set; } return;
protected MemberComparer<IGridEntry> Comparer { get; } = new(); FilteredInGridEntries = AllItems().FilterEntries(FilterString);
refreshEntries();
}
}
protected RowComparer Comparer { get; } = new();
protected override bool SupportsSortingCore => true; protected override bool SupportsSortingCore => true;
protected override bool SupportsSearchingCore => true; protected override bool SupportsSearchingCore => true;
protected override bool IsSortedCore => isSorted; protected override bool IsSortedCore => isSorted;
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor; protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
protected override ListSortDirection SortDirectionCore => listSortDirection; protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
/// <summary> Items that were removed from the base list due to filtering </summary> /// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<IGridEntry> FilterRemoved = new(); private readonly List<IGridEntry> FilterRemoved = new();
private string FilterString; private string FilterString;
private SearchResultSet SearchResults;
private bool isSorted; private bool isSorted;
private ListSortDirection listSortDirection;
private PropertyDescriptor propertyDescriptor; private PropertyDescriptor propertyDescriptor;
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
private HashSet<IGridEntry> FilteredInGridEntries;
#region Unused - Advanced Filtering #region Unused - Advanced Filtering
public bool SupportsAdvancedSorting => false; public bool SupportsAdvancedSorting => false;
@ -82,25 +91,57 @@ namespace LibationWinForms.GridView
base.Remove(entry); base.Remove(entry);
} }
private void ApplyFilter(string filterString) /// <summary>
/// This method should be called whenever there's been a change to the
/// set of all GridEntries that affects sort order or filter status
/// </summary>
private void refreshEntries()
{ {
if (filterString != FilterString) if (FilteredInGridEntries is null)
RemoveFilter();
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (IGridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
foreach (var item in filteredOut)
{ {
FilterRemoved.Add(item); addRemovedItemsBack(FilterRemoved.ToList());
base.Remove(item); }
else
{
var addBackEntries = FilterRemoved.Intersect(FilteredInGridEntries).ToList();
var toRemoveEntries = Items.Except(FilteredInGridEntries).ToList();
addRemovedItemsBack(addBackEntries);
foreach (var newRemove in toRemoveEntries)
{
FilterRemoved.Add(newRemove);
base.Remove(newRemove);
}
}
SortInternal();
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
void addRemovedItemsBack(List<IGridEntry> addBackEntries)
{
//Add removed entries back into Items so they are displayed
//(except for episodes that are collapsed)
foreach (var addBack in addBackEntries)
{
if (addBack is ILibraryBookEntry lbe && lbe.Parent is ISeriesEntry se && !se.Liberate.Expanded)
continue;
FilterRemoved.Remove(addBack);
Add(addBack);
}
}
}
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
{
var filterResults = AllItems().FilterEntries(FilterString);
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
{
FilteredInGridEntries = filterResults;
refreshEntries();
} }
} }
@ -118,7 +159,7 @@ namespace LibationWinForms.GridView
public void CollapseItem(ISeriesEntry sEntry) public void CollapseItem(ISeriesEntry sEntry)
{ {
foreach (var episode in sEntry.Children.Join(Items.BookEntries(), o => o, i => i, (_, i) => i).ToList()) foreach (var episode in sEntry.Children.Intersect(Items.BookEntries()).ToList())
{ {
FilterRemoved.Add(episode); FilterRemoved.Add(episode);
base.Remove(episode); base.Remove(episode);
@ -131,9 +172,9 @@ namespace LibationWinForms.GridView
{ {
var sindex = Items.IndexOf(sEntry); var sindex = Items.IndexOf(sEntry);
foreach (var episode in sEntry.Children.Join(FilterRemoved.BookEntries(), o => o, i => i, (_, i) => i).ToList()) foreach (var episode in Comparer.OrderEntries(sEntry.Children.Intersect(FilterRemoved.BookEntries())).ToList())
{ {
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId)) if (FilteredInGridEntries?.Contains(episode) ?? true)
{ {
FilterRemoved.Remove(episode); FilterRemoved.Remove(episode);
InsertItem(++sindex, episode); InsertItem(++sindex, episode);
@ -144,106 +185,45 @@ namespace LibationWinForms.GridView
public void RemoveFilter() public void RemoveFilter()
{ {
if (FilterString is null) return;
int visibleCount = Items.Count;
foreach (var item in FilterRemoved.ToList())
{
if (item is ISeriesEntry || (item is ILibraryBookEntry lbe && (lbe.Liberate.IsBook || lbe.Parent.Liberate.Expanded)))
{
FilterRemoved.Remove(item);
InsertItem(visibleCount++, item);
}
}
if (IsSortedCore)
Sort();
else
//No user sort is applied, so do default sorting by DateAdded, descending
{
Comparer.PropertyName = nameof(IGridEntry.DateAdded);
Comparer.Direction = ListSortDirection.Descending;
Sort();
}
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
FilterString = null; FilterString = null;
SearchResults = null; FilteredInGridEntries = null;
refreshEntries();
} }
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
{ {
Comparer.PropertyName = property.Name; Comparer.PropertyName = property.Name;
Comparer.Direction = direction; Comparer.SortOrder = direction;
Sort(); SortInternal();
propertyDescriptor = property; propertyDescriptor = property;
listSortDirection = direction;
isSorted = true; isSorted = true;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
} }
protected void Sort() private void SortInternal()
{ {
var itemsList = (List<IGridEntry>)Items; var itemsList = (List<IGridEntry>)Items;
//User Order/OrderDescending and replace items in list instead of using List.Sort() to achieve stable sorting.
var children = itemsList.BookEntries().Where(i => i.Liberate.IsEpisode).ToList(); var sortedItems = Comparer.OrderEntries(itemsList).ToList();
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
itemsList.Clear(); itemsList.Clear();
//Only add parentless items at this stage. After these items are added in the
//correct sorting order, go back and add the children beneath their parents.
itemsList.AddRange(sortedItems); itemsList.AddRange(sortedItems);
foreach (var parent in children.Select(c => c.Parent).Distinct())
{
var pIndex = itemsList.IndexOf(parent);
//children are sorted beneath their series parent
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c, Comparer))
itemsList.Insert(++pIndex, c);
}
} }
protected override void OnListChanged(ListChangedEventArgs e) private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
{ {
if (e.ListChangedType == ListChangedType.ItemChanged) if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
{ refreshEntries();
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is ILibraryBookEntry lbItem)
{
SearchResults = SearchEngineCommands.Search(FilterString);
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
{
FilterRemoved.Add(lbItem);
base.Remove(lbItem);
return;
}
}
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
{
var item = Items[e.NewIndex];
Sort();
var newIndex = Items.IndexOf(item);
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
return;
}
}
base.OnListChanged(e);
} }
protected override void RemoveSortCore() protected override void RemoveSortCore()
{ {
isSorted = false; isSorted = false;
propertyDescriptor = base.SortPropertyCore; propertyDescriptor = base.SortPropertyCore;
listSortDirection = base.SortDirectionCore; Comparer.SortOrder = base.SortDirectionCore;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
} }

View File

@ -181,7 +181,7 @@ namespace LibationWinForms.GridView
geList.AddRange(seriesEntry.Children); geList.AddRange(seriesEntry.Children);
} }
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded)); bindingList = new GridEntryBindingList(geList);
bindingList.CollapseAll(); bindingList.CollapseAll();
syncBindingSource.DataSource = bindingList; syncBindingSource.DataSource = bindingList;
VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count()); VisibleCountChanged?.Invoke(this, bindingList.GetFilteredInItems().Count());
@ -195,8 +195,6 @@ namespace LibationWinForms.GridView
string existingFilter = syncBindingSource.Filter; string existingFilter = syncBindingSource.Filter;
Filter(null); Filter(null);
bindingList.SuspendFilteringOnUpdate = true;
//Add absent entries to grid, or update existing entry //Add absent entries to grid, or update existing entry
var allEntries = bindingList.AllItems().BookEntries(); var allEntries = bindingList.AllItems().BookEntries();
@ -219,8 +217,6 @@ namespace LibationWinForms.GridView
} }
} }
bindingList.SuspendFilteringOnUpdate = false;
//Re-apply filter after adding new/updating existing books to capture any changes //Re-apply filter after adding new/updating existing books to capture any changes
Filter(existingFilter); Filter(existingFilter);

View File

@ -0,0 +1,20 @@
using LibationUiBase.GridView;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.GridView
{
internal class RowComparer : RowComparerBase
{
public ListSortDirection SortOrder { get; set; } = ListSortDirection.Descending;
public override string PropertyName { get; set; } = nameof(IGridEntry.DateAdded);
protected override ListSortDirection GetSortOrder() => SortOrder;
/// <summary>
/// Helper method for ordering grid entries
/// </summary>
public IOrderedEnumerable<IGridEntry> OrderEntries(IEnumerable<IGridEntry> entries)
=> SortOrder is ListSortDirection.Descending ? entries.OrderDescending(this) : entries.Order(this);
}
}