Implement filtering in the sortable binding list.

This commit is contained in:
Michael Bucari-Tovo 2022-05-16 11:16:33 -06:00
parent 91d6181aec
commit d1bddeccc8
3 changed files with 157 additions and 70 deletions

View File

@ -21,31 +21,7 @@ namespace LibationWinForms
=> syncContext = SynchronizationContext.Current; => syncContext = SynchronizationContext.Current;
public override bool SupportsFiltering => true; public override bool SupportsFiltering => true;
public override string Filter { get => FilterString; set => SetFilter(value); }
private string FilterString;
private void SetFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
var searchResults = SearchEngineCommands.Search(filterString);
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
var allItems = ((SortableBindingList<GridEntry>)DataSource).InnerList;
var filterList = productIds.Join(allItems, s => s, ge => ge.AudibleProductId, (pid, ge) => ge).ToList();
((SortableBindingList<GridEntry>)DataSource).SetFilteredItems(filterList);
}
public override void RemoveFilter()
{
((SortableBindingList<GridEntry>)DataSource).RemoveFilter();
base.RemoveFilter();
}
protected override void OnListChanged(ListChangedEventArgs e) protected override void OnListChanged(ListChangedEventArgs e)
{ {
if (syncContext is not null) if (syncContext is not null)

View File

@ -159,7 +159,7 @@ namespace LibationWinForms
#region UI display functions #region UI display functions
private SortableBindingList<GridEntry> bindingList; private SortableFilterableBindingList bindingList;
private bool hasBeenDisplayed; private bool hasBeenDisplayed;
public event EventHandler InitialLoaded; public event EventHandler InitialLoaded;
@ -168,69 +168,56 @@ namespace LibationWinForms
// don't return early if lib size == 0. this will not update correctly if all books are removed // don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking(); var lib = DbContexts.GetLibrary_Flat_NoTracking();
var orderedBooks = lib
// default load order
.OrderByDescending(lb => lb.DateAdded)
//// more advanced example: sort by author, then series, then title
//.OrderBy(lb => lb.Book.AuthorNames)
// .ThenBy(lb => lb.Book.SeriesSortable)
// .ThenBy(lb => lb.Book.TitleSortable)
.ToList();
// bind
if (bindingList?.Count > 0)
updateGrid(orderedBooks);
else
bindToGrid(orderedBooks);
if (!hasBeenDisplayed) if (!hasBeenDisplayed)
{ {
// bind
bindToGrid(lib);
hasBeenDisplayed = true; hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new()); InitialLoaded?.Invoke(this, new());
VisibleCountChanged?.Invoke(this, bindingList.Count);
} }
else
updateGrid(lib);
} }
private void bindToGrid(List<DataLayer.LibraryBook> orderedBooks) private void bindToGrid(List<LibraryBook> orderedBooks)
{ {
bindingList = new SortableBindingList<GridEntry>(orderedBooks.Select(lb => toGridEntry(lb))); bindingList = new SortableFilterableBindingList(orderedBooks.OrderByDescending(lb => lb.DateAdded).Select(lb => new GridEntry(lb)));
gridEntryBindingSource.DataSource = bindingList; gridEntryBindingSource.DataSource = bindingList;
} }
private void updateGrid(List<DataLayer.LibraryBook> orderedBooks) private void updateGrid(List<LibraryBook> dbBooks)
{ {
for (var i = orderedBooks.Count - 1; i >= 0; i--) int visibleCount = bindingList.Count;
//Add absent books to grid, or update current books
for (var i = dbBooks.Count - 1; i >= 0; i--)
{ {
var libraryBook = orderedBooks[i]; var libraryBook = dbBooks[i];
var existingItem = bindingList.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId); var existingItem = bindingList.AllItems.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
// add new to top // add new to top
if (existingItem is null) if (existingItem is null)
bindingList.Insert(0, toGridEntry(libraryBook)); bindingList.Insert(0, new GridEntry(libraryBook));
// update existing // update existing
else else
existingItem.UpdateLibraryBook(libraryBook); existingItem.UpdateLibraryBook(libraryBook);
} }
// remove deleted from grid. note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this // remove deleted from grid.
var oldIds = bindingList.Select(ge => ge.AudibleProductId).ToList(); // note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var newIds = orderedBooks.Select(lb => lb.Book.AudibleProductId).ToList(); var removedBooks =
var remove = oldIds.Except(newIds).ToList(); bindingList
foreach (var id in remove) .AllItems
{ .ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
var oldItem = bindingList.FirstOrDefault(ge => ge.AudibleProductId == id); .ToList();
if (oldItem is not null)
bindingList.Remove(oldItem);
}
}
private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook) foreach (var removed in removedBooks)
{ bindingList.Remove(removed);
var entry = new GridEntry(libraryBook);
// see also notes in Libation/Source/__ARCHITECTURE NOTES.txt :: MVVM if (bindingList.Count != visibleCount)
entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender)); VisibleCountChanged?.Invoke(this, bindingList.Count);
return entry;
} }
#endregion #endregion
@ -239,23 +226,21 @@ namespace LibationWinForms
public void Filter(string searchString) public void Filter(string searchString)
{ {
if (_dataGridView.Rows.Count == 0) int visibleCount = bindingList.Count;
return;
if (string.IsNullOrEmpty(searchString)) if (string.IsNullOrEmpty(searchString))
gridEntryBindingSource.RemoveFilter(); gridEntryBindingSource.RemoveFilter();
else else
gridEntryBindingSource.Filter = searchString; gridEntryBindingSource.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.Count); VisibleCountChanged?.Invoke(this, bindingList.Count);
} }
#endregion #endregion
internal List<LibraryBook> GetVisible() internal List<LibraryBook> GetVisible()
=> bindingList => bindingList
.InnerList
.Select(row => row.LibraryBook) .Select(row => row.LibraryBook)
.ToList(); .ToList();

View File

@ -0,0 +1,126 @@
using ApplicationServices;
using Dinah.Core.DataBinding;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms
{
/*
* Allows filtering of the underlying SortableBindingList<GridEntry>
* by implementing IBindingListView aud using SearchEngineCommands
*
* When filtering is applied, the filtered-out items are removed
* from the base list and added to the private FilterRemoved list.
* All items, filtered or not, are stored in the private AllItems
* list. When filtering is removed, items in the FilterRemoved list
* are added back to the base list.
*
* Remove and InsertItem are overridden to ensure that the current
* filter remains applied when items are removed/added to the list.
*/
internal class SortableFilterableBindingList : SortableBindingList<GridEntry>, IBindingListView
{
/// <summary>
/// Items that were removed from the list due to filtering
/// </summary>
private readonly List<GridEntry> FilterRemoved = new();
/// <summary>
/// Tracks all items in the list, both filtered and not.
/// </summary>
public readonly List<GridEntry> AllItems;
private string FilterString;
private Action Sort;
public SortableFilterableBindingList(IEnumerable<GridEntry> enumeration) : base(enumeration)
{
AllItems = new List<GridEntry>(Items);
//This is only necessary because SortableBindingList doesn't expose Sort()
//You should make SortableBindingList.Sort protected and remove reflection
var method = typeof(SortableBindingList<GridEntry>).GetMethod("Sort", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Sort = method.CreateDelegate<Action>(this);
}
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
#region Unused - Advanced Filtering
public bool SupportsAdvancedSorting => false;
//This ApplySort overload is only called is SupportsAdvancedSorting is true.
//Otherwise BindingList.ApplySort() is used
public void ApplySort(ListSortDescriptionCollection sorts) => throw new NotImplementedException();
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
#endregion
public new void Remove(GridEntry entry)
{
AllItems.Remove(entry);
FilterRemoved.Remove(entry);
base.Remove(entry);
}
protected override void InsertItem(int index, GridEntry item)
{
AllItems.Insert(index, item);
if (FilterString is not null)
{
var searchResults = SearchEngineCommands.Search(FilterString);
//Decide if the new item matches the filter, and either insert it in the
//displayed items or the filtered out items list
if (searchResults.Docs.Any(d => d.ProductId == item.AudibleProductId))
base.InsertItem(index, item);
else
FilterRemoved.Add(item);
}
else
base.InsertItem(index, item);
}
private void ApplyFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
var searchResults = SearchEngineCommands.Search(filterString);
var productIds = searchResults.Docs.Select(d => d.ProductId).ToList();
for (int i = Items.Count - 1; i >= 0; i--)
{
if (!productIds.Contains(Items[i].AudibleProductId))
{
FilterRemoved.Add(Items[i]);
Items.RemoveAt(i);
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, i));
}
}
}
public void RemoveFilter()
{
if (FilterString is null) return;
for (int i = 0; i < FilterRemoved.Count; i++)
{
Items.Insert(i, FilterRemoved[i]);
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, i));
}
FilterRemoved.Clear();
if (IsSortedCore)
Sort();
else
//No user-defined sort is applied, so do default sorting by date added, descending
((List<GridEntry>)Items).Sort((i1,i2) =>i2.LibraryBook.DateAdded.CompareTo(i1.LibraryBook.DateAdded));
FilterString = null;
}
}
}