--- name: async-patterns description: Async/await patterns for the 3SC widget host. Covers Task handling, cancellation, ConfigureAwait, fire-and-forget prevention, and async command patterns in WPF. --- # Async Patterns ## Overview Proper async/await usage is critical for UI responsiveness and application stability. This skill covers patterns specific to WPF desktop applications. ## Definition of Done (DoD) - [ ] All async methods return `Task` or `Task` (never `async void` except event handlers) - [ ] Long-running operations support `CancellationToken` - [ ] No `.Result` or `.Wait()` calls on UI thread - [ ] Fire-and-forget tasks are handled with proper error logging - [ ] Async commands show loading state and handle exceptions ## Core Rules ### 1. Never Block the UI Thread ```csharp // ❌ BAD - Blocks UI thread var result = SomeAsyncMethod().Result; var result = SomeAsyncMethod().GetAwaiter().GetResult(); // ✅ GOOD - Async all the way var result = await SomeAsyncMethod(); ``` ### 2. Always Use CancellationToken ```csharp // ✅ GOOD - Supports cancellation public async Task> LoadWidgetsAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); return await _repository.GetAllAsync(cancellationToken); } ``` ### 3. ConfigureAwait in Library Code ```csharp // In Infrastructure/Application layers (non-UI code): public async Task GetByIdAsync(Guid id, CancellationToken ct) { return await _context.Widgets .FirstOrDefaultAsync(w => w.Id == id, ct) .ConfigureAwait(false); // Don't capture UI context } // In UI layer (ViewModels) - capture context for UI updates: public async Task LoadAsync() { var widgets = await _service.GetWidgetsAsync(); // No ConfigureAwait Widgets = new ObservableCollection(widgets); // Must run on UI thread } ``` ## Async Command Pattern ### Standard Async Command ```csharp public partial class WidgetLibraryViewModel : ObservableObject { [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsNotLoading))] private bool _isLoading; public bool IsNotLoading => !IsLoading; [RelayCommand] private async Task LoadWidgetsAsync(CancellationToken cancellationToken) { if (IsLoading) return; // Prevent double-execution IsLoading = true; ErrorMessage = null; try { var widgets = await _repository.GetAllAsync(cancellationToken); Widgets = new ObservableCollection( widgets.Select(w => new WidgetViewModel(w))); } catch (OperationCanceledException) { // Expected when user cancels - don't log as error } catch (Exception ex) { Log.Error(ex, "Failed to load widgets"); ErrorMessage = "Failed to load widgets. Please try again."; } finally { IsLoading = false; } } } ``` ### Command with Automatic Busy State ```csharp // CommunityToolkit.Mvvm automatically sets IsRunning on async commands [RelayCommand] private async Task RefreshAsync(CancellationToken ct) { await _service.RefreshAsync(ct); } // In XAML - bind to command's IsRunning