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;
|
=> 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)
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
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