#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.Threading; using System.Windows.Forms; namespace Nuclex.Windows.Forms { /// Progress bar with optimized multi-threading behavior /// /// /// If a background thread is generating lots of progress updates, using synchronized /// calls can drastically reduce performance. This progress bar optimizes that case /// by performing the update asynchronously and keeping only the most recent update /// when multiple updates arrive while the asynchronous update call is still running. /// /// /// This design eliminates useless queueing of progress updates, thereby reducing /// CPU load occuring in the UI thread and at the same time avoids blocking the /// worker thread, increasing its performance. /// /// public partial class AsyncProgressBar : ProgressBar { /// Initializes a new asynchronous progress bar public AsyncProgressBar() { InitializeComponent(); this.Disposed += new EventHandler(progressBarDisposed); this.updateProgressDelegate = new MethodInvoker(updateProgress); // Could probably use VolatileWrite() as well, but for consistency reasons // this is an Interlocked call, too. Mixing different synchronization measures // for a variable raises a red flag whenever I see it :) Interlocked.Exchange(ref this.newProgress, -1.0f); } /// Called when the progress bar is being disposed /// Progress bar that is being disposed /// Not used private void progressBarDisposed(object sender, EventArgs arguments) { // CHECK: This method is only called on an explicit Dispose() of the control. // It is legal to call Control.BeginInvoke() without calling Control.EndInvoke(), // so the code is quite correct even if no Dispose() occurs, but is it also clean? // http://www.interact-sw.co.uk/iangblog/2005/05/16/endinvokerequired // Since this has to occur in the UI thread, there's no way that updateProgress() // could be executing just now. But the final call to updateProgress() will not // have EndInvoke() called on it yet, so we do this here before the control // is finally disposed. if(this.progressUpdateAsyncResult != null) { EndInvoke(this.progressUpdateAsyncResult); this.progressUpdateAsyncResult = null; } } /// Asynchronously updates the value to be shown in the progress bar /// New value to set the progress bar to /// /// This will schedule an asynchronous update of the progress bar in the UI thread. /// If you change the progress value again before the progress bar has completed its /// update cycle, the original progress value will be skipped and the progress bar /// jumps directly to the latest progress value. Updates are not queued, there is /// at most one update waiting on the UI thread. It is also strictly guaranteed that /// the last most progress value set will be shown and never skipped. /// public void AsyncSetValue(float value) { // Update the value to be shown on the progress bar. If this happens multiple // times, that's not a problem, the progress bar updates as fast as it can // and always tries to show the most recent value assigned. float oldValue = Interlocked.Exchange(ref this.newProgress, value); // If the previous value was -1, the UI thread has already taken out the most recent // value and assigned it (or is about to assign it) to the progress bar control. // In this case, we'll wait until the current update has completed and immediately // begin the next update - since we know that the value the UI thread has extracted // is no longer the most recent one. if(oldValue == -1.0f) { if(this.progressUpdateAsyncResult != null) { EndInvoke(this.progressUpdateAsyncResult); } this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate); } } /// Synchronously updates the value visualized in the progress bar private void updateProgress() { // Cache these to shorten the code that follows :) int minimum = base.Minimum; int maximum = base.Maximum; // Take out the most recent value that has been given to the asynchronous progress // bar up until now and replace it by -1. This enables the updater to see when // the update has actually been performed and whether it needs to start a new // invocation to ensure the most recent value will remain at the end. float progress = Interlocked.Exchange(ref this.newProgress, -1.0f); // Restrain the value to the progress bar's configured range and assign it. // This is done to prevent exceptions in the UI thread (theoretically the user // could change the progress bar's min and max just before the UI thread executes // this method, so we cannot validate the value in AsyncSetValue()) int value = (int)(progress * (maximum - minimum)) + minimum; base.Value = Math.Min(Math.Max(value, minimum), maximum); } /// New progress being assigned to the progress bar private float newProgress; /// Delegate for the progress update method private MethodInvoker updateProgressDelegate; /// Async result for the invoked control state update method private volatile IAsyncResult progressUpdateAsyncResult; } } // namespace Nuclex.Windows.Forms