Performance improvements and better mvvp pattern following
This commit is contained in:
parent
c2a2e51bde
commit
8cd6219bd9
BIN
Source/LibationWinForms/AvaloniaUI/Assets/libation.ico
Normal file
BIN
Source/LibationWinForms/AvaloniaUI/Assets/libation.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views
|
||||
|
||||
try
|
||||
{
|
||||
productsDisplay.Filter(filterString);
|
||||
await _viewModel.ProductsDisplay.Filter(filterString);
|
||||
lastGoodFilter = filterString;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user