1using CommunityToolkit.Mvvm.ComponentModel;
2using CommunityToolkit.Mvvm.Input;
5using System.Threading.Tasks;
42 public partial class ObservableTask<TProgress> : ObservableObject, IDisposable
44 private readonly
object syncObject =
new();
45 private readonly List<IDisposable> attachedDisposables =
new();
48 private TProgress progress =
default!;
51 private readonly List<IRelayCommand> notifyCommands =
new();
54 private Func<TaskContext<TProgress>, Task>? taskFactory;
57 private int currentGeneration = 0;
102 public bool IsRefreshing {
get;
private set; }
107 public AggregateException?
Exception {
get;
private set; }
124 get => this.progress;
127 if (!EqualityComparer<TProgress>.Default.Equals(
this.progress, value))
129 this.progress = value;
149 private void CancelExistingTask()
172 private void StartNewTask(
bool isRefresh)
176 lock (this.syncObject)
178 if (this.taskFactory is
null)
182 int ThisGeneration = ++this.currentGeneration;
185 this.CancelExistingTask();
188 this.CurrentTask =
null;
189 this.CurrentWatcher =
null;
190 this.ErrorMessage =
string.Empty;
191 this.Exception =
null;
192 this.InnerException =
null;
193 this.IsRefreshing = isRefresh;
197 Progress<TProgress> ProgressReporter =
new(value =>
199 if (ThisGeneration == this.currentGeneration)
216 this.CurrentTask = this.taskFactory.Invoke(Context);
240 AggregateException? AggregateException =
null;
246 if (current is not
null)
248 await current.ConfigureAwait(
false);
251 catch (OperationCanceledException)
258 AggregateException = ex as AggregateException;
270 lock (this.syncObject)
274 if (generation != this.currentGeneration)
279 else if (!current.IsCompleted)
281 else if (current.IsCanceled)
283 else if (current.IsFaulted)
288 this.Exception = AggregateException;
292 this.IsRefreshing =
false;
296 this.CancellationTokenSource =
null;
301 this.StateChanged?.Invoke(
this, this.
State);
309 private void NotifyAll()
313 this.OnPropertyChanged(nameof(this.
State));
314 this.OnPropertyChanged(nameof(this.IsPending));
315 this.OnPropertyChanged(nameof(this.IsNotPending));
316 this.OnPropertyChanged(nameof(this.IsRunning));
317 this.OnPropertyChanged(nameof(this.IsNotRunning));
318 this.OnPropertyChanged(nameof(this.IsSucceeded));
319 this.OnPropertyChanged(nameof(this.IsNotSucceeded));
320 this.OnPropertyChanged(nameof(this.IsFailed));
321 this.OnPropertyChanged(nameof(this.IsNotFailed));
322 this.OnPropertyChanged(nameof(this.IsCanceled));
323 this.OnPropertyChanged(nameof(this.IsNotCanceled));
324 this.OnPropertyChanged(nameof(this.
Exception));
325 this.OnPropertyChanged(nameof(this.InnerException));
326 this.OnPropertyChanged(nameof(this.ErrorMessage));
327 this.OnPropertyChanged(nameof(this.
Progress));
328 this.OnPropertyChanged(nameof(this.IsRefreshing));
330 foreach (IRelayCommand Command
in this.notifyCommands)
332 Command.NotifyCanExecuteChanged();
336 this.CancelCommand.NotifyCanExecuteChanged();
337 this.ReloadCommand.NotifyCanExecuteChanged();
338 this.RefreshCommand.NotifyCanExecuteChanged();
351 await Watcher.ConfigureAwait(
false);
373 await Watcher.ConfigureAwait(
false);
376 if (NewWatcher is not
null && !ReferenceEquals(Watcher, NewWatcher))
389 lock (this.syncObject)
391 this.taskFactory = task;
392 this.notifyCommands.Clear();
393 if (notifyCommands.Length > 0)
394 this.notifyCommands.AddRange(notifyCommands);
405 => this.
Configure(ctx => task(arg, ctx), notifyCommands);
413 this.StartNewTask(isRefresh:
false);
422 this.
Configure(ctx => task(arg, ctx), notifyCommands);
423 this.StartNewTask(isRefresh:
false);
426 public void Load(Func<Task> task, params IRelayCommand[] notifyCommands)
432 public void Load<TArg>(Func<TArg, Task> task, TArg arg, params IRelayCommand[] notifyCommands)
440 public void Run() => this.StartNewTask(isRefresh:
false);
445 public void RunRefresh() => this.StartNewTask(isRefresh:
true);
453 lock (this.syncObject)
455 this.taskFactory = ctx => task(arg, ctx);
457 this.StartNewTask(isRefresh:
false);
465 lock (this.syncObject)
467 this.taskFactory = ctx => task(arg, ctx);
469 this.StartNewTask(isRefresh:
true);
476 [RelayCommand(CanExecute = nameof(IsNotPending))]
482 this.StartNewTask(isRefresh:
false);
489 [RelayCommand(CanExecute = nameof(IsNotPending))]
495 this.StartNewTask(isRefresh:
true);
501 [RelayCommand(CanExecute = nameof(IsRunning))]
507 lock (this.syncObject)
520 private bool disposed;
522 public void Dispose()
524 this.Dispose(disposing:
true);
525 GC.SuppressFinalize(
this);
531 this.Dispose(disposing:
false);
535 protected virtual void Dispose(
bool disposing)
539 this.disposed =
true;
543 lock (this.syncObject)
549 this.CancellationTokenSource =
null;
552 foreach (var d
in this.attachedDisposables)
557 this.attachedDisposables.Clear();
562 internal void AttachDisposable(IDisposable disposable)
564 if (disposable is
null)
566 lock (this.syncObject)
568 this.attachedDisposables.Add(disposable);
574 public partial class ObservableTask : ObservableTask<int>
580 public TResult? Result {
get;
private set; }
587 base.Load(async ctx =>
589 this.Result = await op(ctx);
597 => base.Configure(async ctx => { this.Result = await op(ctx); }, notify);
603 => base.Configure(async ctx => { this.Result = await op(arg, ctx); }, notify);
610 base.Load(async ctx => { this.Result = await op(arg, ctx); }, notify);
618 base.Run<TArg>(async (a, ctx) => { this.Result = await op(a, ctx); }, arg);
626 base.RunRefresh<TArg>(async (a, ctx) => { this.Result = await op(a, ctx); }, arg);
static readonly TimeSpan Default
Default delay interval if waiting for something
A set of never changing property constants and helpful values.
Base class that references services in the app.
static ILogService LogService
Log service.
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
void Load< TArg >(Func< TArg, TaskContext< TProgress >, Task > task, TArg arg, params IRelayCommand[] notifyCommands)
Parameterized overload of Load(Func<TaskContext<TProgress>, Task>, IRelayCommand[])....
async Task WaitAllAsync(bool waitWhilePending=false)
Asynchronously waits until all UI notifications and state updates for the current task have been proc...
void Configure(Func< TaskContext< TProgress >, Task< TResult > > op, params IRelayCommand[] notify)
Configure without running.
void Cancel()
Cancels the running task.
void RunRefresh< TArg >(Func< TArg, TaskContext< TProgress >, Task > task, TArg arg)
Run a parameterized operation in refresh mode immediately.
TProgress Progress
The latest progress value reported by the task.
string? ErrorMessage
A friendly error message from the exception.
Task? CurrentTask
The current task being processed.
IDispatcherAdapter Dispatcher
Dispatcher to marshal state and property updates. Defaults to UI dispatcher.
AggregateException? Exception
If the task faulted, returns its AggregateException (if applicable).
void Load(Func< TaskContext< TProgress >, Task< TResult > > op, params IRelayCommand[] notify)
Configure and immediately run.
CancellationTokenSource? CancellationTokenSource
The CancellationTokenSource for the running task.
void Load(Func< TaskContext< TProgress >, Task > task, params IRelayCommand[] notifyCommands)
Loads (configures) and immediately starts the task (back-compat convenience).
void Run()
Start the configured task now (no CanExecute gating).
async Task WaitCurrentAsync()
Asynchronously waits for the current watcher task to complete.
Exception? InnerException
The inner exception, if available.
void Refresh()
Refreshes the current task. This cancels any running task and starts a new one using the stored facto...
ObservableTaskStatus State
Gets the current state.
Task? CurrentWatcher
The current watcher monitoring the current Task.
void Configure< TArg >(Func< TArg, TaskContext< TProgress >, Task > task, TArg arg, params IRelayCommand[] notifyCommands)
Configure the factory with a parameter to be captured by the runner (no immediate start).
EventHandler< ObservableTaskStatus >? StateChanged
Optional event fired when State changes. Handy for telemetry or local reactions.
void Reload()
Reload the current task. This cancels any running task and starts a new one using the stored factory.
void Configure(Func< TaskContext< TProgress >, Task > task, params IRelayCommand[] notifyCommands)
Store the factory and optional commands without starting the task. Use Run or RunRefresh to start lat...
void RunRefresh()
Start the configured task now with the refresh flag (no CanExecute gating).
bool UseTaskRun
If true, invokes the task factory via Task.Run to force background-thread execution (useful for CPU-b...
void Run< TArg >(Func< TArg, TaskContext< TProgress >, Task > task, TArg arg)
Run a parameterized operation immediately without permanently changing the configured factory....
ObservableTaskStatus
UI-friendly status for an observable async operation. (Renamed to avoid collision with System....
class TaskContext< TProgress >(bool IsRefreshing, CancellationToken CancellationToken, IProgress< TProgress > Progress)
Represents the context for an asynchronous task execution.