Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ObservableTask.cs
1using CommunityToolkit.Mvvm.ComponentModel;
2using CommunityToolkit.Mvvm.Input;
3using NeuroAccessMaui.Services; // For LogService
5using System.Threading.Tasks;
6using System.Threading; // Added for OperationCanceledException
7
9{
15 {
16 Pending,
17 Running,
18 Succeeded,
19 Failed,
20 Canceled
21 }
22
42 public partial class ObservableTask<TProgress> : ObservableObject, IDisposable
43 {
44 private readonly object syncObject = new();
45 private readonly List<IDisposable> attachedDisposables = new();
46
47 // Backing field for the latest progress value.
48 private TProgress progress = default!;
49
50 // Commands to notify when state changes.
51 private readonly List<IRelayCommand> notifyCommands = new();
52
53 // The factory delegate to create tasks; stored so that a new load can be triggered.
54 private Func<TaskContext<TProgress>, Task>? taskFactory;
55
56 // A generation counter to identify the currently active task.
57 private int currentGeneration = 0;
58
63 public bool UseTaskRun { get; set; } = false;
64
71 public Task? CurrentTask { get; private set; }
72
79 public Task? CurrentWatcher { get; private set; }
80
84 public IDispatcherAdapter Dispatcher { get; set; } = UiDispatcher.Instance;
85
89 public ObservableTaskStatus State { get; private set; }
90
91 // Convenience properties for data binding.
92 public bool IsPending => this.State == ObservableTaskStatus.Pending;
93 public bool IsNotPending => this.State != ObservableTaskStatus.Pending;
94 public bool IsRunning => this.State == ObservableTaskStatus.Running;
95 public bool IsNotRunning => this.State != ObservableTaskStatus.Running;
96 public bool IsSucceeded => this.State == ObservableTaskStatus.Succeeded;
97 public bool IsNotSucceeded => this.State != ObservableTaskStatus.Succeeded;
98 public bool IsFailed => this.State == ObservableTaskStatus.Failed;
99 public bool IsNotFailed => this.State != ObservableTaskStatus.Failed;
100 public bool IsCanceled => this.State == ObservableTaskStatus.Canceled;
101 public bool IsNotCanceled => this.State != ObservableTaskStatus.Canceled;
102 public bool IsRefreshing { get; private set; }
103
107 public AggregateException? Exception { get; private set; }
108
112 public Exception? InnerException { get; private set; }
113
117 public string? ErrorMessage { get; private set; }
118
122 public TProgress Progress
123 {
124 get => this.progress;
125 private set
126 {
127 if (!EqualityComparer<TProgress>.Default.Equals(this.progress, value))
128 {
129 this.progress = value;
130 // Marshal via dispatcher.
131 this.Dispatcher.Post(() => this.OnPropertyChanged(nameof(this.Progress)));
132 }
133 }
134 }
135
140
144 public event EventHandler<ObservableTaskStatus>? StateChanged;
145
149 private void CancelExistingTask()
150 {
152 if (Cts is null)
153 return;
154
155 try
156 {
157 if (this.CurrentTask is { IsCompleted: false })
158 {
159 Cts.Cancel();
160 }
161 }
162 catch (Exception ex)
163 {
164 ServiceRef.LogService.LogException(ex);
165 }
166 }
167
172 private void StartNewTask(bool isRefresh)
173 {
174 try
175 {
176 lock (this.syncObject)
177 {
178 if (this.taskFactory is null)
179 return;
180
181 // Increment the generation token.
182 int ThisGeneration = ++this.currentGeneration;
183
184 // Cancel any existing task.
185 this.CancelExistingTask();
186
187 this.CancellationTokenSource = new CancellationTokenSource();
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;
194 this.State = ObservableTaskStatus.Running;
195
196 // Create a progress reporter that ensures UI updates and ignores stale generations.
197 Progress<TProgress> ProgressReporter = new(value =>
198 {
199 if (ThisGeneration == this.currentGeneration)
200 {
201 this.Dispatcher.Post(() => this.Progress = value);
202 }
203 });
204
205 TaskContext<TProgress> Context = new(isRefresh, this.CancellationTokenSource.Token, ProgressReporter);
206
207 // Start the new task.
208 if (this.UseTaskRun)
209 {
210 // Force background-thread execution (good for CPU-bound work).
211 this.CurrentTask = Task.Run(async () => await this.taskFactory!(Context), this.CancellationTokenSource.Token);
212 }
213 else
214 {
215 // Default path (good for naturally async/IO-bound work).
216 this.CurrentTask = this.taskFactory.Invoke(Context);
217 }
218
219 this.CurrentWatcher = this.WatchTaskAsync(ThisGeneration, this.CurrentTask, this.CancellationTokenSource);
220 }
221 }
222 catch (Exception ex)
223 {
224 ServiceRef.LogService.LogException(ex);
225 }
226 finally
227 {
228 // Notify bindings and start watching the new task.
229 this.NotifyAll();
230 }
231 }
232
236 private async Task WatchTaskAsync(int generation, Task? current, CancellationTokenSource cts)
237 {
238 // TODO: Add logging for tasks gone rogue if desired.
239
240 AggregateException? AggregateException = null;
242 string? ErrorMessage = null;
243
244 try
245 {
246 if (current is not null)
247 {
248 await current.ConfigureAwait(false);
249 }
250 }
251 catch (OperationCanceledException)
252 {
253 // Normal cooperative cancellation (includes TaskCanceledException). Suppress error logging.
254 }
255 catch (Exception ex)
256 {
257 // Exceptions are surfaced via Exception/InnerException/ErrorMessage.
258 AggregateException = ex as AggregateException;
259 InnerException = ex.InnerException ?? ex;
261
262 // Log by default (can be routed to telemetry later).
263 ServiceRef.LogService.LogException(ex);
264 }
265 finally
266 {
267 // Marshal via dispatcher for state updates.
268 this.Dispatcher.Post(() =>
269 {
270 lock (this.syncObject)
271 {
272 cts.Dispose();
273
274 if (generation != this.currentGeneration)
275 return;
276
277 if (current is null)
278 this.State = ObservableTaskStatus.Pending;
279 else if (!current.IsCompleted)
280 this.State = ObservableTaskStatus.Running;
281 else if (current.IsCanceled)
282 this.State = ObservableTaskStatus.Canceled;
283 else if (current.IsFaulted)
284 this.State = ObservableTaskStatus.Failed;
285 else
286 this.State = ObservableTaskStatus.Succeeded;
287
288 this.Exception = AggregateException;
289 this.InnerException = InnerException;
290 this.ErrorMessage = ErrorMessage;
291
292 this.IsRefreshing = false;
293
294 if (this.CancellationTokenSource is not null)
295 {
296 this.CancellationTokenSource = null;
297 }
298 }
299
300 this.NotifyAll();
301 this.StateChanged?.Invoke(this, this.State);
302 });
303 }
304 }
305
309 private void NotifyAll()
310 {
311 this.Dispatcher.Post(() =>
312 {
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));
329
330 foreach (IRelayCommand Command in this.notifyCommands)
331 {
332 Command.NotifyCanExecuteChanged();
333 }
334
335 // Also update the automatically generated commands.
336 this.CancelCommand.NotifyCanExecuteChanged();
337 this.ReloadCommand.NotifyCanExecuteChanged();
338 this.RefreshCommand.NotifyCanExecuteChanged();
339 });
340 }
341
345 public async Task WaitCurrentAsync()
346 {
347 Task? Watcher = this.CurrentWatcher;
348 if (Watcher is null)
349 return;
350
351 await Watcher.ConfigureAwait(false);
352 }
353
357 public async Task WaitAllAsync(bool waitWhilePending = false)
358 {
359 while (true)
360 {
361 Task? Watcher = this.CurrentWatcher;
362 if (Watcher is null)
363 {
364 if (waitWhilePending && this.State == ObservableTaskStatus.Pending)
365 {
366 await Task.Delay(Constants.Delays.Default).ConfigureAwait(false);
367 continue;
368 }
369
370 return;
371 }
372
373 await Watcher.ConfigureAwait(false);
374
375 Task? NewWatcher = this.CurrentWatcher;
376 if (NewWatcher is not null && !ReferenceEquals(Watcher, NewWatcher))
377 continue;
378
379 break;
380 }
381 }
382
387 public void Configure(Func<TaskContext<TProgress>, Task> task, params IRelayCommand[] notifyCommands)
388 {
389 lock (this.syncObject)
390 {
391 this.taskFactory = task;
392 this.notifyCommands.Clear();
393 if (notifyCommands.Length > 0)
394 this.notifyCommands.AddRange(notifyCommands);
395 }
396
397 // Leave state as-is (likely Pending). No auto-start here.
398 this.NotifyAll();
399 }
400
404 public void Configure<TArg>(Func<TArg, TaskContext<TProgress>, Task> task, TArg arg, params IRelayCommand[] notifyCommands)
405 => this.Configure(ctx => task(arg, ctx), notifyCommands);
406
410 public void Load(Func<TaskContext<TProgress>, Task> task, params IRelayCommand[] notifyCommands)
411 {
412 this.Configure(task, notifyCommands);
413 this.StartNewTask(isRefresh: false);
414 }
415
420 public void Load<TArg>(Func<TArg, TaskContext<TProgress>, Task> task, TArg arg, params IRelayCommand[] notifyCommands)
421 {
422 this.Configure(ctx => task(arg, ctx), notifyCommands);
423 this.StartNewTask(isRefresh: false);
424 }
425
426 public void Load(Func<Task> task, params IRelayCommand[] notifyCommands)
427 {
428 // Wrap the parameterless task factory in a lambda that ignores the TaskContext.
429 this.Load((Func<TaskContext<TProgress>, Task>)(_ => task()), notifyCommands);
430 }
431
432 public void Load<TArg>(Func<TArg, Task> task, TArg arg, params IRelayCommand[] notifyCommands)
433 {
434 this.Load((Func<TaskContext<TProgress>, Task>)(_ => task(arg)), notifyCommands);
435 }
436
440 public void Run() => this.StartNewTask(isRefresh: false);
441
445 public void RunRefresh() => this.StartNewTask(isRefresh: true);
446
451 public void Run<TArg>(Func<TArg, TaskContext<TProgress>, Task> task, TArg arg)
452 {
453 lock (this.syncObject)
454 {
455 this.taskFactory = ctx => task(arg, ctx);
456 }
457 this.StartNewTask(isRefresh: false);
458 }
459
463 public void RunRefresh<TArg>(Func<TArg, TaskContext<TProgress>, Task> task, TArg arg)
464 {
465 lock (this.syncObject)
466 {
467 this.taskFactory = ctx => task(arg, ctx);
468 }
469 this.StartNewTask(isRefresh: true);
470 }
471
476 [RelayCommand(CanExecute = nameof(IsNotPending))]
477 public void Reload()
478 {
479 if (this.IsPending)
480 return;
481
482 this.StartNewTask(isRefresh: false);
483 }
484
489 [RelayCommand(CanExecute = nameof(IsNotPending))]
490 public void Refresh()
491 {
492 if (this.IsPending)
493 return;
494
495 this.StartNewTask(isRefresh: true);
496 }
497
501 [RelayCommand(CanExecute = nameof(IsRunning))]
502 public void Cancel()
503 {
504 if (!this.IsRunning)
505 return;
506
507 lock (this.syncObject)
508 {
509 try
510 {
511 this.CancellationTokenSource?.Cancel();
512 }
513 catch
514 {
515 // Ignored.
516 }
517 }
518 }
519
520 private bool disposed;
521 // Public Dispose: callers clean up managed + unmanaged
522 public void Dispose()
523 {
524 this.Dispose(disposing: true);
525 GC.SuppressFinalize(this);
526 }
527
528 // Finalizer only if you manage native resources.
529 ~ObservableTask()
530 {
531 this.Dispose(disposing: false);
532 }
533
534 // The core pattern
535 protected virtual void Dispose(bool disposing)
536 {
537 if (this.disposed)
538 return;
539 this.disposed = true;
540
541 if (disposing)
542 {
543 lock (this.syncObject)
544 {
545 try { this.CancellationTokenSource?.Cancel(); }
546 catch { /* ignore */ }
547
548 this.CancellationTokenSource?.Dispose();
549 this.CancellationTokenSource = null;
550
551 // Dispose any attached disposables (e.g., policies)
552 foreach (var d in this.attachedDisposables)
553 {
554 try { d.Dispose(); }
555 catch { /* ignore */ }
556 }
557 this.attachedDisposables.Clear();
558 }
559 }
560 }
561
562 internal void AttachDisposable(IDisposable disposable)
563 {
564 if (disposable is null)
565 return;
566 lock (this.syncObject)
567 {
568 this.attachedDisposables.Add(disposable);
569 }
570 }
571
572 }
573
574 public partial class ObservableTask : ObservableTask<int>
575 {
576 }
577
578 public sealed class ObservableTask<TResult, TProgress> : ObservableTask<TProgress>
579 {
580 public TResult? Result { get; private set; }
581
585 public void Load(Func<TaskContext<TProgress>, Task<TResult>> op, params IRelayCommand[] notify)
586 {
587 base.Load(async ctx =>
588 {
589 this.Result = await op(ctx);
590 }, notify);
591 }
592
596 public void Configure(Func<TaskContext<TProgress>, Task<TResult>> op, params IRelayCommand[] notify)
597 => base.Configure(async ctx => { this.Result = await op(ctx); }, notify);
598
602 public void Configure<TArg>(Func<TArg, TaskContext<TProgress>, Task<TResult>> op, TArg arg, params IRelayCommand[] notify)
603 => base.Configure(async ctx => { this.Result = await op(arg, ctx); }, notify);
604
608 public void Load<TArg>(Func<TArg, TaskContext<TProgress>, Task<TResult>> op, TArg arg, params IRelayCommand[] notify)
609 {
610 base.Load(async ctx => { this.Result = await op(arg, ctx); }, notify);
611 }
612
616 public void Run<TArg>(Func<TArg, TaskContext<TProgress>, Task<TResult>> op, TArg arg)
617 {
618 base.Run<TArg>(async (a, ctx) => { this.Result = await op(a, ctx); }, arg);
619 }
620
624 public void RunRefresh<TArg>(Func<TArg, TaskContext<TProgress>, Task<TResult>> op, TArg arg)
625 {
626 base.RunRefresh<TArg>(async (a, ctx) => { this.Result = await op(a, ctx); }, arg);
627 }
628 }
629
630}
Generic delay intervals
Definition: Constants.cs:651
static readonly TimeSpan Default
Default delay interval if waiting for something
Definition: Constants.cs:655
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
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....
Definition: ImplTypes.g.cs:58
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.
Definition: TaskContext.cs:9