|
10 | 10 | @inject NavigationManager Navigation |
11 | 11 | @inject AuthenticationStateProvider AuthStateProvider |
12 | 12 | @inject ISnackbar Snackbar |
13 | | -@implements IAsyncDisposable |
| 13 | +@implements IDisposable |
14 | 14 | @rendermode InteractiveServer |
15 | 15 | @inject CatalogService CatalogService |
16 | 16 | @inject CurrencyService CurrencyService |
|
307 | 307 |
|
308 | 308 | [CascadingParameter] private Task<AuthenticationState> AuthStateTask { get; set; } = default!; |
309 | 309 | private System.Security.Claims.ClaimsPrincipal? user; |
| 310 | + private readonly CancellationTokenSource _cts = new(); |
| 311 | + private bool _disposed; |
310 | 312 | private int quantity = 1; |
311 | 313 |
|
312 | 314 | private async Task AddToCartAsync() |
|
315 | 317 |
|
316 | 318 | try |
317 | 319 | { |
318 | | - await CartClient.AddToCartAsync(new BookStore.Client.AddToCartClientRequest(book.Id, quantity)); |
| 320 | + await CartClient.AddToCartAsync(new BookStore.Client.AddToCartClientRequest(book.Id, quantity), _cts.Token); |
319 | 321 | Snackbar.Add($"Added {quantity} copy(ies) to cart", Severity.Success); |
320 | 322 | quantity = 1; // Reset quantity |
321 | 323 | } |
|
340 | 342 |
|
341 | 343 | protected override async Task OnParametersSetAsync() |
342 | 344 | { |
| 345 | + if (_disposed) return; |
343 | 346 | // Reload when ID parameter changes |
344 | 347 | // Note: queryFn captures 'Id', so we need to reload if Id changes. |
345 | 348 | // But ReactiveQuery caches the function. We strictly should allow updating the function or creating new query. |
|
350 | 353 | // But we need to check if it actually changed to avoid loop? |
351 | 354 | // Actually OnParametersSetAsync runs on standard parameter set. |
352 | 355 | // We'll manage checking inside InitializeQueryAsync logic or just re-init. |
353 | | - await InitializeQueryAsync(); |
| 356 | + await InitializeQueryAsync(_cts.Token); |
354 | 357 | } |
355 | 358 |
|
356 | 359 | var authState = await AuthStateTask; |
|
361 | 364 | user = newUser; |
362 | 365 | if (user.Identity?.IsAuthenticated == true && bookQuery != null) |
363 | 366 | { |
364 | | - await bookQuery.LoadAsync(silent: true); |
| 367 | + await bookQuery.LoadAsync(silent: true, cancellationToken: _cts.Token); |
365 | 368 | } |
366 | 369 | } |
367 | 370 | } |
368 | 371 |
|
369 | | - private async Task InitializeQueryAsync() |
| 372 | + private async Task InitializeQueryAsync(CancellationToken cancellationToken = default) |
370 | 373 | { |
371 | 374 | // Dispose previous if exists |
372 | 375 | bookQuery?.Dispose(); |
373 | 376 |
|
374 | 377 | bookQuery = new ReactiveQuery<BookDto?>( |
375 | | - queryFn: async () => |
| 378 | + queryFn: async ct => |
376 | 379 | { |
377 | 380 | try |
378 | 381 | { |
379 | 382 | return await BooksClient.GetBookAsync( |
380 | | - id: Id); |
| 383 | + id: Id, cancellationToken: ct); |
381 | 384 | } |
382 | 385 | catch (Refit.ApiException apiEx) when (apiEx.StatusCode == System.Net.HttpStatusCode.NotFound) |
383 | 386 | { |
|
387 | 390 | eventsService: EventsService, |
388 | 391 | invalidationService: InvalidationService, |
389 | 392 | queryKeys: new[] { $"Book:{Id}", "User" }, |
390 | | - onStateChanged: StateHasChanged, |
| 393 | + onStateChanged: () => InvokeAsync(StateHasChanged), |
391 | 394 | logger: Logger |
392 | 395 | ); |
393 | 396 |
|
394 | | - await bookQuery.LoadAsync(); |
| 397 | + await bookQuery.LoadAsync(cancellationToken: cancellationToken); |
395 | 398 | UpdateBreadcrumbs(); |
396 | 399 | } |
397 | 400 |
|
398 | | - private async Task<BookDto?> FetchBookAsync() |
| 401 | + private async Task<BookDto?> FetchBookAsync(CancellationToken cancellationToken) |
399 | 402 | { |
400 | 403 | try |
401 | 404 | { |
402 | 405 | return await BooksClient.GetBookAsync( |
403 | | - id: Id); |
| 406 | + id: Id, cancellationToken: cancellationToken); |
404 | 407 | } |
405 | 408 | catch (Refit.ApiException apiEx) when (apiEx.StatusCode == System.Net.HttpStatusCode.NotFound) |
406 | 409 | { |
|
422 | 425 |
|
423 | 426 | private async Task RetryLoad() |
424 | 427 | { |
| 428 | + if (_disposed) return; |
425 | 429 | if (bookQuery != null) |
426 | 430 | { |
427 | | - await bookQuery.LoadAsync(); |
| 431 | + await bookQuery.LoadAsync(cancellationToken: _cts.Token); |
428 | 432 | UpdateBreadcrumbs(); |
429 | 433 | } |
430 | 434 | } |
|
445 | 449 | { |
446 | 450 | IsFavorite = oldState, |
447 | 451 | LikeCount = oldState ? current.LikeCount + 1 : current.LikeCount - 1 |
448 | | - })); |
| 452 | + }), |
| 453 | + cancellationToken: _cts.Token); |
449 | 454 | } |
450 | 455 |
|
451 | 456 | private async Task RateBookAsync(int rating) |
|
464 | 469 | : current with |
465 | 470 | { |
466 | 471 | UserRating = oldRating |
467 | | - })); |
| 472 | + }), |
| 473 | + cancellationToken: _cts.Token); |
468 | 474 | } |
469 | 475 |
|
470 | 476 | private async Task RemoveRatingAsync() |
|
483 | 489 | : current with |
484 | 490 | { |
485 | 491 | UserRating = oldRating |
486 | | - })); |
| 492 | + }), |
| 493 | + cancellationToken: _cts.Token); |
487 | 494 | } |
488 | 495 |
|
489 | 496 | private async Task DeleteBookAsync() |
|
500 | 507 |
|
501 | 508 | try |
502 | 509 | { |
503 | | - await BooksClient.SoftDeleteBookAsync(currentBook.Id); |
| 510 | + await BooksClient.SoftDeleteBookAsync(currentBook.Id, cancellationToken: _cts.Token); |
504 | 511 | Snackbar.Add("Book deleted", Severity.Success); |
505 | | - if (bookQuery != null) await bookQuery.LoadAsync(); |
| 512 | + if (bookQuery != null) await bookQuery.LoadAsync(cancellationToken: _cts.Token); |
506 | 513 | } |
507 | 514 | catch (Exception ex) |
508 | 515 | { |
|
518 | 525 |
|
519 | 526 | try |
520 | 527 | { |
521 | | - await BooksClient.RestoreBookAsync(currentBook.Id); |
| 528 | + await BooksClient.RestoreBookAsync(currentBook.Id, cancellationToken: _cts.Token); |
522 | 529 | Snackbar.Add("Book restored", Severity.Success); |
523 | | - if (bookQuery != null) await bookQuery.LoadAsync(); |
| 530 | + if (bookQuery != null) await bookQuery.LoadAsync(cancellationToken: _cts.Token); |
524 | 531 | } |
525 | 532 | catch (Exception ex) |
526 | 533 | { |
|
532 | 539 | // Inject EventsService which wasn't injected before |
533 | 540 | [Inject] private BookStoreEventsService EventsService { get; set; } = default!; |
534 | 541 |
|
535 | | - public async ValueTask DisposeAsync() |
| 542 | + public void Dispose() |
536 | 543 | { |
| 544 | + if (_disposed) return; |
| 545 | + _disposed = true; |
| 546 | + |
| 547 | + _cts.Cancel(); |
| 548 | + _cts.Dispose(); |
537 | 549 | bookQuery?.Dispose(); |
538 | 550 | CurrencyService.OnCurrencyChanged -= StateHasChanged; |
539 | 551 | } |
|
0 commit comments