Performance improvements and better mvvp pattern following

This commit is contained in:
Michael Bucari-Tovo 2022-07-15 15:16:27 -06:00
parent c2a2e51bde
commit 8cd6219bd9
19 changed files with 358 additions and 414 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -9,16 +9,19 @@ using System.Linq;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
/*
* Allows filtering and sorting of the underlying BindingList<GridEntry>
* by implementing IBindingListView and using SearchEngineCommands
* 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.
*
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved 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 GridEntryBindingList2 : ObservableCollection<GridEntry2>
{
@ -42,31 +45,19 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
#region Items Management
public new void Remove(GridEntry2 entry)
{
FilterRemoved.Add(entry);
base.Remove(entry);
}
public void ReplaceList(IEnumerable<GridEntry2> newItems)
{
Items.Clear();
((List<GridEntry2>)Items).AddRange(newItems);
ResetCollection();
}
protected override void InsertItem(int index, GridEntry2 item)
{
FilterRemoved.Remove(item);
base.InsertItem(index, item);
}
public void ResetCollection()
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion
#region Filtering
public void ResetCollection()
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
private void ApplyFilter(string filterString)
{
@ -85,8 +76,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
foreach (var item in filteredOut)
{
Remove(item);
FilterRemoved.Add(item);
Items.Remove(item);
}
ResetCollection();
}
public void RemoveFilter()
@ -99,12 +92,15 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
{
if (item is SeriesEntrys2 || item is LibraryBookEntry2 lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
{
InsertItem(visibleCount++, item);
FilterRemoved.Remove(item);
Items.Insert(visibleCount++, item);
}
}
FilterString = null;
SearchResults = null;
ResetCollection();
}
#endregion
@ -128,10 +124,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
/*
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
* fired. When adding 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)
* 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.
@ -154,10 +150,10 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
/*
* Bypass ObservationCollection's InsertItem methos so that CollectionChanged isn't
* fired. When adding 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)
* 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.

View File

@ -22,6 +22,7 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
/// <summary> The Process Queue's viewmodel </summary>
public ProcessQueueViewModel ProcessQueueViewModel { get; } = new ProcessQueueViewModel();
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
/// <summary> Library filterting query </summary>

View File

@ -1,31 +1,128 @@
using Avalonia.Collections;
using Avalonia.Controls;
using DataLayer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using System.Reflection;
using System.Collections;
using Avalonia.Threading;
using ApplicationServices;
using AudibleUtilities;
namespace LibationWinForms.AvaloniaUI.ViewModels
{
public class ProductsDisplayViewModel : ViewModelBase
{
public GridEntryBindingList2 GridEntries { get; set; }
public DataGridCollectionView GridCollectionView { get; set; }
public ProductsDisplayViewModel(IEnumerable<LibraryBook> dbBooks)
{
GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
GridEntries.CollapseAll();
/*
* Would be nice to use built-in groups, but Avalonia doesn't yet let you customize the row group header.
*
GridCollectionView = new DataGridCollectionView(GridEntries);
GridCollectionView.GroupDescriptions.Add(new CustonGroupDescription());
*/
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler InitialLoaded;
private DataGridColumn _currentSortColumn;
private GridEntryBindingList2 _gridEntries;
private bool _removeColumnVisivle;
public GridEntryBindingList2 GridEntries { get => _gridEntries; private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); }
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
public List<LibraryBook> GetVisibleBookEntries()
=> GridEntries
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
public IEnumerable<LibraryBookEntry2> GetAllBookEntries()
=> GridEntries
.AllItems()
.BookEntries();
public ProductsDisplayViewModel()
{
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
_gridEntries = new GridEntryBindingList2(CreateGridEntries(new List<LibraryBook> { book }));
return;
}
}
public static IEnumerable<GridEntry2> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
public void InitialDisplay(List<LibraryBook> dbBooks, Views.ProductsGrid.ProductsDisplay2 productsGrid)
{
try
{
GridEntries = new GridEntryBindingList2(CreateGridEntries(dbBooks));
GridEntries.CollapseAll();
int bookEntryCount = GridEntries.BookEntries().Count();
InitialLoaded?.Invoke(this, EventArgs.Empty);
VisibleCountChanged?.Invoke(this, bookEntryCount);
//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) =>
{
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsGrid.productsGrid))).Cast<GridEntry2>();
int index = 0;
foreach (var di in displayListGE)
{
di.ListIndex = index++;
}
};
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
}
}
public async Task DisplayBooks(List<LibraryBook> dbBooks)
{
try
{
//List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = GridEntries?.Filter;
var newEntries = CreateGridEntries(dbBooks);
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList();
await Dispatcher.UIThread.InvokeAsync(() => 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)
{
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntrys2 se && !series.Liberate.Expanded)
await Dispatcher.UIThread.InvokeAsync(() => GridEntries.CollapseItem(se));
}
await Dispatcher.UIThread.InvokeAsync(() =>
{
GridEntries.Filter = existingFilter;
ReSort();
});
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
}
}
private static IEnumerable<GridEntry2> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
.Where(lb => lb.Book.IsProduct())
@ -50,20 +147,169 @@ namespace LibationWinForms.AvaloniaUI.ViewModels
}
return geList.OrderByDescending(e => e.DateAdded);
}
}
class CustonGroupDescription : DataGridGroupDescription
{
public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
public async Task Filter(string searchString)
{
if (item is SeriesEntrys2 sEntry)
return sEntry;
else if (item is LibraryBookEntry2 lbEntry && lbEntry.Parent is SeriesEntrys2 sEntry2)
return sEntry2;
else return null;
await Dispatcher.UIThread.InvokeAsync(() =>
{
int visibleCount = GridEntries.Count;
if (string.IsNullOrEmpty(searchString))
GridEntries.RemoveFilter();
else
GridEntries.Filter = searchString;
if (visibleCount != GridEntries.Count)
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
//Re-sort after filtering
ReSort();
});
}
public override bool KeysMatch(object groupKey, object itemKey)
public void ToggleSeriesExpanded(SeriesEntrys2 seriesEntry)
{
return base.KeysMatch(groupKey, itemKey);
if (seriesEntry.Liberate.Expanded)
GridEntries.CollapseItem(seriesEntry);
else
GridEntries.ExpandItem(seriesEntry);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
}
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(GridEntry2.DateAdded));
GridEntries.InternalList.Sort(defaultComparer);
GridEntries.InternalList.Reverse();
GridEntries.ResetCollection();
}
else
{
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
}
public void DoneRemovingBooks()
{
foreach (var item in GridEntries.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
RemoveColumnVisivle = false;
}
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = await MessageBox.ShowConfirmationDialog(
null,
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged;
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)
{
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;
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
}
public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{
foreach (var item in GridEntries.AllItems())
{
item.Remove = false;
item.PropertyChanged += Item_PropertyChanged;
}
RemoveColumnVisivle = true;
RemovableCountChanged?.Invoke(this, 0);
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = true;
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
{
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
}
}
}
}

View File

@ -35,7 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views
try
{
productsDisplay.Filter(filterString);
await _viewModel.ProductsDisplay.Filter(filterString);
lastGoodFilter = filterString;
}
catch (Exception ex)

View File

@ -61,7 +61,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => new EditQuickFilters().ShowDialog();
public async void productsDisplay_Initialized(object sender, EventArgs e)
public async void ProductsDisplay_Initialized(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
await performFilter(QuickFilters.Filters.FirstOrDefault());

View File

@ -59,18 +59,18 @@ namespace LibationWinForms.AvaloniaUI.Views
//For removing books within a filter set, use
//Visible Books > Remove from library
productsDisplay.Filter(null);
await _viewModel.ProductsDisplay.Filter(null);
_viewModel.RemoveBooksButtonEnabled = true;
_viewModel.RemoveButtonsVisible = true;
await productsDisplay.ScanAndRemoveBooksAsync(accounts);
await _viewModel.ProductsDisplay.ScanAndRemoveBooksAsync(accounts);
}
public async void removeBooksBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_viewModel.RemoveBooksButtonEnabled = false;
await productsDisplay.RemoveCheckedBooksAsync();
await _viewModel.ProductsDisplay.RemoveCheckedBooksAsync();
_viewModel.RemoveBooksButtonEnabled = true;
}
@ -78,13 +78,13 @@ namespace LibationWinForms.AvaloniaUI.Views
{
_viewModel.RemoveButtonsVisible = false;
productsDisplay.CloseRemoveBooksColumn();
_viewModel.ProductsDisplay.DoneRemovingBooks();
//Restore the filter
await performFilter(lastGoodFilter);
}
public void productsDisplay_RemovableCountChanged(object sender, int removeCount)
public void ProductsDisplay_RemovableCountChanged(object sender, int removeCount)
{
_viewModel.RemoveBooksButtonText = removeCount switch
{

View File

@ -28,7 +28,8 @@ namespace LibationWinForms.AvaloniaUI.Views
Serilog.Log.Logger.Information("Begin backing up visible library books");
_viewModel.ProcessQueueViewModel.AddDownloadDecrypt(
productsDisplay
_viewModel
.ProductsDisplay
.GetVisibleBookEntries()
.UnLiberated()
);
@ -45,7 +46,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
@ -68,7 +69,7 @@ namespace LibationWinForms.AvaloniaUI.Views
if (result != System.Windows.Forms.DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
@ -86,7 +87,7 @@ namespace LibationWinForms.AvaloniaUI.Views
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var visibleLibraryBooks = productsDisplay.GetVisibleBookEntries();
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
@ -100,7 +101,7 @@ namespace LibationWinForms.AvaloniaUI.Views
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
await LibraryCommands.RemoveBooksAsync(visibleIds);
}
public async void productsDisplay_VisibleCountChanged(object sender, int qty)
public async void ProductsDisplay_VisibleCountChanged(object sender, int qty)
{
_viewModel.VisibleCount = qty;
@ -108,7 +109,7 @@ namespace LibationWinForms.AvaloniaUI.Views
}
void setLiberatedVisibleMenuItem()
=> _viewModel.VisibleNotLiberated
= productsDisplay
= _viewModel.ProductsDisplay
.GetVisibleBookEntries()
.Count(lb => lb.Book.UserDefinedItem.BookStatus == LiberatedStatus.NotLiberated);
}

View File

@ -11,7 +11,7 @@
x:Class="LibationWinForms.AvaloniaUI.Views.MainWindow"
Title="Libation"
Name="Form1"
Icon="/AvaloniaUI/Assets/glass-with-glow_16.png">
Icon="/AvaloniaUI/Assets/libation.ico">
<Border BorderBrush="{DynamicResource DataGridGridLinesBrush}" BorderThickness="2" Padding="15">
<Grid RowDefinitions="Auto,Auto,*,Auto">
@ -173,10 +173,8 @@
<!-- Product Display Grid -->
<prgid:ProductsDisplay2
InitialLoaded="productsDisplay_Initialized"
DataContext="{Binding ProductsDisplay}"
LiberateClicked="ProductsDisplay_LiberateClicked"
RemovableCountChanged="productsDisplay_RemovableCountChanged"
VisibleCountChanged="productsDisplay_VisibleCountChanged"
Name="productsDisplay" />
</SplitView>
</Border>

View File

@ -2,7 +2,6 @@ using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationWinForms.AvaloniaUI.Controls;
using System;
using LibationWinForms.AvaloniaUI.Views.ProductsGrid;
using Avalonia.ReactiveUI;
@ -10,6 +9,7 @@ using LibationWinForms.AvaloniaUI.ViewModels;
using LibationFileManager;
using DataLayer;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views
{
@ -45,13 +45,26 @@ namespace LibationWinForms.AvaloniaUI.Views
// misc which belongs in winforms app but doesn't have a UI element
Configure_NonUI();
_viewModel.ProductsDisplay.InitialLoaded += ProductsDisplay_Initialized;
_viewModel.ProductsDisplay.RemovableCountChanged += ProductsDisplay_RemovableCountChanged;
_viewModel.ProductsDisplay.VisibleCountChanged += ProductsDisplay_VisibleCountChanged;
{
this.LibraryLoaded += async (_, dbBooks) => await productsDisplay.Display(dbBooks);
LibraryCommands.LibrarySizeChanged += async (_, _) => await productsDisplay.Display(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
this.LibraryLoaded += MainWindow_LibraryLoaded;
LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.DisplayBooks(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
}
}
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{
if (Design.IsDesignMode)
return;
_viewModel.ProductsDisplay.InitialDisplay(dbBooks, productsDisplay);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);

View File

@ -20,16 +20,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
if (button.DataContext is SeriesEntrys2 sEntry)
{
if (sEntry.Liberate.Expanded)
{
bindingList.CollapseItem(sEntry);
}
else
{
bindingList.ExpandItem(sEntry);
}
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
_viewModel.ToggleSeriesExpanded(sEntry);
//Expanding and collapsing reset the list, which will cause focus to shift
//to the topright cell. Reset focus onto the clicked button's cell.

View File

@ -8,7 +8,7 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
ContextMenu contextMenuStrip1 = new ContextMenu();
private ContextMenu contextMenuStrip1 = new ContextMenu();
private void Configure_ColumnCustomization()
{
if (Design.IsDesignMode) return;
@ -68,10 +68,6 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, productsGrid.Columns.IndexOf(column));
}
//Remove column is always first;
removeGVColumn.DisplayIndex = 0;
removeGVColumn.CanUserReorder = false;
}
private void ContextMenu_ContextMenuOpening(object sender, System.ComponentModel.CancelEventArgs e)

View File

@ -1,81 +0,0 @@
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_Display() { }
public async Task Display(List<LibraryBook> dbBooks)
{
try
{
if (_viewModel is null)
{
_viewModel = new ProductsDisplayViewModel(dbBooks);
InitialLoaded?.Invoke(this, EventArgs.Empty);
int bookEntryCount = bindingList.BookEntries().Count();
VisibleCountChanged?.Invoke(this, bookEntryCount);
//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);
bindingList.CollectionChanged += (s, e) =>
{
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsGrid))).Cast<GridEntry2>();
int index = 0;
foreach (var di in displayListGE)
{
di.ListIndex = index++;
}
};
//Assign the viewmodel after we subscribe to CollectionChanged
//so that out handler executes first.
productsGrid.DataContext = _viewModel;
}
else
{
//List is already displayed. Replace all items with new ones, refilter, and re-sort
string existingFilter = _viewModel?.GridEntries?.Filter;
var newEntries = ProductsDisplayViewModel.CreateGridEntries(dbBooks);
var existingSeriesEntries = bindingList.AllItems().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));
}
await Dispatcher.UIThread.InvokeAsync(() =>
{
bindingList.Filter = existingFilter;
ReSort();
});
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplay2));
}
}
}
}

View File

@ -1,27 +0,0 @@
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_Filtering() { }
public void Filter(string searchString)
{
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
bindingList.RemoveFilter();
else
bindingList.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.BookEntries().Count());
//Re-sort after filtering
ReSort();
}
}
}

View File

@ -1,137 +0,0 @@
using ApplicationServices;
using AudibleUtilities;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private void Configure_ScanAndRemove() { }
private bool RemoveColumnVisible
{
get => removeGVColumn.IsVisible;
set
{
if (value)
{
foreach (var book in bindingList.AllItems())
book.Remove = false;
}
removeGVColumn.DisplayIndex = 0;
removeGVColumn.CanUserReorder = value;
removeGVColumn.IsVisible = value;
}
}
public void CloseRemoveBooksColumn()
{
RemoveColumnVisible = false;
foreach (var item in bindingList.AllItems())
item.PropertyChanged -= Item_PropertyChanged;
}
public async Task RemoveCheckedBooksAsync()
{
var selectedBooks = GetAllBookEntries().Where(lbe => lbe.Remove == true).ToList();
if (selectedBooks.Count == 0)
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = await MessageBox.ShowConfirmationDialog(
null,
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
"Remove books from Libation?");
if (result != DialogResult.Yes)
return;
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);
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)
{
RemovableCountChanged?.Invoke(this, 0);
removeGVColumn.IsVisible = true;
foreach (var item in bindingList.AllItems())
item.PropertyChanged += Item_PropertyChanged;
try
{
if (accounts is null || accounts.Length == 0)
return;
var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks
.Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated());
var removedBooks = await LibraryCommands.FindInactiveBooks(Login.WinformLoginChoiceEager.ApiExtendedFunc, lib, accounts);
var removable = allBooks.Where(lbe => removedBooks.Any(rb => rb.Book.AudibleProductId == lbe.AudibleProductId)).ToList();
foreach (var r in removable)
r.Remove = true;
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",
ex);
}
}
private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(GridEntry2.Remove) && sender is LibraryBookEntry2 lbEntry)
{
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount);
}
}
}
}

View File

@ -1,41 +0,0 @@
using Avalonia.Controls;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2
{
private DataGridColumn CurrentSortColumn;
private void Configure_Sorting() { }
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(GridEntry2.DateAdded));
bindingList.InternalList.Sort(defaultComparer);
bindingList.InternalList.Reverse();
bindingList.ResetCollection();
}
else
{
CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
}
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{
//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 = e.Column.CustomSortComparer as RowComparer;
comparer.SortDirection = null;
CurrentSortColumn = e.Column;
}
}
}

View File

@ -13,11 +13,21 @@
Name="productsGrid"
AutoGenerateColumns="False"
Items="{Binding GridEntries}"
Sorting="ProductsGrid_Sorting"
CanUserSortColumns="True"
CanUserReorderColumns="True">
<DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt IsVisible="True" Header="Remove" IsThreeState="True" IsReadOnly="False" CanUserSort="True" Binding="{Binding Remove, Mode=TwoWay}" Width="70" SortMemberPath="Remove" />
<controls:DataGridCheckBoxColumnExt
PropertyChanged="RemoveColumn_PropertyChanged"
IsVisible="{Binding RemoveColumnVisivle}"
Header="Remove"
IsThreeState="True"
IsReadOnly="False"
CanUserSort="True"
Binding="{Binding Remove, Mode=TwoWay}"
Width="70" SortMemberPath="Remove" />
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
<DataGridTemplateColumn.CellTemplate>

View File

@ -1,38 +1,17 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using DataLayer;
using LibationWinForms.AvaloniaUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
{
public partial class ProductsDisplay2 : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler InitialLoaded;
public List<LibraryBook> GetVisibleBookEntries()
=> bindingList
.BookEntries()
.Select(lbe => lbe.LibraryBook)
.ToList();
private IEnumerable<LibraryBookEntry2> GetAllBookEntries()
=> bindingList
.AllItems()
.BookEntries();
private ProductsDisplayViewModel _viewModel;
private GridEntryBindingList2 bindingList => _viewModel.GridEntries;
DataGridColumn removeGVColumn;
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
public ProductsDisplay2()
{
@ -40,34 +19,32 @@ namespace LibationWinForms.AvaloniaUI.Views.ProductsGrid
Configure_Buttons();
Configure_ColumnCustomization();
Configure_Display();
Configure_Filtering();
Configure_ScanAndRemove();
Configure_Sorting();
foreach ( var column in productsGrid.Columns)
foreach (var column in productsGrid.Columns)
{
column.CustomSortComparer = new RowComparer(column);
}
if (Design.IsDesignMode)
{
using var context = DbContexts.GetContext();
var book = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
productsGrid.DataContext = _viewModel = new ProductsDisplayViewModel(new List<LibraryBook> { book });
return;
}
}
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{
_viewModel.Sort(e.Column);
}
private void RemoveColumn_PropertyChanged(object sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
{
if (sender is DataGridColumn col && e.Property.Name == nameof(DataGridColumn.IsVisible))
{
col.DisplayIndex = 0;
col.CanUserReorder = false;
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
productsGrid = this.FindControl<DataGrid>(nameof(productsGrid));
productsGrid.Sorting += ProductsGrid_Sorting;
productsGrid.CanUserSortColumns = true;
removeGVColumn = productsGrid.Columns[0];
}
}
}

View File

@ -52,6 +52,7 @@
<None Remove="AvaloniaUI\Assets\glass-with-glow_16.png" />
<None Remove="AvaloniaUI\Assets\import_16x16.png" />
<None Remove="AvaloniaUI\Assets\last.png" />
<None Remove="AvaloniaUI\Assets\libation.ico" />
<None Remove="AvaloniaUI\Assets\LibationStyles.xaml" />
<None Remove="AvaloniaUI\Assets\MBIcons\Asterisk.png" />
<None Remove="AvaloniaUI\Assets\MBIcons\error.png" />