From eb49dcfc54947ab9b95b8c801faaaa41f8f713a6 Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Wed, 13 Jul 2022 01:14:05 -0600 Subject: [PATCH] Incremental prgress. --- .../DataGridCheckBoxColumnExt.axaml.cs | 72 +++++++++- .../AvaloniaUI/ViewModels/GridEntry2.cs | 1 - .../ViewModels/GridEntryBindingList2.cs | 4 +- .../AvaloniaUI/ViewModels/RowComparer.cs | 99 +++++++++++++ .../AvaloniaUI/Views/ProductsDisplay2.axaml | 2 +- .../Views/ProductsDisplay2.axaml.cs | 131 ++++-------------- 6 files changed, 194 insertions(+), 115 deletions(-) create mode 100644 Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs diff --git a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs index 9e82e369..b481dda4 100644 --- a/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Controls/DataGridCheckBoxColumnExt.axaml.cs @@ -1,15 +1,79 @@ using Avalonia.Controls; -using Avalonia.Controls.Utils; using Avalonia.Interactivity; using LibationWinForms.AvaloniaUI.ViewModels; using System; -using System.Linq; +using System.Reflection; namespace LibationWinForms.AvaloniaUI.Controls { - /// The purpose of this extension WAS to immediately commit any check state changes to the viewmodel, but for the life of me I cannot get it to work! + /// The purpose of this extension it to immediately commit any check + /// state changes to the viewmodel. There must be a better way to do this, but + /// I sure as shit can't find it. public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn { - + Func _owningGrid_get; + Func _endCellEdit; + Func _waitForLostFocus; + public DataGrid OwningGrid + { + get + { + if (_owningGrid_get == null) + { + var pi = typeof(DataGridColumn).GetProperty(nameof(OwningGrid), BindingFlags.NonPublic | BindingFlags.Instance); + var mi = pi.GetGetMethod(true); + _owningGrid_get = mi.CreateDelegate>(this); + } + return _owningGrid_get(); + } + } + + public Func WaitForLostFocus + { + get + { + if (_endCellEdit == null) + { + var mi = typeof(DataGrid).GetMethod(nameof(WaitForLostFocus), BindingFlags.NonPublic | BindingFlags.Instance); + _waitForLostFocus = mi.CreateDelegate>(OwningGrid); + } + return _waitForLostFocus; + } + } + + public Func EndCellEdit + { + get + { + if (_endCellEdit == null) + { + var mi = typeof(DataGrid).GetMethod(nameof(EndCellEdit), BindingFlags.NonPublic | BindingFlags.Instance); + _endCellEdit = mi.CreateDelegate>(OwningGrid); + } + return _endCellEdit; + } + } + + protected override IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem) + { + var ele = base.GenerateEditingElementDirect(cell, dataItem) as CheckBox; + ele.Checked += EditingElement_Checked; + ele.Unchecked += EditingElement_Checked; + ele.Indeterminate += EditingElement_Checked; + return ele; + } + + private void EditingElement_Checked(object sender, RoutedEventArgs e) + { + if (sender is CheckBox cbox && cbox.DataContext is GridEntry2 gentry) + { + var check = cbox.IsChecked; + WaitForLostFocus(() => + { + EndCellEdit(DataGridEditAction.Cancel, true, true, false); + gentry.Remove = check; + }); + } + } } } diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs index 21e684b1..3f866bf3 100644 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs +++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntry2.cs @@ -57,7 +57,6 @@ namespace LibationWinForms.AvaloniaUI.ViewModels public string ProductRating { get => _productRating; protected set { this.RaiseAndSetIfChanged(ref _productRating, value); } } public string MyRating { get => _myRating; protected set { this.RaiseAndSetIfChanged(ref _myRating, value); } } - protected bool? _remove = false; public abstract bool? Remove { get; set; } public abstract LiberateButtonStatus2 Liberate { get; } diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs index b2c42b57..9a881515 100644 --- a/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs +++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/GridEntryBindingList2.cs @@ -128,13 +128,11 @@ namespace LibationWinForms.AvaloniaUI.ViewModels public void ExpandItem(SeriesEntrys2 sEntry) { - var sindex = Items.IndexOf(sEntry); - foreach (var episode in FilterRemoved.BookEntries().Where(b => b.Parent == sEntry).ToList()) { if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId)) { - InsertItem(++sindex, episode); + Add(episode); } } sEntry.Liberate.Expanded = true; diff --git a/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs b/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs new file mode 100644 index 00000000..8a3585bc --- /dev/null +++ b/Source/LibationWinForms/AvaloniaUI/ViewModels/RowComparer.cs @@ -0,0 +1,99 @@ +using Avalonia.Controls; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibationWinForms.AvaloniaUI.ViewModels +{ + //TODO keep episodes beneath their parents when other entries compare equal (because as it stands, children always compare > parents. + /// + /// This compare class ensures that all top-level grid entries (standalone books or series parents) + /// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain + /// sorted by series index, ascending. + /// + internal class RowComparer : IComparer + { + private static readonly System.Reflection.PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + private static readonly System.Reflection.PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + public DataGridColumn Column { get; init; } + public string PropertyName { get; private set; } + public ListSortDirection? SortDirection { get; set; } + + public RowComparer(DataGridColumn column) + { + Column = column; + PropertyName = Column.SortMemberPath; + } + + + public int Compare(object x, object y) + { + if (x is null && y is not null) return -1; + if (x is not null && y is null) return 1; + if (x is null && y is null) return 0; + + var geA = (GridEntry2)x; + var geB = (GridEntry2)y; + + SortDirection ??= GetSortOrder(); + + SeriesEntrys2 parentA = null; + SeriesEntrys2 parentB = null; + + if (geA is LibraryBookEntry2 lbA && lbA.Parent is SeriesEntrys2 seA) + parentA = seA; + if (geB is LibraryBookEntry2 lbB && lbB.Parent is SeriesEntrys2 seB) + parentB = seB; + + + + //both a and b are standalone + if (parentA is null && parentB is null) + return Compare(geA, geB); + + //a is a standalone, b is a child + if (parentA is null && parentB is not null) + { + // b is a child of a, parent is always first + if (parentB == geA) + return SortDirection is ListSortDirection.Ascending ? -1 : 1; + else + return Compare(geA, parentB); + } + + //a is a child, b is a standalone + if (parentA is not null && parentB is null) + { + // a is a child of b, parent is always first + if (parentA == geB) + return SortDirection is ListSortDirection.Ascending ? 1 : -1; + else + return Compare(parentA, geB); + } + + //both are children of the same series, always present in order of series index, ascending + if (parentA == parentB) + return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1); + + //a and b are children of different series. + return Compare(parentA, parentB); + } + + //Avalonia doesn't expose the column's CurrentSortingState, so we must get it through reflection + private ListSortDirection? GetSortOrder() + => CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?; + + private int Compare(GridEntry2 x, GridEntry2 y) + { + var val1 = x.GetMemberValue(PropertyName); + var val2 = y.GetMemberValue(PropertyName); + + return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); + } + } +} diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml index 50361033..55a4323f 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml +++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml @@ -11,7 +11,7 @@ - + diff --git a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs index 5c40f5b1..4c8b6bf3 100644 --- a/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs +++ b/Source/LibationWinForms/AvaloniaUI/Views/ProductsDisplay2.axaml.cs @@ -124,7 +124,27 @@ namespace LibationWinForms.AvaloniaUI.Views }; } - #endregion + #endregion + + #region Filter + + 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(); + } + + #endregion #region Sorting @@ -150,7 +170,10 @@ namespace LibationWinForms.AvaloniaUI.Views private void ReSort() { if (CurrentSortColumn is null) + { bindingList.InternalList.Sort((i1, i2) => i2.DateAdded.CompareTo(i1.DateAdded)); + bindingList.ResetCollection(); + } else CurrentSortColumn.Sort(((RowComparer)CurrentSortColumn.CustomSortComparer).SortDirection ?? ListSortDirection.Ascending); } @@ -168,88 +191,6 @@ namespace LibationWinForms.AvaloniaUI.Views CurrentSortColumn = e.Column; } - private class RowComparer : IComparer - { - private static readonly System.Reflection.PropertyInfo HeaderCellPi = typeof(DataGridColumn).GetProperty("HeaderCell", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - private static readonly System.Reflection.PropertyInfo CurrentSortingStatePi = typeof(DataGridColumnHeader).GetProperty("CurrentSortingState", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - public DataGridColumn Column { get; init; } - public string PropertyName { get; init; } - public ListSortDirection? SortDirection { get; set; } - - public RowComparer(DataGridColumn column) - { - Column = column; - PropertyName = column.SortMemberPath; - } - - /// - /// This compare method ensures that all top-level grid entries (standalone books or series parents) - /// are sorted by PropertyName while all episodes remain immediately beneath their parents and remain - /// sorted by series index, ascending. - /// - public int Compare(object x, object y) - { - if (x is null) return -1; - if (y is null) return 1; - if (x is null && y is null) return 0; - - var geA = (GridEntry2)x; - var geB = (GridEntry2)y; - - SortDirection ??= GetSortOrder(); - - SeriesEntrys2 parentA = null; - SeriesEntrys2 parentB = null; - - if (geA is LibraryBookEntry2 lbA && lbA.Parent is SeriesEntrys2 seA) - parentA = seA; - if (geB is LibraryBookEntry2 lbB && lbB.Parent is SeriesEntrys2 seB) - parentB = seB; - - //both a and b are standalone - if (parentA is null && parentB is null) - return Compare(geA, geB); - - //a is a standalone, b is a child - if (parentA is null && parentB is not null) - { - // b is a child of a, parent is always first - if (parentB == geA) - return SortDirection is ListSortDirection.Ascending ? -1 : 1; - else - return Compare(geA, parentB); - } - - //a is a child, b is a standalone - if (parentA is not null && parentB is null) - { - // a is a child of b, parent is always first - if (parentA == geB) - return SortDirection is ListSortDirection.Ascending ? 1 : -1; - else - return Compare(parentA, geB); - } - - //both are children of the same series, always present in order of series index, ascending - if (parentA == parentB) - return geA.SeriesIndex.CompareTo(geB.SeriesIndex) * (SortDirection is ListSortDirection.Ascending ? 1 : -1); - - //a and b are children of different series. - return Compare(parentA, parentB); - } - - private ListSortDirection? GetSortOrder() - => CurrentSortingStatePi.GetValue(HeaderCellPi.GetValue(Column)) as ListSortDirection?; - - private int Compare(GridEntry2 x, GridEntry2 y) - { - var val1 = x.GetMemberValue(PropertyName); - var val2 = y.GetMemberValue(PropertyName); - - return x.GetMemberComparer(val1.GetType()).Compare(val1, val2); - } - } #endregion @@ -571,29 +512,7 @@ namespace LibationWinForms.AvaloniaUI.Views existingEpisodeEntry.UpdateLibraryBook(episodeBook); } - #endregion - - #region Filter - - 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 - - bindingList.ResetCollection(); - ReSort(); - } - - #endregion + #endregion #region Column Customizations