Implement filtering in the sortable binding list.
This commit is contained in:
parent
91d6181aec
commit
d1bddeccc8
@ -21,31 +21,7 @@ namespace LibationWinForms
|
||||
=> syncContext = SynchronizationContext.Current;
|
||||
|
||||
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)
|
||||
{
|
||||
if (syncContext is not null)
|
||||
|
||||
@ -159,7 +159,7 @@ namespace LibationWinForms
|
||||
|
||||
#region UI display functions
|
||||
|
||||
private SortableBindingList<GridEntry> bindingList;
|
||||
private SortableFilterableBindingList bindingList;
|
||||
|
||||
private bool hasBeenDisplayed;
|
||||
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
|
||||
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)
|
||||
{
|
||||
// bind
|
||||
bindToGrid(lib);
|
||||
hasBeenDisplayed = true;
|
||||
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;
|
||||
}
|
||||
|
||||
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 existingItem = bindingList.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
var libraryBook = dbBooks[i];
|
||||
var existingItem = bindingList.AllItems.FirstOrDefault(i => i.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||
|
||||
// add new to top
|
||||
if (existingItem is null)
|
||||
bindingList.Insert(0, toGridEntry(libraryBook));
|
||||
bindingList.Insert(0, new GridEntry(libraryBook));
|
||||
// update existing
|
||||
else
|
||||
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
|
||||
var oldIds = bindingList.Select(ge => ge.AudibleProductId).ToList();
|
||||
var newIds = orderedBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
||||
var remove = oldIds.Except(newIds).ToList();
|
||||
foreach (var id in remove)
|
||||
{
|
||||
var oldItem = bindingList.FirstOrDefault(ge => ge.AudibleProductId == id);
|
||||
if (oldItem is not null)
|
||||
bindingList.Remove(oldItem);
|
||||
}
|
||||
}
|
||||
// remove deleted from grid.
|
||||
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
|
||||
var removedBooks =
|
||||
bindingList
|
||||
.AllItems
|
||||
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId)
|
||||
.ToList();
|
||||
|
||||
private GridEntry toGridEntry(DataLayer.LibraryBook libraryBook)
|
||||
{
|
||||
var entry = new GridEntry(libraryBook);
|
||||
// see also notes in Libation/Source/__ARCHITECTURE NOTES.txt :: MVVM
|
||||
entry.LibraryBookUpdated += (sender, _) => _dataGridView.InvalidateRow(_dataGridView.GetRowIdOfBoundItem((GridEntry)sender));
|
||||
return entry;
|
||||
foreach (var removed in removedBooks)
|
||||
bindingList.Remove(removed);
|
||||
|
||||
if (bindingList.Count != visibleCount)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -239,23 +226,21 @@ namespace LibationWinForms
|
||||
|
||||
public void Filter(string searchString)
|
||||
{
|
||||
if (_dataGridView.Rows.Count == 0)
|
||||
return;
|
||||
int visibleCount = bindingList.Count;
|
||||
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
gridEntryBindingSource.RemoveFilter();
|
||||
else
|
||||
gridEntryBindingSource.Filter = searchString;
|
||||
|
||||
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
if (visibleCount != bindingList.Count)
|
||||
VisibleCountChanged?.Invoke(this, bindingList.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal List<LibraryBook> GetVisible()
|
||||
=> bindingList
|
||||
.InnerList
|
||||
.Select(row => row.LibraryBook)
|
||||
.ToList();
|
||||
|
||||
|
||||
126
Source/LibationWinForms/grid/SortableFilterableBindingList.cs
Normal file
126
Source/LibationWinForms/grid/SortableFilterableBindingList.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user