Refactor ProductsDisplay
This commit is contained in:
parent
c01e1c3e4b
commit
dfedb23efd
@ -6,7 +6,7 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
|
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
|
||||||
MinWidth="265" MinHeight="110"
|
MinWidth="265" MinHeight="110"
|
||||||
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
|
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
|
||||||
Title="{Binding Caption}" IsExtendedIntoWindowDecorations="True" ShowInTaskbar="True"
|
Title="{Binding Caption}" ShowInTaskbar="True"
|
||||||
Icon="/Assets/1x1.png">
|
Icon="/Assets/1x1.png">
|
||||||
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
|
||||||
|
|
||||||
@ -34,13 +34,13 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</DockPanel.Styles>
|
</DockPanel.Styles>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
|
||||||
<Button Grid.Column="0" MinWidth="75" MinHeight="25" Name="Button1" Click="Button1_Click" Margin="5">
|
<Button Grid.Column="0" MinWidth="75" MinHeight="28" Name="Button1" Click="Button1_Click" Margin="5">
|
||||||
<TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/>
|
<TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="25" Name="Button2" Click="Button2_Click" Margin="5">
|
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="28" Name="Button2" Click="Button2_Click" Margin="5">
|
||||||
<TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/>
|
<TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="25" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
|
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="28" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
|
||||||
<TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
|
<TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@ -136,7 +136,8 @@
|
|||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
|
||||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.4.2" />
|
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0-preview4" />
|
||||||
|
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,177 +0,0 @@
|
|||||||
using ApplicationServices;
|
|
||||||
using LibationSearchEngine;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Allows filtering of the underlying ObservableCollection<GridEntry>
|
|
||||||
*
|
|
||||||
* When filtering is applied, the filtered-out items are removed
|
|
||||||
* from the base list and added to the private FilterRemoved list.
|
|
||||||
* When filtering is removed, items in the FilterRemoved list are
|
|
||||||
* added back to the base list.
|
|
||||||
*
|
|
||||||
* Items are added and removed to/from the ObservableCollection's
|
|
||||||
* internal list instead of the ObservableCollection itself to
|
|
||||||
* avoid ObservableCollection firing CollectionChanged for every
|
|
||||||
* item. Editing the list this way improve's display performance,
|
|
||||||
* but requires ResetCollection() to be called after all changes
|
|
||||||
* have been made.
|
|
||||||
*/
|
|
||||||
public class GridEntryCollection : ObservableCollection<GridEntry>
|
|
||||||
{
|
|
||||||
public GridEntryCollection(IEnumerable<GridEntry> enumeration)
|
|
||||||
: base(new List<GridEntry>(enumeration)) { }
|
|
||||||
public GridEntryCollection(List<GridEntry> list)
|
|
||||||
: base(list) { }
|
|
||||||
|
|
||||||
public List<GridEntry> InternalList => Items as List<GridEntry>;
|
|
||||||
/// <returns>All items in the list, including those filtered out.</returns>
|
|
||||||
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
|
|
||||||
|
|
||||||
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
|
|
||||||
public bool SuspendFilteringOnUpdate { get; set; }
|
|
||||||
public string Filter { get => FilterString; set => ApplyFilter(value); }
|
|
||||||
|
|
||||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
|
||||||
private readonly List<GridEntry> FilterRemoved = new();
|
|
||||||
private string FilterString;
|
|
||||||
private SearchResultSet SearchResults;
|
|
||||||
|
|
||||||
#region Items Management
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes all items from the collection, both visible and hidden, adds new items to the visible collection.
|
|
||||||
/// </summary>
|
|
||||||
public void ReplaceList(IEnumerable<GridEntry> newItems)
|
|
||||||
{
|
|
||||||
Items.Clear();
|
|
||||||
FilterRemoved.Clear();
|
|
||||||
((List<GridEntry>)Items).AddRange(newItems);
|
|
||||||
ResetCollection();
|
|
||||||
}
|
|
||||||
public void ResetCollection()
|
|
||||||
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Filtering
|
|
||||||
|
|
||||||
|
|
||||||
private void ApplyFilter(string filterString)
|
|
||||||
{
|
|
||||||
if (filterString != FilterString)
|
|
||||||
RemoveFilter();
|
|
||||||
|
|
||||||
FilterString = filterString;
|
|
||||||
SearchResults = SearchEngineCommands.Search(filterString);
|
|
||||||
|
|
||||||
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)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);
|
|
||||||
Items.Remove(item);
|
|
||||||
}
|
|
||||||
ResetCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveFilter()
|
|
||||||
{
|
|
||||||
if (FilterString is null) return;
|
|
||||||
|
|
||||||
int visibleCount = Items.Count;
|
|
||||||
|
|
||||||
foreach (var item in FilterRemoved.ToList())
|
|
||||||
{
|
|
||||||
if (item is SeriesEntry || item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
|
|
||||||
{
|
|
||||||
|
|
||||||
FilterRemoved.Remove(item);
|
|
||||||
Items.Insert(visibleCount++, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterString = null;
|
|
||||||
SearchResults = null;
|
|
||||||
ResetCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Expand/Collapse
|
|
||||||
|
|
||||||
public void CollapseAll()
|
|
||||||
{
|
|
||||||
foreach (var series in Items.SeriesEntries().ToList())
|
|
||||||
CollapseItem(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExpandAll()
|
|
||||||
{
|
|
||||||
foreach (var series in Items.SeriesEntries().ToList())
|
|
||||||
ExpandItem(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CollapseItem(SeriesEntry sEntry)
|
|
||||||
{
|
|
||||||
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
|
|
||||||
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
|
|
||||||
* event handler causes serious performance problems. And unfotrunately, Avalonia
|
|
||||||
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
|
||||||
* overload that would fire only once for all changed items.
|
|
||||||
*
|
|
||||||
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
|
|
||||||
*/
|
|
||||||
|
|
||||||
FilterRemoved.Add(episode);
|
|
||||||
Items.Remove(episode);
|
|
||||||
}
|
|
||||||
|
|
||||||
sEntry.Liberate.Expanded = false;
|
|
||||||
ResetCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExpandItem(SeriesEntry sEntry)
|
|
||||||
{
|
|
||||||
var sindex = Items.IndexOf(sEntry);
|
|
||||||
|
|
||||||
foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
|
|
||||||
{
|
|
||||||
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
|
|
||||||
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
|
|
||||||
* event handler causes serious performance problems. And unfotrunately, Avalonia
|
|
||||||
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
|
|
||||||
* overload that would fire only once for all changed items.
|
|
||||||
*
|
|
||||||
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
|
|
||||||
*/
|
|
||||||
|
|
||||||
FilterRemoved.Remove(episode);
|
|
||||||
Items.Insert(++sindex, episode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sEntry.Liberate.Expanded = true;
|
|
||||||
ResetCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -6,12 +5,9 @@ using System.ComponentModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Reflection;
|
|
||||||
using System.Collections;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using AudibleUtilities;
|
using AudibleUtilities;
|
||||||
using LibationAvalonia.Views;
|
|
||||||
using LibationAvalonia.Dialogs.Login;
|
using LibationAvalonia.Dialogs.Login;
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
|
|
||||||
@ -24,81 +20,35 @@ namespace LibationAvalonia.ViewModels
|
|||||||
public event EventHandler<int> RemovableCountChanged;
|
public event EventHandler<int> RemovableCountChanged;
|
||||||
public event EventHandler InitialLoaded;
|
public event EventHandler InitialLoaded;
|
||||||
|
|
||||||
private DataGridColumn _currentSortColumn;
|
/// <summary>Backing list of all grid entries</summary>
|
||||||
private DataGrid productsDataGrid;
|
private readonly List<GridEntry> SOURCE = new();
|
||||||
|
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
|
||||||
|
private List<GridEntry> FilteredInGridEntries;
|
||||||
|
public string FilterString { get; private set; }
|
||||||
|
public DataGridCollectionView GridEntries { get; }
|
||||||
|
|
||||||
private GridEntryCollection _gridEntries;
|
|
||||||
private bool _removeColumnVisivle;
|
private bool _removeColumnVisivle;
|
||||||
public GridEntryCollection GridEntries { get => _gridEntries; private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); }
|
|
||||||
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
|
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
|
||||||
|
|
||||||
public List<LibraryBook> GetVisibleBookEntries()
|
public List<LibraryBook> GetVisibleBookEntries()
|
||||||
=> GridEntries.InternalList
|
=> GridEntries
|
||||||
|
.Cast<GridEntry>()
|
||||||
.BookEntries()
|
.BookEntries()
|
||||||
.Select(lbe => lbe.LibraryBook)
|
.Select(lbe => lbe.LibraryBook)
|
||||||
.ToList();
|
.ToList();
|
||||||
public IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
|
||||||
=> GridEntries
|
private IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||||
.AllItems()
|
=> SOURCE
|
||||||
.BookEntries();
|
.BookEntries();
|
||||||
public ProductsDisplayViewModel() { }
|
|
||||||
public ProductsDisplayViewModel(List<GridEntry> items)
|
public ProductsDisplayViewModel()
|
||||||
{
|
{
|
||||||
GridEntries = new GridEntryCollection(items);
|
GridEntries = new(SOURCE);
|
||||||
|
GridEntries.Filter = CollectionFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Display Functions
|
#region Display Functions
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Call once on load so we can modify access a private member with reflection
|
|
||||||
/// </summary>
|
|
||||||
public void RegisterCollectionChanged(ProductsDisplay productsDisplay = null)
|
|
||||||
{
|
|
||||||
productsDataGrid ??= productsDisplay?.productsGrid;
|
|
||||||
|
|
||||||
if (GridEntries is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//Avalonia displays items in the DataConncetion from an internal copy of
|
|
||||||
//the bound list, not the actual bound list. So we need to reflect to get
|
|
||||||
//the current display order and set each GridEntry.ListIndex correctly.
|
|
||||||
var DataConnection_PI = typeof(DataGrid).GetProperty("DataConnection", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
||||||
var DataSource_PI = DataConnection_PI.PropertyType.GetProperty("DataSource", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
|
|
||||||
GridEntries.CollectionChanged += (s, e) =>
|
|
||||||
{
|
|
||||||
if (s != GridEntries) return;
|
|
||||||
|
|
||||||
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsDataGrid))).Cast<GridEntry>();
|
|
||||||
int index = 0;
|
|
||||||
foreach (var di in displayListGE)
|
|
||||||
{
|
|
||||||
di.ListIndex = index++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Only call once per lifetime
|
|
||||||
/// </summary>
|
|
||||||
public void InitialDisplay(List<LibraryBook> dbBooks)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
GridEntries = new GridEntryCollection(CreateGridEntries(dbBooks));
|
|
||||||
GridEntries.CollapseAll();
|
|
||||||
|
|
||||||
InitialLoaded?.Invoke(this, EventArgs.Empty);
|
|
||||||
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
|
||||||
|
|
||||||
RegisterCollectionChanged();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Call when there's been a change to the library
|
/// Call when there's been a change to the library
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -106,29 +56,25 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//List is already displayed. Replace all items with new ones, refilter, and re-sort
|
var existingSeriesEntries = SOURCE.SeriesEntries().ToList();
|
||||||
string existingFilter = GridEntries?.Filter;
|
|
||||||
var newEntries = CreateGridEntries(dbBooks);
|
|
||||||
|
|
||||||
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList();
|
SOURCE.Clear();
|
||||||
|
SOURCE.AddRange(CreateGridEntries(dbBooks));
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
//If replacing the list, preserve user's existing collapse/expand
|
||||||
{
|
//state. When resetting a list, default state is cosed.
|
||||||
GridEntries.ReplaceList(newEntries);
|
|
||||||
|
|
||||||
//We're replacing the list, so preserve usere's existing collapse/expand
|
|
||||||
//state. When resetting a list, default state is open.
|
|
||||||
foreach (var series in existingSeriesEntries)
|
foreach (var series in existingSeriesEntries)
|
||||||
{
|
{
|
||||||
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
|
var sEntry = SOURCE.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
|
||||||
if (sEntry is SeriesEntry se && !series.Liberate.Expanded)
|
if (sEntry is SeriesEntry se)
|
||||||
GridEntries.CollapseItem(se);
|
se.Liberate.Expanded = series.Liberate.Expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
GridEntries.Filter = existingFilter;
|
//Run query on new list
|
||||||
ReSort();
|
FilteredInGridEntries = QueryResults(SOURCE, FilterString);
|
||||||
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
|
||||||
});
|
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -136,7 +82,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
|
private static List<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
var geList = dbBooks
|
var geList = dbBooks
|
||||||
.Where(lb => lb.Book.IsProduct())
|
.Where(lb => lb.Book.IsProduct())
|
||||||
@ -159,81 +105,74 @@ namespace LibationAvalonia.ViewModels
|
|||||||
geList.Add(seriesEntry);
|
geList.Add(seriesEntry);
|
||||||
geList.AddRange(seriesEntry.Children);
|
geList.AddRange(seriesEntry.Children);
|
||||||
}
|
}
|
||||||
return geList.OrderByDescending(e => e.DateAdded);
|
|
||||||
|
var bookList = geList.OrderByDescending(e => e.DateAdded).ToList();
|
||||||
|
|
||||||
|
//ListIndex is used by RowComparer to make column sort stable
|
||||||
|
int index = 0;
|
||||||
|
foreach (GridEntry di in bookList)
|
||||||
|
di.ListIndex = index++;
|
||||||
|
|
||||||
|
return bookList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
|
public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
|
||||||
{
|
{
|
||||||
if (seriesEntry.Liberate.Expanded)
|
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
|
||||||
GridEntries.CollapseItem(seriesEntry);
|
GridEntries.Refresh();
|
||||||
else
|
|
||||||
GridEntries.ExpandItem(seriesEntry);
|
|
||||||
|
|
||||||
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Filtering
|
#region Filtering
|
||||||
|
|
||||||
public async Task Filter(string searchString)
|
public async Task Filter(string searchString)
|
||||||
{
|
{
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
if (searchString == FilterString)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FilteredInGridEntries = QueryResults(SOURCE, searchString);
|
||||||
|
|
||||||
|
FilterString = searchString;
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CollectionFilter(object item)
|
||||||
{
|
{
|
||||||
int visibleCount = GridEntries.Count;
|
if (item is LibraryBookEntry lbe
|
||||||
|
&& lbe.IsEpisode
|
||||||
|
&& lbe.Parent?.Liberate?.Expanded != true)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(searchString))
|
if (FilteredInGridEntries is null) return true;
|
||||||
GridEntries.RemoveFilter();
|
|
||||||
else
|
|
||||||
GridEntries.Filter = searchString;
|
|
||||||
|
|
||||||
if (visibleCount != GridEntries.Count)
|
return FilteredInGridEntries.Contains(item);
|
||||||
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
|
}
|
||||||
|
|
||||||
//Re-sort after filtering
|
private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString)
|
||||||
ReSort();
|
{
|
||||||
});
|
if (string.IsNullOrEmpty(searchString)) return null;
|
||||||
|
|
||||||
|
var SearchResults = SearchEngineCommands.Search(searchString);
|
||||||
|
|
||||||
|
var booksFilteredIn = entries.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
|
||||||
|
|
||||||
|
//Find all series containing children that match the search criteria
|
||||||
|
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
|
||||||
|
|
||||||
|
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Sorting
|
|
||||||
|
|
||||||
public void Sort(DataGridColumn sortColumn)
|
|
||||||
{
|
|
||||||
//Force the comparer to get the current sort order. We can't
|
|
||||||
//retrieve it from inside this event handler because Avalonia
|
|
||||||
//doesn't set the property until after this event.
|
|
||||||
var comparer = sortColumn.CustomSortComparer as RowComparer;
|
|
||||||
comparer.SortDirection = null;
|
|
||||||
|
|
||||||
_currentSortColumn = sortColumn;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Must be invoked on UI thread
|
|
||||||
private void ReSort()
|
|
||||||
{
|
|
||||||
if (_currentSortColumn is null)
|
|
||||||
{
|
|
||||||
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia.
|
|
||||||
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry.DateAdded));
|
|
||||||
GridEntries.InternalList.Sort(defaultComparer);
|
|
||||||
GridEntries.InternalList.Reverse();
|
|
||||||
GridEntries.ResetCollection();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Scan and Remove Books
|
#region Scan and Remove Books
|
||||||
|
|
||||||
public void DoneRemovingBooks()
|
public void DoneRemovingBooks()
|
||||||
{
|
{
|
||||||
foreach (var item in GridEntries.AllItems())
|
foreach (var item in SOURCE)
|
||||||
item.PropertyChanged -= Item_PropertyChanged;
|
item.PropertyChanged -= GridEntry_PropertyChanged;
|
||||||
RemoveColumnVisivle = false;
|
RemoveColumnVisivle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,41 +195,39 @@ namespace LibationAvalonia.ViewModels
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var book in selectedBooks)
|
foreach (var book in selectedBooks)
|
||||||
book.PropertyChanged -= Item_PropertyChanged;
|
book.PropertyChanged -= GridEntry_PropertyChanged;
|
||||||
|
|
||||||
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
|
||||||
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
|
||||||
|
|
||||||
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
|
||||||
//so there's no need to remove books from the grid display here.
|
|
||||||
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
|
|
||||||
|
|
||||||
foreach (var b in GetAllBookEntries())
|
|
||||||
b.Remove = false;
|
|
||||||
|
|
||||||
RemovableCountChanged?.Invoke(this, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//After ProductsDisplay2.Display() re-creates the list,
|
//After DisplayBooks() re-creates the list,
|
||||||
//re-subscribe to all items' PropertyChanged events.
|
//re-subscribe to all items' PropertyChanged events.
|
||||||
|
|
||||||
foreach (var b in GetAllBookEntries())
|
foreach (var b in GetAllBookEntries())
|
||||||
b.PropertyChanged += Item_PropertyChanged;
|
b.PropertyChanged += GridEntry_PropertyChanged;
|
||||||
|
|
||||||
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GridEntries.CollectionChanged += BindingList_CollectionChanged;
|
||||||
|
|
||||||
|
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
|
||||||
|
//so there's no need to remove books from the grid display here.
|
||||||
|
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
|
||||||
|
|
||||||
|
RemovableCountChanged?.Invoke(this, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
|
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
|
||||||
{
|
{
|
||||||
foreach (var item in GridEntries.AllItems())
|
foreach (var item in SOURCE)
|
||||||
{
|
{
|
||||||
item.Remove = false;
|
item.Remove = false;
|
||||||
item.PropertyChanged += Item_PropertyChanged;
|
item.PropertyChanged += GridEntry_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveColumnVisivle = true;
|
RemoveColumnVisivle = true;
|
||||||
@ -303,9 +240,6 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
var allBooks = GetAllBookEntries();
|
var allBooks = GetAllBookEntries();
|
||||||
|
|
||||||
foreach (var b in allBooks)
|
|
||||||
b.Remove = false;
|
|
||||||
|
|
||||||
var lib = allBooks
|
var lib = allBooks
|
||||||
.Select(lbe => lbe.LibraryBook)
|
.Select(lbe => lbe.LibraryBook)
|
||||||
.Where(lb => !lb.Book.HasLiberated());
|
.Where(lb => !lb.Book.HasLiberated());
|
||||||
@ -327,7 +261,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
|
if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,25 +13,19 @@ namespace LibationAvalonia.ViewModels
|
|||||||
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
|
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
|
||||||
/// properties when 2 items compare equal.
|
/// properties when 2 items compare equal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class RowComparer : IComparer, IComparer<GridEntry>
|
internal class RowComparer : IComparer, IComparer<GridEntry>, 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; }
|
public DataGridColumn Column { get; init; }
|
||||||
public string PropertyName { get; private set; }
|
public string PropertyName { get; private set; }
|
||||||
public ListSortDirection? SortDirection { get; set; }
|
|
||||||
|
|
||||||
public RowComparer(DataGridColumn column)
|
public RowComparer(DataGridColumn column)
|
||||||
{
|
{
|
||||||
Column = column;
|
Column = column;
|
||||||
PropertyName = Column.SortMemberPath;
|
PropertyName = Column.SortMemberPath;
|
||||||
}
|
}
|
||||||
public RowComparer(ListSortDirection direction, string propertyName)
|
|
||||||
{
|
|
||||||
SortDirection = direction;
|
|
||||||
PropertyName = propertyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Compare(object x, object y)
|
public int Compare(object x, object y)
|
||||||
{
|
{
|
||||||
@ -42,7 +36,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
var geA = (GridEntry)x;
|
var geA = (GridEntry)x;
|
||||||
var geB = (GridEntry)y;
|
var geB = (GridEntry)y;
|
||||||
|
|
||||||
SortDirection ??= GetSortOrder();
|
var sortDirection = GetSortOrder();
|
||||||
|
|
||||||
SeriesEntry parentA = null;
|
SeriesEntry parentA = null;
|
||||||
SeriesEntry parentB = null;
|
SeriesEntry parentB = null;
|
||||||
@ -54,16 +48,16 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
//both a and b are top-level grid entries
|
//both a and b are top-level grid entries
|
||||||
if (parentA is null && parentB is null)
|
if (parentA is null && parentB is null)
|
||||||
return InternalCompare(geA, geB);
|
return InternalCompare(geA, geB, sortDirection);
|
||||||
|
|
||||||
//a is top-level, b is a child
|
//a is top-level, b is a child
|
||||||
if (parentA is null && parentB is not null)
|
if (parentA is null && parentB is not null)
|
||||||
{
|
{
|
||||||
// b is a child of a, parent is always first
|
// b is a child of a, parent is always first
|
||||||
if (parentB == geA)
|
if (parentB == geA)
|
||||||
return SortDirection is ListSortDirection.Ascending ? -1 : 1;
|
return sortDirection is ListSortDirection.Ascending ? -1 : 1;
|
||||||
else
|
else
|
||||||
return InternalCompare(geA, parentB);
|
return InternalCompare(geA, parentB, sortDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
//a is a child, b is a top-level
|
//a is a child, b is a top-level
|
||||||
@ -71,24 +65,24 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
// a is a child of b, parent is always first
|
// a is a child of b, parent is always first
|
||||||
if (parentA == geB)
|
if (parentA == geB)
|
||||||
return SortDirection is ListSortDirection.Ascending ? 1 : -1;
|
return sortDirection is ListSortDirection.Ascending ? 1 : -1;
|
||||||
else
|
else
|
||||||
return InternalCompare(parentA, geB);
|
return InternalCompare(parentA, geB, sortDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
//both are children of the same series, always present in order of series index, ascending
|
//both are children of the same series, always present in order of series index, ascending
|
||||||
if (parentA == parentB)
|
if (parentA == parentB)
|
||||||
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1);
|
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
|
||||||
|
|
||||||
//a and b are children of different series.
|
//a and b are children of different series.
|
||||||
return InternalCompare(parentA, parentB);
|
return InternalCompare(parentA, parentB, sortDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
//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()
|
private ListSortDirection? GetSortOrder()
|
||||||
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
|
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
|
||||||
|
|
||||||
private int InternalCompare(GridEntry x, GridEntry y)
|
private int InternalCompare(GridEntry x, GridEntry y, ListSortDirection? sortDirection)
|
||||||
{
|
{
|
||||||
var val1 = x.GetMemberValue(PropertyName);
|
var val1 = x.GetMemberValue(PropertyName);
|
||||||
var val2 = y.GetMemberValue(PropertyName);
|
var val2 = y.GetMemberValue(PropertyName);
|
||||||
@ -98,7 +92,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
//If items compare equal, compare them by their positions in the the list.
|
//If items compare equal, compare them by their positions in the the list.
|
||||||
//This is how you achieve a stable sort.
|
//This is how you achieve a stable sort.
|
||||||
if (compareResult == 0)
|
if (compareResult == 0)
|
||||||
return x.ListIndex.CompareTo(y.ListIndex);
|
return x.ListIndex.CompareTo(y.ListIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
|
||||||
else
|
else
|
||||||
return compareResult;
|
return compareResult;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
|
public SeriesEntry(LibraryBook parent, IEnumerable<LibraryBook> children)
|
||||||
{
|
{
|
||||||
Liberate = new LiberateButtonStatus(IsSeries) { Expanded = true };
|
Liberate = new LiberateButtonStatus(IsSeries);
|
||||||
SeriesIndex = -1;
|
SeriesIndex = -1;
|
||||||
LibraryBook = parent;
|
LibraryBook = parent;
|
||||||
|
|
||||||
|
|||||||
@ -174,13 +174,12 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
public void ProductsDisplay_Initialized1(object sender, EventArgs e)
|
public void ProductsDisplay_Initialized1(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is ProductsDisplay products)
|
|
||||||
_viewModel.ProductsDisplay.RegisterCollectionChanged(products);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
|
||||||
{
|
{
|
||||||
_viewModel.ProductsDisplay.InitialDisplay(dbBooks);
|
await _viewModel.ProductsDisplay.DisplayBooks(dbBooks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
|
|||||||
@ -21,15 +21,24 @@
|
|||||||
|
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
|
|
||||||
<controls:DataGridCheckBoxColumnExt
|
<DataGridTemplateColumn
|
||||||
PropertyChanged="RemoveColumn_PropertyChanged"
|
|
||||||
IsVisible="{Binding RemoveColumnVisivle}"
|
|
||||||
Header="Remove"
|
|
||||||
IsThreeState="True"
|
|
||||||
IsReadOnly="False"
|
|
||||||
CanUserSort="True"
|
CanUserSort="True"
|
||||||
Binding="{Binding Remove, Mode=TwoWay}"
|
IsVisible="{Binding RemoveColumnVisivle}"
|
||||||
Width="70" SortMemberPath="Remove" />
|
PropertyChanged="RemoveColumn_PropertyChanged"
|
||||||
|
Header="Remove"
|
||||||
|
IsReadOnly="False"
|
||||||
|
SortMemberPath="Remove"
|
||||||
|
Width="75">
|
||||||
|
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<CheckBox
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
IsThreeState="True"
|
||||||
|
IsChecked="{Binding Remove, Mode=TwoWay}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
|
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
|||||||
@ -27,20 +27,25 @@ namespace LibationAvalonia.Views
|
|||||||
if (Design.IsDesignMode)
|
if (Design.IsDesignMode)
|
||||||
{
|
{
|
||||||
using var context = DbContexts.GetContext();
|
using var context = DbContexts.GetContext();
|
||||||
List<GridEntry> sampleEntries = new()
|
List<LibraryBook> sampleEntries = new()
|
||||||
{
|
{
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU")),
|
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")),
|
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
|
||||||
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")),
|
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
|
||||||
};
|
};
|
||||||
DataContext = new ProductsDisplayViewModel(sampleEntries);
|
|
||||||
|
var pdvm = new ProductsDisplayViewModel();
|
||||||
|
pdvm.DisplayBooks(sampleEntries);
|
||||||
|
DataContext = pdvm;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Configure_ColumnCustomization();
|
Configure_ColumnCustomization();
|
||||||
|
|
||||||
foreach (var column in productsGrid.Columns)
|
foreach (var column in productsGrid.Columns)
|
||||||
@ -51,7 +56,7 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
|
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
|
||||||
{
|
{
|
||||||
_viewModel.Sort(e.Column);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user