#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support.Threading;
// Possible problem:
//
// After Run() is called, the action may not actually run if
// it is using another thread runner and that one is cancelled.
//
// Thus, a second call to Run() has to schedule the action again,
// even if it might already be scheduled, but should also not execute
// the action a second time if is was indeed still scheduled.
namespace Nuclex.Windows.Forms.ViewModels {
/// Encapsulates an action that can run in a thread
///
///
/// Sometimes a view model wants to allow multiple actions to take place
/// at the same time. Think multiple panels on the view require updating
/// from a web service - you can make both requests at the same time
/// instead of sequentially.
///
///
/// This class is also of use for things that need to be done sequentially
/// by sharing the thread runner of the threaded view model. That way,
/// you still have cancellable actions you can run at will and they
/// automatically queue themselves to be executed one after another.
///
///
public abstract class ThreadedAction : Observable, IDisposable {
#region class ThreadedActionThreadRunner
/// Thread runner for the threaded action
private class ThreadedActionThreadRunner : ThreadRunner {
/// Initializes a new thread runner for the threaded view model
public ThreadedActionThreadRunner(ThreadedAction viewModel) {
this.threadedAction = viewModel;
}
/// Reports an error
/// Error that will be reported
protected override void ReportError(Exception exception) {
this.threadedAction.reportErrorFromThread(exception);
}
/// Called when the status of the busy flag changes
protected override void BusyChanged() {
// Narf. Can't use this.
}
/// View model the thread runner belongs to
private ThreadedAction threadedAction;
}
#endregion // class ThreadedActionThreadRunner
/// Initializes all common fields of the instance
private ThreadedAction() {
this.callRunIfNotCancelledDelegate = new Action(
callThreadedExecuteIfNotCancelled
);
this.reportErrorDelegate = new Action(ReportError);
}
/// Initializes a threaded action that uses its own thread runner
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
}
///
/// Initializes a threaded action that uses the view model's thread runner
///
/// View model whose thread runner will be used
///
/// UI dispatcher that can be used to run callbacks in the UI thread
///
public ThreadedAction(
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
) : this() {
if(uiContext == null) {
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
if(this.uiContext == null) {
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
}
} else {
this.uiContext = uiContext;
}
this.externalThreadRunner = viewModel.ThreadRunner;
}
/// Immediately releases all resources owned by the instance
public virtual void Dispose() {
if(this.isBusy) {
Cancel();
}
if(this.ownThreadRunner != null) {
this.ownThreadRunner.Dispose();
this.ownThreadRunner = null;
}
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Dispose();
this.currentCancellationTokenSource = null;
}
}
/// Whether the view model is currently busy executing a task
public bool IsBusy {
get { return this.isBusy; }
private set {
if(value != this.isBusy) {
this.isBusy = value;
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// Cancels the running background task, if any
public void Cancel() {
lock(this.runningTaskSyncRoot) {
// If the background task is not running, do nothing. This also allows
// us to avoid needless recreation of the same cancellation token source.
if(!this.isBusy) {
return;
}
// If a task is currently running, cancel it
if(this.isRunning) {
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
this.currentCancellationTokenSource = null;
}
}
// If the task was scheduled to be repeated, we also have to mark
// the upcoming cancellation token source as canceled because the scheduled
// run will still be happening (it will just cancel out immediately).
if(this.nextCancellationTokenSource != null) {
this.nextCancellationTokenSource.Cancel();
this.nextCancellationTokenSource = null;
}
this.isScheduledAgain = false;
// If the task was not running, we can clear the busy state because it
// is not going to reach the running state.
if(!this.isRunning) {
this.isBusy = false;
}
}
}
///
/// Starts the task, cancelling the running task before doing so
///
public void Restart() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
if(this.currentCancellationTokenSource != null) {
this.currentCancellationTokenSource.Cancel();
}
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
this.isScheduledAgain = false;
}
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// Starts the task
public void Start() {
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// after this one is finished (because now, data might have changed after
// execution has finished).
if(this.isRunning) {
// If we already created a new cancellation token source, keep it,
// otherwise create a new one for the next execution
if(!this.isScheduledAgain) {
this.nextCancellationTokenSource = new CancellationTokenSource();
this.isScheduledAgain = true;
}
} else {
// If there's no cancellation token source, create one. If an execution
// was already scheduled and the cancellation token source is still valid,
// then reuse that in order to be able to cancel all scheduled executions.
if(this.currentCancellationTokenSource == null) {
this.currentCancellationTokenSource = new CancellationTokenSource();
}
// Schedule another execution of the action
scheduleExecution();
}
reportBusyChange = (this.isBusy == false);
this.isBusy = true;
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
/// Reports an error
/// Error that will be reported
protected abstract void ReportError(Exception exception);
/// Executes the threaded action from the background thread
/// Token by which execution can be canceled
protected abstract void Run(CancellationToken cancellationToken);
///
/// Calls the Run() method from the background thread and manages the flags
///
///
private void callThreadedExecuteIfNotCancelled(
CancellationTokenSource cancellationTokenSource
) {
lock(this) {
if(cancellationTokenSource.Token.IsCancellationRequested) {
return;
}
this.isRunning = true;
}
try {
Run(cancellationTokenSource.Token);
}
finally {
bool reportBusyChange = false;
lock(this) {
this.isRunning = false;
// Cancel the current cancellation token because this execution may have
// been scheduled multiple times (there's no way for the Run() method to
// know if the currently scheduled execution was cancelled, so it is forced
// to reschedule on each call - accepting redundant schedules).
cancellationTokenSource.Cancel();
// Pick the next cancellation token source. Normally it is null, but this
// is more elegant because we can avoid an while if statement this way :)
this.currentCancellationTokenSource = nextCancellationTokenSource;
this.nextCancellationTokenSource = null;
// If Start() was called while we were executing, another execution is required
// (because the data may have changed during the call to Start()).
if(this.isScheduledAgain) {
this.isScheduledAgain = false;
scheduleExecution();
} else { // We're idle now
reportBusyChange = (this.isBusy == true);
this.isBusy = false;
}
}
if(reportBusyChange) {
OnPropertyChanged(nameof(IsBusy));
}
}
}
/// Schedules one execution of the action
private void scheduleExecution() {
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
ThreadRunner runner = this.externalThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
runner = this.ownThreadRunner;
if(runner != null) {
runner.RunInBackground(
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
);
}
}
/// Reports an error that occurred in the runner's background thread
/// Exception that the thread has encountered
private void reportErrorFromThread(Exception exception) {
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
}
/// Sets the UI context that will be used by the threaded action
/// The UI context the threaded action will use
private void updateUiContext(ISynchronizeInvoke uiContext) {
this.uiContext = uiContext;
}
/// Synchronization context of the thread in which the view runs
private ISynchronizeInvoke uiContext;
/// Delegate for the ReportError() method
private Action reportErrorDelegate;
/// Delegate for the callThreadedExecuteIfNotCancelled() method
private Action callRunIfNotCancelledDelegate;
/// Thread runner on which the action can run its background task
private ThreadedActionThreadRunner ownThreadRunner;
///
/// External thread runner on which the action runs its background task if assigned
///
private ThreadRunner externalThreadRunner;
/// Synchronization root for the threaded execute method
private object runningTaskSyncRoot = new object();
/// Used to cancel the currently running task
private CancellationTokenSource currentCancellationTokenSource;
/// Used to cancel the upcoming task if a re-run was scheduled
private CancellationTokenSource nextCancellationTokenSource;
/// Whether the background task is running or waiting to run
private volatile bool isBusy;
/// Whether execution is taking place right now
///
/// If this flag is set and the Start() method is called, another run needs to
/// be scheduled.
///
private bool isRunning;
/// Whether run was called while the action was already running
private bool isScheduledAgain;
}
} // namespace Nuclex.Windows.Forms.ViewModels