Files
Sharepoint-Toolbox/SharepointToolbox/ViewModels/FeatureViewModelBase.cs
T
Dev f4cc81bb71 chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager)
- Add InputDialog, Spinner common view
- Add DuplicatesCsvExportService
- Refresh views, dialogs, and view models across tabs
- Update localization strings (en/fr)
- Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:23:11 +02:00

128 lines
4.6 KiB
C#

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;
[ObservableProperty]
private bool _isIndeterminate;
/// <summary>
/// Sites selected in the global toolbar picker. Updated via GlobalSitesChangedMessage.
/// Derived VMs check this in RunOperationAsync before falling back to per-tab SiteUrl.
/// </summary>
protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; } = Array.Empty<SiteInfo>();
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;
IsIndeterminate = false;
try
{
var progress = new Progress<OperationProgress>(p =>
{
// Indeterminate reports (throttle waits, inner scan steps) must not
// reset the determinate bar to 0%; only update the status message
// and flip the bar into marquee mode. The next determinate report
// restores % and clears the marquee flag.
if (p.IsIndeterminate)
{
IsIndeterminate = true;
}
else
{
IsIndeterminate = false;
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);
// Success path: replace any lingering "Scanning X…" with a neutral
// completion marker so stale in-progress labels don't stick around.
StatusMessage = TranslationSource.Instance["status.complete"];
ProgressValue = 100;
IsIndeterminate = false;
}
catch (OperationCanceledException)
{
StatusMessage = TranslationSource.Instance["status.cancelled"];
IsIndeterminate = false;
_logger.LogInformation("Operation cancelled by user.");
}
catch (Exception ex)
{
StatusMessage = $"{TranslationSource.Instance["err.generic"]} {ex.Message}";
IsIndeterminate = false;
_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));
Messenger.Register<GlobalSitesChangedMessage>(this, (r, m) => ((FeatureViewModelBase)r).OnGlobalSitesReceived(m.Value));
}
protected virtual void OnTenantSwitched(TenantProfile profile)
{
// Derived classes override to reset their state
}
private void OnGlobalSitesReceived(IReadOnlyList<SiteInfo> sites)
{
GlobalSites = sites;
OnGlobalSitesChanged(sites);
}
/// <summary>
/// Called when the global site selection changes. Override in derived VMs
/// to update UI state (e.g., pre-fill SiteUrl from first global site).
/// </summary>
protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites)
{
// Derived classes override to react to global site changes
}
}