using System.Globalization; using OpenNest; namespace OpenNest.Data; /// /// Browse state for Add from Drawing Library: the requested search, sort, page and /// nest filter, and the last page applied. By default only drawings with the nest's /// material, grade and thickness are listed; lifts that /// filter. Every change issues one bounded server query. A response that a later request /// superseded is discarded (and the earlier request is cancelled), so an older result can /// never replace a newer one. Call from one thread (the UI thread). /// public sealed class DrawingLibraryBrowseSession : IDisposable { private readonly IDrawingLibrary _library; private CancellationTokenSource? _pending; private int _generation; private bool _disposed; public DrawingLibraryBrowseSession(IDrawingLibrary library, Nest nest, int pageSize = DrawingLibraryQuery.DefaultLimit) { ArgumentNullException.ThrowIfNull(library); ArgumentNullException.ThrowIfNull(nest); if (pageSize < 1 || pageSize > DrawingLibraryQuery.MaxLimit) throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, $"Page size must be 1 to {DrawingLibraryQuery.MaxLimit}."); _library = library; PageSize = pageSize; MaterialName = (nest.Material?.Name ?? "").Trim(); MaterialGrade = (nest.Material?.Grade ?? "").Trim(); // A nest without a thickness has nothing to match; it filters by material only. Thickness = double.IsFinite(nest.Thickness) && nest.Thickness > 0 ? DrawingLibraryRules.NormalizeThickness(nest.Thickness) : null; } public int PageSize { get; } /// The nest's material name, applied unless ; blank means any. public string MaterialName { get; } /// The nest's grade, applied unless ; blank means any. public string MaterialGrade { get; } /// The nest's thickness, applied unless ; null means any. public double? Thickness { get; } /// True when the nest filter is lifted and every active drawing is listed. public bool ShowAll { get; private set; } /// True when the latest request narrows by the nest's material, grade or thickness. public bool IsFilteredToNest => !ShowAll && (MaterialName.Length > 0 || MaterialGrade.Length > 0 || Thickness is not null); /// Trimmed search text of the latest request. public string Search { get; private set; } = ""; public DrawingSortField Sort { get; private set; } = DrawingSortField.SavedAt; public bool Descending { get; private set; } = true; /// Offset of the latest request. public int Offset { get; private set; } /// The latest applied page; null before the first result and after a failed request. public DrawingLibraryPage? Page { get; private set; } /// True while the latest request is outstanding; superseded requests do not count. public bool IsLoading { get; private set; } public bool CanGoPrevious => Offset > 0; public bool CanGoNext => Page is { } page && page.Offset + page.Items.Count < page.Total; /// Operator-facing summary of , such as "Showing 101-200 of 1,234 drawings". public string Summary { get { if (Page is not { } page) return ""; if (page.Total == 0) { if (Search.Length > 0) return "No drawings match the search."; return IsFilteredToNest ? "No library drawings match this nest's material and thickness. Choose Show all to see every drawing." : "No drawings in the library."; } if (page.Items.Count == 0) return $"No drawings on this page ({Count(page.Total)} in total)."; return $"Showing {Count(page.Offset + 1)}-{Count(page.Offset + page.Items.Count)} of {Count(page.Total)} drawings"; } } /// Applies new search text and returns to the first page. public Task SetSearchAsync(string? search) { Search = (search ?? "").Trim(); Offset = 0; return RunAsync(); } /// Lifts or restores the nest filter and returns to the first page. public Task SetShowAllAsync(bool showAll) { ShowAll = showAll; Offset = 0; return RunAsync(); } /// Sorts by a column: a new column starts ascending, the same column reverses. public Task SortByAsync(DrawingSortField field) { Descending = field == Sort && !Descending; Sort = field; Offset = 0; return RunAsync(); } public Task NextPageAsync() { if (!CanGoNext) return Task.FromResult(false); Offset = Page!.Offset + PageSize; return RunAsync(); } public Task PreviousPageAsync() { if (!CanGoPrevious) return Task.FromResult(false); Offset = System.Math.Max(0, Offset - PageSize); return RunAsync(); } /// Re-reads the current page; steps back to the last page if it is now past the end. public Task RefreshAsync() => RunAsync(); /// /// Sends the current request. Returns true when its page was applied, false when a later /// request superseded it. Failures of the latest request clear and propagate; /// failures of superseded requests are ignored. /// private async Task RunAsync() { ObjectDisposedException.ThrowIf(_disposed, this); // Supersede before cancelling: a request that completes synchronously when cancelled // must already see itself as superseded. Each source is released by its own request. var previous = _pending; using var cancellation = new CancellationTokenSource(); _pending = cancellation; var generation = ++_generation; IsLoading = true; try { previous?.Cancel(); var page = await QueryAsync(cancellation.Token); if (generation != _generation) return false; // An archive or another PC can leave the requested page past the end. if (page.Items.Count == 0 && page.Offset > 0 && page.Total > 0) { Offset = (page.Total - 1) / PageSize * PageSize; page = await QueryAsync(cancellation.Token); if (generation != _generation) return false; } Page = page; return true; } catch (Exception) when (generation != _generation) { return false; } catch { Page = null; throw; } finally { if (ReferenceEquals(_pending, cancellation)) _pending = null; if (generation == _generation) IsLoading = false; } } private Task QueryAsync(CancellationToken cancellationToken) => _library.QueryAsync( new DrawingLibraryQuery { Search = Search, MaterialName = ShowAll ? "" : MaterialName, MaterialGrade = ShowAll ? "" : MaterialGrade, Thickness = ShowAll ? null : Thickness, Sort = Sort, Descending = Descending, Offset = Offset, Limit = PageSize, }, cancellationToken); private static string Count(int value) => value.ToString("N0", CultureInfo.CurrentCulture); /// /// Cancels any request in flight (which releases its own source when it ends); /// the library client is not owned and stays open. /// public void Dispose() { if (_disposed) return; _disposed = true; _generation++; IsLoading = false; var pending = _pending; _pending = null; pending?.Cancel(); } }