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.Text;
using System.Threading.Tasks;
using ReactiveUI;
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
/// properties when 2 items compare equal.
/// </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 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);
//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
@ -102,5 +102,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
else
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)
{
Liberate = new LiberateButtonStatus2(IsSeries);
Liberate = new LiberateButtonStatus2(IsSeries) { Expanded = true };
SeriesIndex = -1;
LibraryBook = parent;

View File

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

View File

@ -9,7 +9,7 @@
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700"
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
Title="MainWindow"
Title="Libation"
Name="Form1"
Icon="/AvaloniaUI/Assets/glass-with-glow_16.png">
@ -128,8 +128,8 @@
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button Name="removeBooksBtn" Click="removeBooksBtn_Click" Height="30" Width="220" Content="Remove # Books from Libation"/>
<Button Name="doneRemovingBtn" Click="doneRemovingBtn_Click" Height="30" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="removeBooksBtn_Click" Height="30" Width="220" Content="{Binding RemoveBooksButtonText}"/>
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Width="160" Margin="10,0,0,0" Content="Done Removing Books"/>
</StackPanel>
<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<List<LibraryBook>> LibraryLoaded;
private MainWindowViewModel _viewModel;
public MainWindow()
{
@ -26,6 +27,8 @@ namespace LibationWinForms.AvaloniaUI.Views
#endif
this.FindAllControls();
this.DataContext = _viewModel = new MainWindowViewModel();
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
Configure_BackupCounts();
Configure_ScanAuto();
@ -43,8 +46,8 @@ namespace LibationWinForms.AvaloniaUI.Views
Configure_NonUI();
{
this.LibraryLoaded += (_, dbBooks) => productsDisplay.Display(dbBooks);
LibraryCommands.LibrarySizeChanged += (_, _) => productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
this.LibraryLoaded += async (_, dbBooks) => await productsDisplay.Display(dbBooks);
LibraryCommands.LibrarySizeChanged += async (_, _) => await productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
}
}
@ -107,9 +110,6 @@ namespace LibationWinForms.AvaloniaUI.Views
filterBtn = this.FindControl<Button>(nameof(filterBtn));
toggleQueueHideBtn = this.FindControl<Button>(nameof(toggleQueueHideBtn));
removeBooksBtn = this.FindControl<Button>(nameof(removeBooksBtn));
doneRemovingBtn = this.FindControl<Button>(nameof(doneRemovingBtn));
splitContainer1 = this.FindControl<SplitView>(nameof(splitContainer1));
productsDisplay = this.FindControl<ProductsDisplay2>(nameof(productsDisplay));
processBookQueue1 = this.FindControl<ProcessQueueControl2>(nameof(processBookQueue1));

View File

@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
@ -6,6 +7,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
@ -13,7 +15,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
private void Configure_Display() { }
public void Display(List<LibraryBook> dbBooks)
public async Task Display(List<LibraryBook> dbBooks)
{
try
{
@ -50,7 +52,20 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
//List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = _viewModel?.GridEntries?.Filter;
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;
ReSort();
}

View File

@ -30,8 +30,14 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
removeGVColumn.IsVisible = value;
}
}
public void CloseRemoveBooksColumn()
=> RemoveColumnVisible = false;
{
RemoveColumnVisible = false;
foreach (var item in bindingList.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
}
public async Task RemoveCheckedBooksAsync()
{
@ -49,11 +55,34 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
if (result != System.Windows.Forms.DialogResult.Yes)
return;
RemoveBooks(selectedBooks);
foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged;
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);
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)
@ -83,6 +112,9 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
r.Remove = true;
RemovableCountChanged?.Invoke(this, GetAllBookEntries().Count(lbe => lbe.Remove is true));
foreach (var item in bindingList.AllItems())
item.PropertyChanged += Item_PropertyChanged;
}
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
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
{
removed.Parent.Children.Remove(removed);
//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());
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
}
}
}
}

View File

@ -15,7 +15,10 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
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();
}
else