Further sorting and remove books refinements

This commit is contained in:
Michael Bucari-Tovo 2022-07-14 21:14:40 -06:00
parent efd6156fa8
commit 7b7e1d8574
9 changed files with 89 additions and 46 deletions

View File

@ -3,10 +3,15 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI;
namespace LibationWinForms.AvaloniaUI.ViewModels namespace LibationWinForms.AvaloniaUI.ViewModels
{ {
public class MainWindowViewModel public class MainWindowViewModel : ViewModelBase
{ {
private string _removeBooksButtonText = "Remove # Books from Libation";
private bool _removeButtonsVisible = true;
public string RemoveBooksButtonText { get => _removeBooksButtonText; set => this.RaiseAndSetIfChanged(ref _removeBooksButtonText, value); }
public bool RemoveButtonsVisible { get => _removeButtonsVisible; set => this.RaiseAndSetIfChanged(ref _removeButtonsVisible, value); }
} }
} }

View File

@ -13,7 +13,7 @@ namespace LibationWinForms.AvaloniaUI.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 internal class RowComparer : IComparer, IComparer<GridEntry2>
{ {
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);
@ -81,7 +81,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
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 Compare(parentA, parentB); 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
@ -102,5 +102,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
else else
return compareResult; return compareResult;
} }
public int Compare(GridEntry2 x, GridEntry2 y)
{
return Compare((object)x, y);
}
} }
} }

View File

@ -55,7 +55,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
public SeriesEntrys2(LibraryBook parent, IEnumerable<LibraryBook> children) public SeriesEntrys2(LibraryBook parent, IEnumerable<LibraryBook> children)
{ {
Liberate = new LiberateButtonStatus2(IsSeries); Liberate = new LiberateButtonStatus2(IsSeries) { Expanded = true };
SeriesIndex = -1; SeriesIndex = -1;
LibraryBook = parent; LibraryBook = parent;

View File

@ -10,8 +10,10 @@ namespace LibationWinForms.AvaloniaUI.Views
{ {
private void Configure_RemoveBooks() private void Configure_RemoveBooks()
{ {
removeBooksBtn.IsVisible = false; if (Avalonia.Controls.Design.IsDesignMode)
doneRemovingBtn.IsVisible = false; return;
_viewModel.RemoveButtonsVisible = false;
removeLibraryBooksToolStripMenuItem.Click += removeLibraryBooksToolStripMenuItem_Click; removeLibraryBooksToolStripMenuItem.Click += removeLibraryBooksToolStripMenuItem_Click;
} }
@ -22,8 +24,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void doneRemovingBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
removeBooksBtn.IsVisible = false; _viewModel.RemoveButtonsVisible = false;
doneRemovingBtn.IsVisible = false;
productsDisplay.CloseRemoveBooksColumn(); productsDisplay.CloseRemoveBooksColumn();
@ -80,15 +81,14 @@ namespace LibationWinForms.AvaloniaUI.Views
filterSearchTb.IsVisible = false; filterSearchTb.IsVisible = false;
productsDisplay.Filter(null); productsDisplay.Filter(null);
removeBooksBtn.IsVisible = true; _viewModel.RemoveButtonsVisible = true;
doneRemovingBtn.IsVisible = true;
await productsDisplay.ScanAndRemoveBooksAsync(accounts); await productsDisplay.ScanAndRemoveBooksAsync(accounts);
} }
public void productsDisplay_RemovableCountChanged(object sender, int removeCount) public void productsDisplay_RemovableCountChanged(object sender, int removeCount)
{ {
removeBooksBtn.Content = removeCount switch _viewModel.RemoveBooksButtonText = removeCount switch
{ {
1 => "Remove 1 Book from Libation", 1 => "Remove 1 Book from Libation",
_ => $"Remove {removeCount} Books from Libation" _ => $"Remove {removeCount} Books from Libation"

View File

@ -9,7 +9,7 @@
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls" xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700" mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow" x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
Title="MainWindow" Title="Libation"
Name="Form1" Name="Form1"
Icon="/AvaloniaUI/Assets/glass-with-glow_16.png"> Icon="/AvaloniaUI/Assets/glass-with-glow_16.png">
@ -128,8 +128,8 @@
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Name="removeBooksBtn" Click="removeBooksBtn_Click" Height="30" Width="220" Content="Remove # Books from Libation"/> <Button IsVisible="{Binding RemoveButtonsVisible}" Click="removeBooksBtn_Click" Height="30" Width="220" Content="{Binding RemoveBooksButtonText}"/>
<Button Name="doneRemovingBtn" Click="doneRemovingBtn_Click" Height="30" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/> <Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/>
</StackPanel> </StackPanel>
<TextBox Grid.Column="1" Name="filterSearchTb" KeyDown="filterSearchTb_KeyPress" /> <TextBox Grid.Column="1" Name="filterSearchTb" KeyDown="filterSearchTb_KeyPress" />

View File

@ -17,6 +17,7 @@ namespace LibationWinForms.AvaloniaUI.Views
{ {
public event EventHandler Load; public event EventHandler Load;
public event EventHandler<List<LibraryBook>> LibraryLoaded; public event EventHandler<List<LibraryBook>> LibraryLoaded;
private MainWindowViewModel _viewModel;
public MainWindow() public MainWindow()
{ {
@ -26,6 +27,8 @@ namespace LibationWinForms.AvaloniaUI.Views
#endif #endif
this.FindAllControls(); this.FindAllControls();
this.DataContext = _viewModel = new MainWindowViewModel();
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it // eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
Configure_BackupCounts(); Configure_BackupCounts();
Configure_ScanAuto(); Configure_ScanAuto();
@ -43,8 +46,8 @@ namespace LibationWinForms.AvaloniaUI.Views
Configure_NonUI(); Configure_NonUI();
{ {
this.LibraryLoaded += (_, dbBooks) => productsDisplay.Display(dbBooks); this.LibraryLoaded += async (_, dbBooks) => await productsDisplay.Display(dbBooks);
LibraryCommands.LibrarySizeChanged += (_, _) => productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); LibraryCommands.LibrarySizeChanged += async (_, _) => await productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance); this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
} }
} }
@ -107,9 +110,6 @@ namespace LibationWinForms.AvaloniaUI.Views
filterBtn = this.FindControl<Button>(nameof(filterBtn)); filterBtn = this.FindControl<Button>(nameof(filterBtn));
toggleQueueHideBtn = this.FindControl<Button>(nameof(toggleQueueHideBtn)); toggleQueueHideBtn = this.FindControl<Button>(nameof(toggleQueueHideBtn));
removeBooksBtn = this.FindControl<Button>(nameof(removeBooksBtn));
doneRemovingBtn = this.FindControl<Button>(nameof(doneRemovingBtn));
splitContainer1 = this.FindControl<SplitView>(nameof(splitContainer1)); splitContainer1 = this.FindControl<SplitView>(nameof(splitContainer1));
productsDisplay = this.FindControl<ProductsDisplay2>(nameof(productsDisplay)); productsDisplay = this.FindControl<ProductsDisplay2>(nameof(productsDisplay));
processBookQueue1 = this.FindControl<ProcessQueueControl2>(nameof(processBookQueue1)); processBookQueue1 = this.FindControl<ProcessQueueControl2>(nameof(processBookQueue1));

View File

@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer; using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels; using LibationWinForms.AvaloniaUI.ViewModels;
using System; using System;
@ -6,6 +7,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{ {
@ -13,7 +15,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{ {
private void Configure_Display() { } private void Configure_Display() { }
public void Display(List<LibraryBook> dbBooks) public async Task Display(List<LibraryBook> dbBooks)
{ {
try try
{ {
@ -50,7 +52,20 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
//List is already displayed. Replace all items with new ones, refilter, and re-sort //List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = _viewModel?.GridEntries?.Filter; string existingFilter = _viewModel?.GridEntries?.Filter;
var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks); var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks);
bindingList.ReplaceList(newEntries);
var existingSeriesEntries = bindingList.InternalList.SeriesEntries().ToList();
await Dispatcher.UIThread.InvokeAsync(() => bindingList.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)
{
var sEntry = bindingList.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntrys2 se && !series.Liberate.Expanded)
await Dispatcher.UIThread.InvokeAsync(() => bindingList.CollapseItem(se));
}
bindingList.Filter = existingFilter; bindingList.Filter = existingFilter;
ReSort(); ReSort();
} }

View File

@ -30,8 +30,14 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
removeGVColumn.IsVisible = value; removeGVColumn.IsVisible = value;
} }
} }
public void CloseRemoveBooksColumn() public void CloseRemoveBooksColumn()
=> RemoveColumnVisible = false; {
RemoveColumnVisible = false;
foreach (var item in bindingList.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
}
public async Task RemoveCheckedBooksAsync() public async Task RemoveCheckedBooksAsync()
{ {
@ -49,11 +55,34 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
if (result != System.Windows.Forms.DialogResult.Yes) if (result != System.Windows.Forms.DialogResult.Yes)
return; return;
RemoveBooks(selectedBooks); foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList(); var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
bindingList.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); var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true)); foreach (var b in GetAllBookEntries())
b.Remove = false;
RemovableCountChanged?.Invoke(this, 0);
}
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
return;
//After ProductsDisplay2.Display() re-creates the list,
//re-subscribe to all items' PropertyChanged events.
foreach (var b in GetAllBookEntries())
b.PropertyChanged += Item_PropertyChanged;
bindingList.CollectionChanged -= BindingList_CollectionChanged;
} }
public async Task ScanAndRemoveBooksAsync(params Account[] accounts) public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
@ -83,6 +112,9 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
r.Remove = true; r.Remove = true;
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true)); RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
foreach (var item in bindingList.AllItems())
item.PropertyChanged += Item_PropertyChanged;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -94,30 +126,13 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
} }
} }
private void RemoveBooks(IEnumerable<LibraryBookEntry2> removedBooks) private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
//Remove books in series from their parents' Children list if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{ {
removed.Parent.Children.Remove(removed); int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
//In Avalonia, if you fire PropertyChanged with an empty or invalid property name, nothing is updated.
//So we must notify for specific properties that we believed changed.
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.Length));
removed.Parent.RaisePropertyChanged(nameof(SeriesEntrys2.PurchaseDate));
} }
//Remove series that have no children
var removedSeries =
bindingList
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry2>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
} }
} }
} }

View File

@ -15,7 +15,10 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{ {
if (CurrentSortColumn is null) if (CurrentSortColumn is null)
{ {
bindingList.InternalList.Sort((i1, i2) => i2.DateAdded.CompareTo(i1.DateAdded)); //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(GridEntry2.DateAdded));
bindingList.InternalList.Sort(defaultComparer);
bindingList.InternalList.Reverse();
bindingList.ResetCollection(); bindingList.ResetCollection();
} }
else else