#region CPL License /* Nuclex Framework Copyright (C) 2002-2019 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion 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 = getMainWindow(); } 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 = getMainWindow(); } 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 }); } /// Finds the application's main window /// Main window of the application private static Form getMainWindow() { IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; // We can get two things: a list of all open windows and the handle of // the window that the process has registered as main window. Use the latter // to pick the correct window from the former. FormCollection openForms = Application.OpenForms; int openFormCount = openForms.Count; for(int index = 0; index < openFormCount; ++index) { if(openForms[index].IsHandleCreated) { if(openForms[index].Handle == mainWindowHandle) { return openForms[index]; } } } // No matching main window found: use the first one in good faith or fail. if(openFormCount > 0) { return openForms[0]; } else { return null; } } /// 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