feat(01-06): implement FeatureViewModelBase with async/cancel/progress pattern
- Add ProgressUpdatedMessage ValueChangedMessage for StatusBar live updates - Add FeatureViewModelBase with CancellationTokenSource lifecycle, IsRunning, IProgress<OperationProgress>, OperationCanceledException handling - Add 6 unit tests covering lifecycle, progress, cancellation, error handling and CanExecute guard
This commit is contained in:
82
SharepointToolbox/ViewModels/FeatureViewModelBase.cs
Normal file
82
SharepointToolbox/ViewModels/FeatureViewModelBase.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharepointToolbox.Core.Messages;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Localization;
|
||||
|
||||
namespace SharepointToolbox.ViewModels;
|
||||
|
||||
public abstract partial class FeatureViewModelBase : ObservableRecipient
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly ILogger<FeatureViewModelBase> _logger;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(CancelCommand))]
|
||||
private bool _isRunning;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _progressValue;
|
||||
|
||||
public IAsyncRelayCommand RunCommand { get; }
|
||||
public RelayCommand CancelCommand { get; }
|
||||
|
||||
protected FeatureViewModelBase(ILogger<FeatureViewModelBase> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
RunCommand = new AsyncRelayCommand(ExecuteAsync, () => !IsRunning);
|
||||
CancelCommand = new RelayCommand(() => _cts?.Cancel(), () => IsRunning);
|
||||
IsActive = true; // Activates ObservableRecipient for WeakReferenceMessenger
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
IsRunning = true;
|
||||
StatusMessage = string.Empty;
|
||||
ProgressValue = 0;
|
||||
try
|
||||
{
|
||||
var progress = new Progress<OperationProgress>(p =>
|
||||
{
|
||||
ProgressValue = p.Total > 0 ? (int)(100.0 * p.Current / p.Total) : 0;
|
||||
StatusMessage = p.Message;
|
||||
WeakReferenceMessenger.Default.Send(new ProgressUpdatedMessage(p));
|
||||
});
|
||||
await RunOperationAsync(_cts.Token, progress);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
StatusMessage = TranslationSource.Instance["status.cancelled"];
|
||||
_logger.LogInformation("Operation cancelled by user.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"{TranslationSource.Instance["err.generic"]} {ex.Message}";
|
||||
_logger.LogError(ex, "Operation failed.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRunning = false;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress);
|
||||
|
||||
protected override void OnActivated()
|
||||
{
|
||||
Messenger.Register<TenantSwitchedMessage>(this, (r, m) => ((FeatureViewModelBase)r).OnTenantSwitched(m.Value));
|
||||
}
|
||||
|
||||
protected virtual void OnTenantSwitched(TenantProfile profile)
|
||||
{
|
||||
// Derived classes override to reset their state
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user