Refactor ProductsDisplay

This commit is contained in:
Michael Bucari-Tovo 2022-12-09 12:27:52 -07:00
parent c01e1c3e4b
commit dfedb23efd
9 changed files with 151 additions and 386 deletions

View File

@ -6,7 +6,7 @@
mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110" mc:Ignorable="d" d:DesignWidth="265" d:DesignHeight="110"
MinWidth="265" MinHeight="110" MinWidth="265" MinHeight="110"
x:Class="LibationAvalonia.Dialogs.MessageBoxWindow" x:Class="LibationAvalonia.Dialogs.MessageBoxWindow"
Title="{Binding Caption}" IsExtendedIntoWindowDecorations="True" ShowInTaskbar="True" Title="{Binding Caption}" ShowInTaskbar="True"
Icon="/Assets/1x1.png"> Icon="/Assets/1x1.png">
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto"> <Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
@ -34,13 +34,13 @@
</Style> </Style>
</DockPanel.Styles> </DockPanel.Styles>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" DockPanel.Dock="Bottom">
<Button Grid.Column="0" MinWidth="75" MinHeight="25" Name="Button1" Click="Button1_Click" Margin="5"> <Button Grid.Column="0" MinWidth="75" MinHeight="28" Name="Button1" Click="Button1_Click" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button1Text}"/>
</Button> </Button>
<Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="25" Name="Button2" Click="Button2_Click" Margin="5"> <Button Grid.Column="1" IsVisible="{Binding HasButton2}" MinWidth="75" MinHeight="28" Name="Button2" Click="Button2_Click" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button2Text}"/>
</Button> </Button>
<Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="25" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5"> <Button Grid.Column="2" IsVisible="{Binding HasButton3}" MinWidth="75" MinHeight="28" Name="Button3" Click="Button3_Click" Content="Cancel" Margin="5">
<TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/> <TextBlock VerticalAlignment="Center" Text="{Binding Button3Text}"/>
</Button> </Button>
</StackPanel> </StackPanel>

View File

@ -136,7 +136,8 @@
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " /> <PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.4.2" /> <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.0-preview4" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup> </ItemGroup>

View File

@ -1,177 +0,0 @@
using ApplicationServices;
using LibationSearchEngine;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
namespace LibationAvalonia.ViewModels
{
/*
* Allows filtering of the underlying ObservableCollection<GridEntry>
*
* When filtering is applied, the filtered-out items are removed
* from the base list and added to the private FilterRemoved list.
* When filtering is removed, items in the FilterRemoved list are
* added back to the base list.
*
* Items are added and removed to/from the ObservableCollection's
* internal list instead of the ObservableCollection itself to
* avoid ObservableCollection firing CollectionChanged for every
* item. Editing the list this way improve's display performance,
* but requires ResetCollection() to be called after all changes
* have been made.
*/
public class GridEntryCollection : ObservableCollection<GridEntry>
{
public GridEntryCollection(IEnumerable<GridEntry> enumeration)
: base(new List<GridEntry>(enumeration)) { }
public GridEntryCollection(List<GridEntry> list)
: base(list) { }
public List<GridEntry> InternalList => Items as List<GridEntry>;
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
public bool SuspendFilteringOnUpdate { get; set; }
public string Filter { get => FilterString; set => ApplyFilter(value); }
/// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<GridEntry> FilterRemoved = new();
private string FilterString;
private SearchResultSet SearchResults;
#region Items Management
/// <summary>
/// Removes all items from the collection, both visible and hidden, adds new items to the visible collection.
/// </summary>
public void ReplaceList(IEnumerable<GridEntry> newItems)
{
Items.Clear();
FilterRemoved.Clear();
((List<GridEntry>)Items).AddRange(newItems);
ResetCollection();
}
public void ResetCollection()
=> OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion
#region Filtering
private void ApplyFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
var booksFilteredIn = Items.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = Items.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
foreach (var item in filteredOut)
{
FilterRemoved.Add(item);
Items.Remove(item);
}
ResetCollection();
}
public void RemoveFilter()
{
if (FilterString is null) return;
int visibleCount = Items.Count;
foreach (var item in FilterRemoved.ToList())
{
if (item is SeriesEntry || item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded))
{
FilterRemoved.Remove(item);
Items.Insert(visibleCount++, item);
}
}
FilterString = null;
SearchResults = null;
ResetCollection();
}
#endregion
#region Expand/Collapse
public void CollapseAll()
{
foreach (var series in Items.SeriesEntries().ToList())
CollapseItem(series);
}
public void ExpandAll()
{
foreach (var series in Items.SeriesEntries().ToList())
ExpandItem(series);
}
public void CollapseItem(SeriesEntry sEntry)
{
foreach (var episode in Items.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
/*
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
* event handler causes serious performance problems. And unfotrunately, Avalonia
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Add(episode);
Items.Remove(episode);
}
sEntry.Liberate.Expanded = false;
ResetCollection();
}
public void ExpandItem(SeriesEntry sEntry)
{
var sindex = Items.IndexOf(sEntry);
foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).OrderByDescending(lbe => lbe.SeriesIndex).ToList())
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
/*
* Bypass ObservationCollection's InsertItem method so that CollectionChanged isn't
* fired. When adding or removing many items at once, Avalonia's CollectionChanged
* event handler causes serious performance problems. And unfotrunately, Avalonia
* doesn't respect the NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList? changedItems)
* overload that would fire only once for all changed items.
*
* Doing this requires resetting the list so the view knows it needs to rebuild its display.
*/
FilterRemoved.Remove(episode);
Items.Insert(++sindex, episode);
}
}
sEntry.Liberate.Expanded = true;
ResetCollection();
}
#endregion
}
}

View File

@ -1,4 +1,3 @@
using Avalonia.Controls;
using DataLayer; using DataLayer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -6,12 +5,9 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using System.Reflection;
using System.Collections;
using Avalonia.Threading; using Avalonia.Threading;
using ApplicationServices; using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using LibationAvalonia.Views;
using LibationAvalonia.Dialogs.Login; using LibationAvalonia.Dialogs.Login;
using Avalonia.Collections; using Avalonia.Collections;
@ -24,81 +20,35 @@ namespace LibationAvalonia.ViewModels
public event EventHandler<int> RemovableCountChanged; public event EventHandler<int> RemovableCountChanged;
public event EventHandler InitialLoaded; public event EventHandler InitialLoaded;
private DataGridColumn _currentSortColumn; /// <summary>Backing list of all grid entries</summary>
private DataGrid productsDataGrid; private readonly List<GridEntry> SOURCE = new();
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private List<GridEntry> FilteredInGridEntries;
public string FilterString { get; private set; }
public DataGridCollectionView GridEntries { get; }
private GridEntryCollection _gridEntries;
private bool _removeColumnVisivle; private bool _removeColumnVisivle;
public GridEntryCollection GridEntries { get => _gridEntries; private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); }
public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); } public bool RemoveColumnVisivle { get => _removeColumnVisivle; private set => this.RaiseAndSetIfChanged(ref _removeColumnVisivle, value); }
public List<LibraryBook> GetVisibleBookEntries() public List<LibraryBook> GetVisibleBookEntries()
=> GridEntries.InternalList => GridEntries
.Cast<GridEntry>()
.BookEntries() .BookEntries()
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.ToList(); .ToList();
public IEnumerable<LibraryBookEntry> GetAllBookEntries()
=> GridEntries private IEnumerable<LibraryBookEntry> GetAllBookEntries()
.AllItems() => SOURCE
.BookEntries(); .BookEntries();
public ProductsDisplayViewModel() { }
public ProductsDisplayViewModel(List<GridEntry> items) public ProductsDisplayViewModel()
{ {
GridEntries = new GridEntryCollection(items); GridEntries = new(SOURCE);
GridEntries.Filter = CollectionFilter;
} }
#region Display Functions #region Display Functions
/// <summary>
/// Call once on load so we can modify access a private member with reflection
/// </summary>
public void RegisterCollectionChanged(ProductsDisplay productsDisplay = null)
{
productsDataGrid ??= productsDisplay?.productsGrid;
if (GridEntries is null)
return;
//Avalonia displays items in the DataConncetion from an internal copy of
//the bound list, not the actual bound list. So we need to reflect to get
//the current display order and set each GridEntry.ListIndex correctly.
var DataConnection_PI = typeof(DataGrid).GetProperty("DataConnection", BindingFlags.NonPublic | BindingFlags.Instance);
var DataSource_PI = DataConnection_PI.PropertyType.GetProperty("DataSource", BindingFlags.Public | BindingFlags.Instance);
GridEntries.CollectionChanged += (s, e) =>
{
if (s != GridEntries) return;
var displayListGE = ((IEnumerable)DataSource_PI.GetValue(DataConnection_PI.GetValue(productsDataGrid))).Cast<GridEntry>();
int index = 0;
foreach (var di in displayListGE)
{
di.ListIndex = index++;
}
};
}
/// <summary>
/// Only call once per lifetime
/// </summary>
public void InitialDisplay(List<LibraryBook> dbBooks)
{
try
{
GridEntries = new GridEntryCollection(CreateGridEntries(dbBooks));
GridEntries.CollapseAll();
InitialLoaded?.Invoke(this, EventArgs.Empty);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
RegisterCollectionChanged();
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "Error displaying library in {0}", nameof(ProductsDisplayViewModel));
}
}
/// <summary> /// <summary>
/// Call when there's been a change to the library /// Call when there's been a change to the library
/// </summary> /// </summary>
@ -106,29 +56,25 @@ namespace LibationAvalonia.ViewModels
{ {
try try
{ {
//List is already displayed. Replace all items with new ones, refilter, and re-sort var existingSeriesEntries = SOURCE.SeriesEntries().ToList();
string existingFilter = GridEntries?.Filter;
var newEntries = CreateGridEntries(dbBooks);
var existingSeriesEntries = GridEntries.AllItems().SeriesEntries().ToList(); SOURCE.Clear();
SOURCE.AddRange(CreateGridEntries(dbBooks));
await Dispatcher.UIThread.InvokeAsync(() => //If replacing the list, preserve user's existing collapse/expand
//state. When resetting a list, default state is cosed.
foreach (var series in existingSeriesEntries)
{ {
GridEntries.ReplaceList(newEntries); var sEntry = SOURCE.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntry se)
se.Liberate.Expanded = series.Liberate.Expanded;
}
//We're replacing the list, so preserve usere's existing collapse/expand //Run query on new list
//state. When resetting a list, default state is open. FilteredInGridEntries = QueryResults(SOURCE, FilterString);
foreach (var series in existingSeriesEntries)
{ await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
var sEntry = GridEntries.InternalList.FirstOrDefault(ge => ge.AudibleProductId == series.AudibleProductId);
if (sEntry is SeriesEntry se && !series.Liberate.Expanded)
GridEntries.CollapseItem(se);
}
GridEntries.Filter = existingFilter;
ReSort();
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
});
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -136,7 +82,7 @@ namespace LibationAvalonia.ViewModels
} }
} }
private static IEnumerable<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks) private static List<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{ {
var geList = dbBooks var geList = dbBooks
.Where(lb => lb.Book.IsProduct()) .Where(lb => lb.Book.IsProduct())
@ -159,81 +105,74 @@ namespace LibationAvalonia.ViewModels
geList.Add(seriesEntry); geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children); geList.AddRange(seriesEntry.Children);
} }
return geList.OrderByDescending(e => e.DateAdded);
var bookList = geList.OrderByDescending(e => e.DateAdded).ToList();
//ListIndex is used by RowComparer to make column sort stable
int index = 0;
foreach (GridEntry di in bookList)
di.ListIndex = index++;
return bookList;
} }
public void ToggleSeriesExpanded(SeriesEntry seriesEntry) public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
{ {
if (seriesEntry.Liberate.Expanded) seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
GridEntries.CollapseItem(seriesEntry); GridEntries.Refresh();
else
GridEntries.ExpandItem(seriesEntry);
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
} }
#endregion #endregion
#region Filtering #region Filtering
public async Task Filter(string searchString) public async Task Filter(string searchString)
{ {
await Dispatcher.UIThread.InvokeAsync(() => if (searchString == FilterString)
{ return;
int visibleCount = GridEntries.Count;
if (string.IsNullOrEmpty(searchString)) FilteredInGridEntries = QueryResults(SOURCE, searchString);
GridEntries.RemoveFilter();
else
GridEntries.Filter = searchString;
if (visibleCount != GridEntries.Count) FilterString = searchString;
VisibleCountChanged?.Invoke(this, GridEntries.BookEntries().Count());
//Re-sort after filtering await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
ReSort();
});
} }
private bool CollectionFilter(object item)
{
if (item is LibraryBookEntry lbe
&& lbe.IsEpisode
&& lbe.Parent?.Liberate?.Expanded != true)
return false;
if (FilteredInGridEntries is null) return true;
return FilteredInGridEntries.Contains(item);
}
private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString)
{
if (string.IsNullOrEmpty(searchString)) return null;
var SearchResults = SearchEngineCommands.Search(searchString);
var booksFilteredIn = entries.BookEntries().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = entries.SeriesEntries().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
return booksFilteredIn.Concat(seriesFilteredIn).ToList();
}
#endregion #endregion
#region Sorting
public void Sort(DataGridColumn sortColumn)
{
//Force the comparer to get the current sort order. We can't
//retrieve it from inside this event handler because Avalonia
//doesn't set the property until after this event.
var comparer = sortColumn.CustomSortComparer as RowComparer;
comparer.SortDirection = null;
_currentSortColumn = sortColumn;
}
//Must be invoked on UI thread
private void ReSort()
{
if (_currentSortColumn is null)
{
//Sort ascending and reverse. That's how the comparer is designed to work to be compatible with Avalonia.
var defaultComparer = new RowComparer(ListSortDirection.Descending, nameof(GridEntry.DateAdded));
GridEntries.InternalList.Sort(defaultComparer);
GridEntries.InternalList.Reverse();
GridEntries.ResetCollection();
}
else
{
_currentSortColumn.Sort(((RowComparer)_currentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending);
}
}
#endregion
#region Scan and Remove Books #region Scan and Remove Books
public void DoneRemovingBooks() public void DoneRemovingBooks()
{ {
foreach (var item in GridEntries.AllItems()) foreach (var item in SOURCE)
item.PropertyChanged -= Item_PropertyChanged; item.PropertyChanged -= GridEntry_PropertyChanged;
RemoveColumnVisivle = false; RemoveColumnVisivle = false;
} }
@ -248,49 +187,47 @@ namespace LibationAvalonia.ViewModels
var result = await MessageBox.ShowConfirmationDialog( var result = await MessageBox.ShowConfirmationDialog(
null, null,
libraryBooks, libraryBooks,
// do not use `$` string interpolation. See impl. // do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?", "Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?"); "Remove books from Libation?");
if (result != DialogResult.Yes) if (result != DialogResult.Yes)
return; return;
foreach (var book in selectedBooks) foreach (var book in selectedBooks)
book.PropertyChanged -= Item_PropertyChanged; book.PropertyChanged -= GridEntry_PropertyChanged;
var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList(); var idsToRemove = libraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
return;
//After DisplayBooks() re-creates the list,
//re-subscribe to all items' PropertyChanged events.
foreach (var b in GetAllBookEntries())
b.PropertyChanged += GridEntry_PropertyChanged;
GridEntries.CollectionChanged -= BindingList_CollectionChanged;
}
GridEntries.CollectionChanged += BindingList_CollectionChanged; GridEntries.CollectionChanged += BindingList_CollectionChanged;
//The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(), //The RemoveBooksAsync will fire LibrarySizeChanged, which calls ProductsDisplay2.Display(),
//so there's no need to remove books from the grid display here. //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);
foreach (var b in GetAllBookEntries())
b.Remove = false;
RemovableCountChanged?.Invoke(this, 0); 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) public async Task ScanAndRemoveBooksAsync(params Account[] accounts)
{ {
foreach (var item in GridEntries.AllItems()) foreach (var item in SOURCE)
{ {
item.Remove = false; item.Remove = false;
item.PropertyChanged += Item_PropertyChanged; item.PropertyChanged += GridEntry_PropertyChanged;
} }
RemoveColumnVisivle = true; RemoveColumnVisivle = true;
@ -303,9 +240,6 @@ namespace LibationAvalonia.ViewModels
var allBooks = GetAllBookEntries(); var allBooks = GetAllBookEntries();
foreach (var b in allBooks)
b.Remove = false;
var lib = allBooks var lib = allBooks
.Select(lbe => lbe.LibraryBook) .Select(lbe => lbe.LibraryBook)
.Where(lb => !lb.Book.HasLiberated()); .Where(lb => !lb.Book.HasLiberated());
@ -327,7 +261,7 @@ namespace LibationAvalonia.ViewModels
} }
} }
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e) private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry) if (e.PropertyName == nameof(GridEntry.Remove) && sender is LibraryBookEntry lbEntry)
{ {

View File

@ -13,25 +13,19 @@ namespace LibationAvalonia.ViewModels
/// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex /// sorted by series index, ascending. Stable sorting is achieved by comparing the GridEntry.ListIndex
/// properties when 2 items compare equal. /// properties when 2 items compare equal.
/// </summary> /// </summary>
internal class RowComparer : IComparer, IComparer<GridEntry> internal class RowComparer : IComparer, IComparer<GridEntry>, IComparer<object>
{ {
private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", BindingFlags.NonPublic | BindingFlags.Instance);
public DataGridColumn Column { get; init; } public DataGridColumn Column { get; init; }
public string PropertyName { get; private set; } public string PropertyName { get; private set; }
public ListSortDirection? SortDirection { get; set; }
public RowComparer(DataGridColumn column) public RowComparer(DataGridColumn column)
{ {
Column = column; Column = column;
PropertyName = Column.SortMemberPath; PropertyName = Column.SortMemberPath;
} }
public RowComparer(ListSortDirection direction, string propertyName)
{
SortDirection = direction;
PropertyName = propertyName;
}
public int Compare(object x, object y) public int Compare(object x, object y)
{ {
@ -42,7 +36,7 @@ namespace LibationAvalonia.ViewModels
var geA = (GridEntry)x; var geA = (GridEntry)x;
var geB = (GridEntry)y; var geB = (GridEntry)y;
SortDirection ??= GetSortOrder(); var sortDirection = GetSortOrder();
SeriesEntry parentA = null; SeriesEntry parentA = null;
SeriesEntry parentB = null; SeriesEntry parentB = null;
@ -54,16 +48,16 @@ namespace LibationAvalonia.ViewModels
//both a and b are top-level grid entries //both a and b are top-level grid entries
if (parentA is null && parentB is null) if (parentA is null && parentB is null)
return InternalCompare(geA, geB); return InternalCompare(geA, geB, sortDirection);
//a is top-level, b is a child //a is top-level, b is a child
if (parentA is null && parentB is not null) if (parentA is null && parentB is not null)
{ {
// b is a child of a, parent is always first // b is a child of a, parent is always first
if (parentB == geA) if (parentB == geA)
return SortDirection is ListSortDirection.Ascending ? -1 : 1; return sortDirection is ListSortDirection.Ascending ? -1 : 1;
else else
return InternalCompare(geA, parentB); return InternalCompare(geA, parentB, sortDirection);
} }
//a is a child, b is a top-level //a is a child, b is a top-level
@ -71,24 +65,24 @@ namespace LibationAvalonia.ViewModels
{ {
// a is a child of b, parent is always first // a is a child of b, parent is always first
if (parentA == geB) if (parentA == geB)
return SortDirection is ListSortDirection.Ascending ? 1 : -1; return sortDirection is ListSortDirection.Ascending ? 1 : -1;
else else
return InternalCompare(parentA, geB); return InternalCompare(parentA, geB, sortDirection);
} }
//both are children of the same series, always present in order of series index, ascending //both are children of the same series, always present in order of series index, ascending
if (parentA == parentB) if (parentA == parentB)
return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1); return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
//a and b are children of different series. //a and b are children of different series.
return InternalCompare(parentA, parentB); return InternalCompare(parentA, parentB, sortDirection);
} }
//Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection //Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection
private ListSortDirection? GetSortOrder() private ListSortDirection? GetSortOrder()
=> CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?; => CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?;
private int InternalCompare(GridEntry x, GridEntry y) private int InternalCompare(GridEntry x, GridEntry y, ListSortDirection? sortDirection)
{ {
var val1 = x.GetMemberValue(PropertyName); var val1 = x.GetMemberValue(PropertyName);
var val2 = y.GetMemberValue(PropertyName); var val2 = y.GetMemberValue(PropertyName);
@ -98,7 +92,7 @@ namespace LibationAvalonia.ViewModels
//If items compare equal, compare them by their positions in the the list. //If items compare equal, compare them by their positions in the the list.
//This is how you achieve a stable sort. //This is how you achieve a stable sort.
if (compareResult == 0) if (compareResult == 0)
return x.ListIndex.CompareTo(y.ListIndex); return x.ListIndex.CompareTo(y.ListIndex) * (sortDirection is ListSortDirection.Ascending ? 1 : -1);
else else
return compareResult; return compareResult;
} }

View File

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

View File

@ -174,13 +174,12 @@ namespace LibationAvalonia.Views
public void ProductsDisplay_Initialized1(object sender, EventArgs e) public void ProductsDisplay_Initialized1(object sender, EventArgs e)
{ {
if (sender is ProductsDisplay products)
_viewModel.ProductsDisplay.RegisterCollectionChanged(products);
} }
private void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks) private async void MainWindow_LibraryLoaded(object sender, List<LibraryBook> dbBooks)
{ {
_viewModel.ProductsDisplay.InitialDisplay(dbBooks); await _viewModel.ProductsDisplay.DisplayBooks(dbBooks);
} }
private void InitializeComponent() private void InitializeComponent()

View File

@ -20,16 +20,25 @@
CanUserReorderColumns="True"> CanUserReorderColumns="True">
<DataGrid.Columns> <DataGrid.Columns>
<controls:DataGridCheckBoxColumnExt <DataGridTemplateColumn
PropertyChanged="RemoveColumn_PropertyChanged"
IsVisible="{Binding RemoveColumnVisivle}"
Header="Remove"
IsThreeState="True"
IsReadOnly="False"
CanUserSort="True" CanUserSort="True"
Binding="{Binding Remove, Mode=TwoWay}" IsVisible="{Binding RemoveColumnVisivle}"
Width="70" SortMemberPath="Remove" /> PropertyChanged="RemoveColumn_PropertyChanged"
Header="Remove"
IsReadOnly="False"
SortMemberPath="Remove"
Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox
HorizontalAlignment="Center"
IsThreeState="True"
IsChecked="{Binding Remove, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate"> <DataGridTemplateColumn CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>

View File

@ -27,20 +27,25 @@ namespace LibationAvalonia.Views
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
List<GridEntry> sampleEntries = new() List<LibraryBook> sampleEntries = new()
{ {
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU")), //context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G")), context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG")), context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q")), context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO")), context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4")), context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0")), context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
new LibraryBookEntry(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")), context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
}; };
DataContext = new ProductsDisplayViewModel(sampleEntries);
var pdvm = new ProductsDisplayViewModel();
pdvm.DisplayBooks(sampleEntries);
DataContext = pdvm;
return; return;
} }
Configure_ColumnCustomization(); Configure_ColumnCustomization();
foreach (var column in productsGrid.Columns) foreach (var column in productsGrid.Columns)
@ -51,7 +56,7 @@ namespace LibationAvalonia.Views
private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e) private void ProductsGrid_Sorting(object sender, DataGridColumnEventArgs e)
{ {
_viewModel.Sort(e.Column);
} }
private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void RemoveColumn_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)