Merge pull request #1174 from Mbucari/master

Null safety & UI tweak
This commit is contained in:
rmcrackan 2025-03-04 13:10:51 -05:00 committed by GitHub
commit 649b52af1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 40 deletions

View File

@ -16,22 +16,23 @@ using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
public class ProductsDisplayViewModel : ViewModelBase public class ProductsDisplayViewModel : ViewModelBase
{ {
/// <summary>Number of visible rows has changed</summary> /// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged; public event EventHandler<int>? VisibleCountChanged;
public event EventHandler<int> RemovableCountChanged; public event EventHandler<int>? RemovableCountChanged;
/// <summary>Backing list of all grid entries</summary> /// <summary>Backing list of all grid entries</summary>
private readonly AvaloniaList<IGridEntry> SOURCE = new(); private readonly AvaloniaList<IGridEntry> SOURCE = new();
/// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary> /// <summary>Grid entries included in the filter set. If null, all grid entries are shown</summary>
private HashSet<IGridEntry> FilteredInGridEntries; private HashSet<IGridEntry>? FilteredInGridEntries;
public string FilterString { get; private set; } public string? FilterString { get; private set; }
private DataGridCollectionView _gridEntries; private DataGridCollectionView? _gridEntries;
public DataGridCollectionView GridEntries public DataGridCollectionView? GridEntries
{ {
get => _gridEntries; get => _gridEntries;
private set => this.RaiseAndSetIfChanged(ref _gridEntries, value); private set => this.RaiseAndSetIfChanged(ref _gridEntries, value);
@ -60,14 +61,14 @@ namespace LibationAvalonia.ViewModels
VisibleCountChanged?.Invoke(this, 0); VisibleCountChanged?.Invoke(this, 0);
} }
private static readonly System.Reflection.MethodInfo SetFlagsMethod; private static readonly System.Reflection.MethodInfo? SetFlagsMethod;
/// <summary> /// <summary>
/// Tells the <see cref="DataGridCollectionView"/> whether it should process changes to the underlying collection /// Tells the <see cref="DataGridCollectionView"/> whether it should process changes to the underlying collection
/// </summary> /// </summary>
/// <remarks> DataGridCollectionView.CollectionViewFlags.ShouldProcessCollectionChanged = 4</remarks> /// <remarks> DataGridCollectionView.CollectionViewFlags.ShouldProcessCollectionChanged = 4</remarks>
private void SetShouldProcessCollectionChanged(bool flagSet) private void SetShouldProcessCollectionChanged(bool flagSet)
=> SetFlagsMethod.Invoke(GridEntries, new object[] { 4, flagSet }); => SetFlagsMethod?.Invoke(GridEntries, new object[] { 4, flagSet });
static ProductsDisplayViewModel() static ProductsDisplayViewModel()
{ {
@ -131,13 +132,16 @@ namespace LibationAvalonia.ViewModels
//Saves ~500 ms on a library of ~4500 books. //Saves ~500 ms on a library of ~4500 books.
//Perform on UI thread for safety, but at this time, merely setting the DataGridCollectionView //Perform on UI thread for safety, but at this time, merely setting the DataGridCollectionView
//does not trigger UI actions in the way that modifying the list after it's been linked does. //does not trigger UI actions in the way that modifying the list after it's been linked does.
await Dispatcher.UIThread.InvokeAsync(() => GridEntries = new(SOURCE) { Filter = CollectionFilter }); await Dispatcher.UIThread.InvokeAsync(() =>
{
GridEntries = new(SOURCE) { Filter = CollectionFilter };
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
});
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
GridEntries_CollectionChanged(); GridEntries_CollectionChanged();
} }
private void GridEntries_CollectionChanged(object sender = null, EventArgs e = null) private void GridEntries_CollectionChanged(object? sender = null, EventArgs? e = null)
{ {
var count var count
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count() = FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
@ -151,7 +155,12 @@ namespace LibationAvalonia.ViewModels
/// </summary> /// </summary>
internal async Task UpdateGridAsync(List<LibraryBook> dbBooks) internal async Task UpdateGridAsync(List<LibraryBook> dbBooks)
{ {
GridEntries.CollectionChanged -= GridEntries_CollectionChanged; if (GridEntries == null)
{
//always bind before updating. Binding creates GridEntries.
await BindToGridAsync(dbBooks);
return;
}
#region Add new or update existing grid entries #region Add new or update existing grid entries
@ -203,7 +212,8 @@ namespace LibationAvalonia.ViewModels
await Filter(FilterString); await Filter(FilterString);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
GridEntries.CollectionChanged += GridEntries_CollectionChanged; if (GridEntries != null)
GridEntries.CollectionChanged += GridEntries_CollectionChanged;
GridEntries_CollectionChanged(); GridEntries_CollectionChanged();
} }
@ -211,7 +221,7 @@ namespace LibationAvalonia.ViewModels
{ {
foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList()) foreach (var removed in removedBooks.Cast<IGridEntry>().Concat(removedSeries).Where(b => b is not null).ToList())
{ {
if (GridEntries.PassesFilter(removed)) if (GridEntries?.PassesFilter(removed) ?? false)
GridEntries.Remove(removed); GridEntries.Remove(removed);
else else
{ {
@ -222,7 +232,7 @@ namespace LibationAvalonia.ViewModels
} }
} }
private void UpsertBook(LibraryBook book, ILibraryBookEntry existingBookEntry) private void UpsertBook(LibraryBook book, ILibraryBookEntry? existingBookEntry)
{ {
if (existingBookEntry is null) if (existingBookEntry is null)
// Add the new product to top // Add the new product to top
@ -232,7 +242,7 @@ namespace LibationAvalonia.ViewModels
existingBookEntry.UpdateLibraryBook(book); existingBookEntry.UpdateLibraryBook(book);
} }
private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks) private void UpsertEpisode(LibraryBook episodeBook, ILibraryBookEntry? existingEpisodeEntry, List<ISeriesEntry> seriesEntries, IEnumerable<LibraryBook> dbBooks)
{ {
if (existingEpisodeEntry is null) if (existingEpisodeEntry is null)
{ {
@ -282,10 +292,13 @@ namespace LibationAvalonia.ViewModels
private async Task refreshGrid() private async Task refreshGrid()
{ {
if (GridEntries.IsEditingItem) if (GridEntries != null)
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit); {
if (GridEntries.IsEditingItem)
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh); await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
}
} }
public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry) public async Task ToggleSeriesExpanded(ISeriesEntry seriesEntry)
@ -299,7 +312,7 @@ namespace LibationAvalonia.ViewModels
#region Filtering #region Filtering
public async Task Filter(string searchString) public async Task Filter(string? searchString)
{ {
FilterString = searchString; FilterString = searchString;
@ -323,7 +336,7 @@ namespace LibationAvalonia.ViewModels
return FilteredInGridEntries.Contains(item); return FilteredInGridEntries.Contains(item);
} }
private async void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e) private async void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs? e)
{ {
var filterResults = SOURCE.FilterEntries(FilterString); var filterResults = SOURCE.FilterEntries(FilterString);
@ -366,9 +379,9 @@ namespace LibationAvalonia.ViewModels
foreach (var book in selectedBooks) foreach (var book in selectedBooks)
book.PropertyChanged -= GridEntry_PropertyChanged; book.PropertyChanged -= GridEntry_PropertyChanged;
void BindingList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) void BindingList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs? e)
{ {
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset) if (e?.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Reset)
return; return;
//After DisplayBooks() re-creates the list, //After DisplayBooks() re-creates the list,
@ -380,7 +393,8 @@ namespace LibationAvalonia.ViewModels
GridEntries.CollectionChanged -= BindingList_CollectionChanged; GridEntries.CollectionChanged -= BindingList_CollectionChanged;
} }
GridEntries.CollectionChanged += BindingList_CollectionChanged; if (GridEntries != null)
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.
@ -432,9 +446,9 @@ namespace LibationAvalonia.ViewModels
} }
} }
private void GridEntry_PropertyChanged(object sender, PropertyChangedEventArgs e) private void GridEntry_PropertyChanged(object? sender, PropertyChangedEventArgs? e)
{ {
if (e.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry) if (e?.PropertyName == nameof(IGridEntry.Remove) && sender is ILibraryBookEntry)
{ {
int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true); int removeCount = GetAllBookEntries().Count(lbe => lbe.Remove is true);
RemovableCountChanged?.Invoke(this, removeCount); RemovableCountChanged?.Invoke(this, removeCount);

View File

@ -6,10 +6,20 @@
x:DataType="vm:ProcessBookViewModel" x:DataType="vm:ProcessBookViewModel"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}"> x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1"> <UserControl.Styles>
<Style Selector="Border#QueuedItemBorder:not(:pointerover) Button">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="Border#QueuedItemBorder:pointerover Button">
<Setter Property="IsVisible" Value="True" />
</Style>
</UserControl.Styles>
<Border Name="QueuedItemBorder" Background="Transparent" BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left"> <Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left">
<Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" /> <Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" />
</Panel> </Panel>
@ -29,7 +39,7 @@
<TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/> <TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/>
</Panel> </Panel>
</Grid> </Grid>
<Grid Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto"> <Grid Name="ButtonsGrid" Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
<Grid.Styles> <Grid.Styles>
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="Padding" Value="0,1,0,1" /> <Setter Property="Padding" Value="0,1,0,1" />
@ -42,22 +52,22 @@
</Style> </Style>
</Grid.Styles> </Grid.Styles>
<StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical"> <StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
<Button Click="MoveFirst_Click"> <Button ToolTip.Tip="Move book to top of queue" Click="MoveFirst_Click">
<Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" /> <Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" />
</Button> </Button>
<Button Click="MoveUp_Click"> <Button ToolTip.Tip="Move book up in queue" Click="MoveUp_Click">
<Path VerticalAlignment="Top" Data="{StaticResource UpButtonIcon}" /> <Path VerticalAlignment="Top" Data="{StaticResource UpButtonIcon}" />
</Button> </Button>
<Button Click="MoveDown_Click"> <Button ToolTip.Tip="Move book down in queue" Click="MoveDown_Click">
<Path VerticalAlignment="Bottom" Data="{StaticResource DownButtonIcon}" /> <Path VerticalAlignment="Bottom" Data="{StaticResource DownButtonIcon}" />
</Button> </Button>
<Button Click="MoveLast_Click"> <Button ToolTip.Tip="Move book to bottom of queue" Click="MoveLast_Click">
<Path VerticalAlignment="Bottom" Data="{StaticResource LastButtonIcon}" /> <Path VerticalAlignment="Bottom" Data="{StaticResource LastButtonIcon}" />
</Button> </Button>
</StackPanel> </StackPanel>
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top"> <Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top" IsVisible="{CompiledBinding !IsFinished}">
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" IsVisible="{CompiledBinding !IsFinished}" CornerRadius="11" Click="Cancel_Click"> <Button Height="32" Background="{DynamicResource CancelRed}" Width="22" CornerRadius="11" Click="Cancel_Click">
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" /> <Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
</Button> </Button>
</Panel> </Panel>
@ -65,7 +75,7 @@
<Panel Margin="3" Width="50" Grid.Column="2"> <Panel Margin="3" Width="50" Grid.Column="2">
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" /> <TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" />
</Panel> </Panel>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>

View File

@ -2,6 +2,7 @@
using DataLayer; using DataLayer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace LibationUiBase.GridView namespace LibationUiBase.GridView
@ -47,9 +48,11 @@ namespace LibationUiBase.GridView
otherSet is not null && otherSet is not null &&
searchSet.Intersect(otherSet).Count() != searchSet.Count); searchSet.Intersect(otherSet).Count() != searchSet.Count);
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string searchString) [return: NotNullIfNotNull(nameof(searchString))]
public static HashSet<IGridEntry>? FilterEntries(this IEnumerable<IGridEntry> entries, string? searchString)
{ {
if (string.IsNullOrEmpty(searchString)) return null; if (string.IsNullOrEmpty(searchString))
return null;
var searchResultSet = SearchEngineCommands.Search(searchString); var searchResultSet = SearchEngineCommands.Search(searchString);