diff --git a/Nuclex.Support (x86).csproj b/Nuclex.Support (x86).csproj index ea47a40..da7467a 100644 --- a/Nuclex.Support (x86).csproj +++ b/Nuclex.Support (x86).csproj @@ -145,20 +145,20 @@ - - + + - - - ProgressionTracker.cs + + + ProgressTracker.cs - ProgressionTracker.cs + SetProgression.cs diff --git a/Source/Scheduling/QueueOperation.cs b/Source/Scheduling/QueueOperation.cs index 1cba412..18279c3 100644 --- a/Source/Scheduling/QueueOperation.cs +++ b/Source/Scheduling/QueueOperation.cs @@ -45,7 +45,7 @@ namespace Nuclex.Support.Scheduling { // Construct a WeightedProgression with the default weight for each // progression and wrap it in an ObservedProgression foreach(OperationType operation in childs) - this.children.Add(new WeightedProgression(operation)); + this.children.Add(new WeightedWaitable(operation)); // Since all progressions have a weight of 1.0, the total weight is // equal to the number of progressions in our list @@ -55,10 +55,10 @@ namespace Nuclex.Support.Scheduling { /// Initializes a new queue operation with custom weights /// Child operations to execute in this operation - public QueueOperation(IEnumerable> childs) : this() { + public QueueOperation(IEnumerable> childs) : this() { // Construct an ObservedProgression around each of the WeightedProgressions - foreach(WeightedProgression operation in childs) { + foreach(WeightedWaitable operation in childs) { this.children.Add(operation); // Sum up the total weight @@ -74,11 +74,11 @@ namespace Nuclex.Support.Scheduling { asyncOperationProgressChanged ); - this.children = new List>(); + this.children = new List>(); } /// Provides access to the child operations of this queue - public IList> Children { + public IList> Children { get { return this.children; } } @@ -125,7 +125,7 @@ namespace Nuclex.Support.Scheduling { /// and launches the operation by calling its Begin() method. /// private void startCurrentOperation() { - OperationType operation = this.children[this.currentOperationIndex].Progression; + OperationType operation = this.children[this.currentOperationIndex].Waitable; operation.AsyncEnded += this.asyncOperationEndedDelegate; @@ -143,7 +143,7 @@ namespace Nuclex.Support.Scheduling { /// counts up the accumulated progress of the queue. /// private void endCurrentOperation() { - OperationType operation = this.children[this.currentOperationIndex].Progression; + OperationType operation = this.children[this.currentOperationIndex].Waitable; // Disconnect from the operation's events operation.AsyncEnded -= this.asyncOperationEndedDelegate; @@ -217,7 +217,7 @@ namespace Nuclex.Support.Scheduling { /// Delegate to the asyncOperationProgressUpdated() method private EventHandler asyncOperationProgressChangedDelegate; /// Operations being managed in the queue - private List> children; + private List> children; /// Summed weight of all operations in the queue private float totalWeight; /// Accumulated weight of the operations already completed diff --git a/Source/Tracking/Internal/ObservedWeightedProgression.cs b/Source/Tracking/Internal/ObservedWeightedWaitable.cs similarity index 88% rename from Source/Tracking/Internal/ObservedWeightedProgression.cs rename to Source/Tracking/Internal/ObservedWeightedWaitable.cs index 0c7c8de..0020b4a 100644 --- a/Source/Tracking/Internal/ObservedWeightedProgression.cs +++ b/Source/Tracking/Internal/ObservedWeightedWaitable.cs @@ -27,7 +27,7 @@ namespace Nuclex.Support.Tracking { /// /// Type of the progression that is being observed /// - internal class ObservedWeightedProgression : IDisposable + internal class ObservedWeightedWaitable : IDisposable where ProgressionType : Waitable { /// Delegate for reporting progress updates @@ -41,15 +41,15 @@ namespace Nuclex.Support.Tracking { /// /// Callback to invoke when the progression has ended /// - internal ObservedWeightedProgression( - WeightedProgression weightedProgression, + internal ObservedWeightedWaitable( + WeightedWaitable weightedProgression, ReportDelegate progressUpdateCallback, ReportDelegate endedCallback ) { this.weightedProgression = weightedProgression; // See if this progression has already ended (initial check for performance) - if(weightedProgression.Progression.Ended) { + if(weightedProgression.Waitable.Ended) { this.progress = 1.0f; @@ -58,7 +58,7 @@ namespace Nuclex.Support.Tracking { this.endedCallback = endedCallback; this.progressUpdateCallback = progressUpdateCallback; - this.weightedProgression.Progression.AsyncEnded += + this.weightedProgression.Waitable.AsyncEnded += new EventHandler(asyncEnded); // Check whether this progression might have ended before we were able to @@ -66,10 +66,10 @@ namespace Nuclex.Support.Tracking { // other event and (important) set our progress to 1.0 because, since we // might not have gotten the 'Ended' event, it might otherwise stay at 0.0 // even though the progression is in the 'Ended' state. - if(weightedProgression.Progression.Ended) { + if(weightedProgression.Waitable.Ended) { this.progress = 1.0f; } else { - this.progressReporter = this.weightedProgression.Progression as IProgressReporter; + this.progressReporter = this.weightedProgression.Waitable as IProgressReporter; if(this.progressReporter != null) { this.asyncProgressChangedEventHandler = new EventHandler( @@ -90,7 +90,7 @@ namespace Nuclex.Support.Tracking { } /// Weighted progression being observed - public WeightedProgression WeightedProgression { + public WeightedWaitable WeightedWaitable { get { return this.weightedProgression; } } @@ -143,7 +143,7 @@ namespace Nuclex.Support.Tracking { // is no risk of deadlock involved, so we don't need a fancy syncRoot! lock(this) { if(this.endedCallback != null) { - this.weightedProgression.Progression.AsyncEnded -= + this.weightedProgression.Waitable.AsyncEnded -= new EventHandler(asyncEnded); if(this.progressReporter != null) { @@ -166,7 +166,7 @@ namespace Nuclex.Support.Tracking { /// The observed progression's progress reporting interface private IProgressReporter progressReporter; /// The weighted progression that is being observed - private WeightedProgression weightedProgression; + private WeightedWaitable weightedProgression; /// Callback to invoke when the progress updates private volatile ReportDelegate progressUpdateCallback; /// Callback to invoke when the progression ends diff --git a/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs b/Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs similarity index 88% rename from Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs rename to Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs index e67163d..b01c183 100644 --- a/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs +++ b/Source/Tracking/Internal/WeightedWaitableWrapperCollection.cs @@ -49,14 +49,14 @@ namespace Nuclex.Support.Tracking { /// internal class WeightedProgressionWrapperCollection : TransformingReadOnlyCollection< - ObservedWeightedProgression, WeightedProgression + ObservedWeightedWaitable, WeightedWaitable > where ProgressionType : Waitable { /// Initializes a new weighted progression collection wrapper /// Items to be exposed as weighted progressions internal WeightedProgressionWrapperCollection( - IList> items + IList> items ) : base(items) { } @@ -69,10 +69,10 @@ namespace Nuclex.Support.Tracking { /// be called frequently, because the TransformingReadOnlyCollection does /// not cache otherwise store the transformed items. /// - protected override WeightedProgression Transform( - ObservedWeightedProgression item + protected override WeightedWaitable Transform( + ObservedWeightedWaitable item ) { - return item.WeightedProgression; + return item.WeightedWaitable; } } diff --git a/Source/Tracking/ProgressionTracker.Test.cs b/Source/Tracking/ProgressTracker.Test.cs similarity index 89% rename from Source/Tracking/ProgressionTracker.Test.cs rename to Source/Tracking/ProgressTracker.Test.cs index adc8cf8..aacaf66 100644 --- a/Source/Tracking/ProgressionTracker.Test.cs +++ b/Source/Tracking/ProgressTracker.Test.cs @@ -148,7 +148,7 @@ namespace Nuclex.Support.Tracking { /// Validates that the tracker properly sums the progress [Test] public void TestSummedProgress() { - ProgressionTracker tracker = new ProgressionTracker(); + ProgressTracker tracker = new ProgressTracker(); IProgressionTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); @@ -160,7 +160,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.0f)) } ); @@ -174,7 +174,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f)) } ); @@ -194,7 +194,7 @@ namespace Nuclex.Support.Tracking { /// [Test] public void TestDelayedRemoval() { - ProgressionTracker tracker = new ProgressionTracker(); + ProgressTracker tracker = new ProgressTracker(); IProgressionTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); @@ -206,7 +206,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.0f)) } ); @@ -220,7 +220,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f)) } ); @@ -231,7 +231,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.75f)) } ); @@ -245,7 +245,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(1.0f)) } ); @@ -265,7 +265,7 @@ namespace Nuclex.Support.Tracking { /// [Test] public void TestSoleEndedProgression() { - ProgressionTracker tracker = new ProgressionTracker(); + ProgressTracker tracker = new ProgressTracker(); IProgressionTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); @@ -280,7 +280,7 @@ namespace Nuclex.Support.Tracking { /// [Test] public void TestEndedProgression() { - ProgressionTracker tracker = new ProgressionTracker(); + ProgressTracker tracker = new ProgressTracker(); IProgressionTrackerSubscriber mockedSubscriber = mockSubscriber(tracker); @@ -292,7 +292,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.0f)) } ); @@ -304,7 +304,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f)) } ); @@ -315,7 +315,7 @@ namespace Nuclex.Support.Tracking { Method("ProgressUpdated"). With( new Matcher[] { - new NMock2.Matchers.TypeMatcher(typeof(ProgressionTracker)), + new NMock2.Matchers.TypeMatcher(typeof(ProgressTracker)), new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(1.0f)) } ); @@ -335,7 +335,7 @@ namespace Nuclex.Support.Tracking { /// [Test] public void TestProvokedDeadlock() { - ProgressionTracker tracker = new ProgressionTracker(); + ProgressTracker tracker = new ProgressTracker(); TestProgression test1 = new TestProgression(); tracker.Track(test1); @@ -351,14 +351,14 @@ namespace Nuclex.Support.Tracking { /// Mocks a subscriber for the events of a tracker /// Tracker to mock an event subscriber for /// The mocked event subscriber - private IProgressionTrackerSubscriber mockSubscriber(ProgressionTracker tracker) { + private IProgressionTrackerSubscriber mockSubscriber(ProgressTracker tracker) { IProgressionTrackerSubscriber mockedSubscriber = this.mockery.NewMock(); tracker.AsyncIdleStateChanged += new EventHandler(mockedSubscriber.IdleStateChanged); - tracker.AsyncProgressUpdated += + tracker.AsyncProgressChanged += new EventHandler(mockedSubscriber.ProgressUpdated); return mockedSubscriber; diff --git a/Source/Tracking/ProgressTracker.cs b/Source/Tracking/ProgressTracker.cs new file mode 100644 index 0000000..b28a917 --- /dev/null +++ b/Source/Tracking/ProgressTracker.cs @@ -0,0 +1,364 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2008 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.Collections.Generic; +using System.Threading; + +namespace Nuclex.Support.Tracking { + + /// + /// Helps tracking the progress of one or more waitable background processes + /// + /// + /// + /// This is useful if you want to display a progress bar for multiple + /// Waitables but can not guarantee that no additional Waitables + /// will appear inmidst of execution. + /// + /// + /// This class does not implement the interface itself + /// in order to not violate the design principles of Waitables which + /// guarantee that a will only finish once (whereas the + /// progress tracker might 'finish' any number of times). + /// + /// + public class ProgressTracker : IDisposable, IProgressReporter { + + #region class WaitableMatcher + + /// Matches a direct Waitable to a fully wrapped one + private class WaitableMatcher { + + /// + /// Initializes a new Waitable matcher that matches against + /// the specified Waitable + /// + /// Waitable to match against + public WaitableMatcher(Waitable toMatch) { + this.toMatch = toMatch; + } + + /// + /// Checks whether the provided Waitable matches the comparison + /// Waitable of the instance + /// + /// Waitable to match to the comparison Waitable + public bool Matches(ObservedWeightedWaitable other) { + return ReferenceEquals(other.WeightedWaitable.Waitable, this.toMatch); + } + + /// Waitable this instance compares against + private Waitable toMatch; + + } + + #endregion // class WaitableMatcher + + /// Triggered when the idle state of the tracker changes + /// + /// The tracker is idle when no Waitables are being tracked in it. If you're + /// using this class to feed a progress bar, this would be the event to use for + /// showing or hiding the progress bar. The tracker starts off as idle because, + /// upon construction, its list of Waitables will be empty. + /// + public event EventHandler AsyncIdleStateChanged; + + /// Triggered when the total progress has changed + public event EventHandler AsyncProgressChanged; + + /// Initializes a new Waitable tracker + public ProgressTracker() { + + this.trackedWaitables = new List>(); + this.idle = true; + + this.asyncEndedDelegate = + new ObservedWeightedWaitable.ReportDelegate(asyncEnded); + this.asyncProgressUpdatedDelegate = + new ObservedWeightedWaitable.ReportDelegate(asyncProgressChanged); + + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + lock(this.trackedWaitables) { + + // Get rid of all Waitables we're tracking. This unsubscribes the + // observers from the events of the Waitables and stops us from + // being kept alive and receiving any further events if some of the + // tracked Waitables are still executing. + for(int index = 0; index < this.trackedWaitables.Count; ++index) + this.trackedWaitables[index].Dispose(); + + // Help the GC a bit by untangling the references :) + this.trackedWaitables.Clear(); + this.trackedWaitables = null; + + } // lock + } + + /// Begins tracking the specified waitable background process + /// Waitable background process to be tracked + public void Track(Waitable waitable) { + Track(waitable, 1.0f); + } + + /// Begins tracking the specified waitable background process + /// Waitable background process to be tracked + /// Weight to assign to this waitable background process + public void Track(Waitable waitable, float weight) { + + // Add the new Waitable into the tracking list. This has to be done + // inside a lock to prevent issues with the progressUpdate callback, which could + // access the totalWeight field before it has been updated to reflect the + // new Waitable added to the collection. + lock(this.trackedWaitables) { + + bool wasEmpty = (this.trackedWaitables.Count == 0); + + // This can be done after we registered the wrapper to our delegates because + // any incoming progress updates will be stopped from the danger of a + // division-by-zero from the potentially still zeroed totalWeight by the lock. + this.totalWeight += weight; + + if(waitable.Ended) { + + // If the ended Waitable would become the only Waitable in the list, + // there's no sense in doing anything at all because it would have to be + // thrown right out again. Only add the Waitable when there are other + // running Waitables to properly sum total progress for consistency. + if(!wasEmpty) { + + // Construct a new observation wrapper. This is done inside the lock + // because as soon as we are subscribed to the events, we can potentially + // receive them. The lock eliminates the risk of processing a progress update + // before the Waitable has been added to the tracked Waitables list. + this.trackedWaitables.Add( + new ObservedWeightedWaitable( + new WeightedWaitable(waitable, weight), + this.asyncProgressUpdatedDelegate, + this.asyncEndedDelegate + ) + ); + + // All done, the total progress is different now, so force a recalculation and + // send out the AsyncProgressUpdated event. + recalculateProgress(); + + } + + } else { // Not ended -- Waitable is still running + + // Construct a new Waitable observer and add the Waitable to our + // list of tracked Waitables. + ObservedWeightedWaitable observedWaitable = + new ObservedWeightedWaitable( + new WeightedWaitable(waitable, weight), + this.asyncProgressUpdatedDelegate, + this.asyncEndedDelegate + ); + + this.trackedWaitables.Add(observedWaitable); + + // If this is the first Waitable to be added to the list, tell our + // owner that we're idle no longer! + if(wasEmpty) + setIdle(false); + + // All done, the total progress is different now, so force a recalculation and + // send out the AsyncProgressUpdated event. + recalculateProgress(); + + // The Waitable might have ended before we had registered to its AsyncEnded + // event, so we have to do this to be on the safe side. This might cause + // asyncEnded() to be called twice, but that's not a problem in this + // implementation and improves performance and simplicity for the normal path. + if(waitable.Ended) { + asyncEnded(); + observedWaitable.Dispose(); + } + + } // if Waitable ended + + } // lock + + } + + /// Stops tracking the specified waitable background process + /// Waitable background process to stop tracking of + public void Untrack(Waitable waitable) { + lock(this.trackedWaitables) { + + // Locate the object to be untracked in our collection + int removeIndex = this.trackedWaitables.FindIndex( + new Predicate>( + new WaitableMatcher(waitable).Matches + ) + ); + if(removeIndex == -1) + throw new InvalidOperationException("Item is not being tracked"); + + // Remove and dispose the Waitable the user wants to untrack + { + ObservedWeightedWaitable wrappedWaitable = + this.trackedWaitables[removeIndex]; + + this.trackedWaitables.RemoveAt(removeIndex); + wrappedWaitable.Dispose(); + } + + // If the list is empty, then we're back in the idle state + if(this.trackedWaitables.Count == 0) { + + this.totalWeight = 0.0f; + + // If we entered the idle state with this call, report the state change! + setIdle(true); + + } else { + + // Rebuild the total weight from scratch. Subtracting the removed Waitable's + // weight would work, too, but we might accumulate rounding errors making the sum + // drift slowly away from the actual value. + this.totalWeight = 0.0f; + for(int index = 0; index < this.trackedWaitables.Count; ++index) + this.totalWeight += this.trackedWaitables[index].WeightedWaitable.Weight; + + } + + } // lock + } + + /// Whether the tracker is currently idle + public bool Idle { + get { return this.idle; } + } + + /// Current summed progress of the tracked Waitables + public float Progress { + get { return this.progress; } + } + + /// Fires the AsyncIdleStateChanged event + /// New idle state to report + protected virtual void OnAsyncIdleStateChanged(bool idle) { + EventHandler copy = AsyncIdleStateChanged; + if(copy != null) + copy(this, new IdleStateEventArgs(idle)); + } + + /// Fires the AsyncProgressUpdated event + /// New progress to report + protected virtual void OnAsyncProgressUpdated(float progress) { + EventHandler copy = AsyncProgressChanged; + if(copy != null) + copy(this, new ProgressReportEventArgs(progress)); + } + + /// Recalculates the total progress of the tracker + private void recalculateProgress() { + float totalProgress = 0.0f; + + // Lock the collection to avoid trouble when someone tries to remove one + // of our tracked Waitables while we're just doing a progress update + lock(this.trackedWaitables) { + + // This is a safety measure. In theory, even after all Waitables have + // ended and the collection of tracked Waitables is cleared, a waiting + // thread might deliver another progress update causing this method to + // be entered. In this case, the right thing is to do nothing at all. + if(this.totalWeight == 0.0f) + return; + + // Sum up the total progress + for(int index = 0; index < this.trackedWaitables.Count; ++index) { + float weight = this.trackedWaitables[index].WeightedWaitable.Weight; + totalProgress += this.trackedWaitables[index].Progress * weight; + } + + // This also needs to be in the lock to guarantee that the totalWeight is + // the one for the number of Waitables we just summed -- by design, + // the total weight always has to be updated at the same time as the collection. + totalProgress /= this.totalWeight; + + // Finally, trigger the event + this.progress = totalProgress; + OnAsyncProgressUpdated(totalProgress); + + } // lock + } + + /// Called when one of the tracked Waitables has ended + private void asyncEnded() { + lock(this.trackedWaitables) { + + // If any Waitables in the list are still going, keep the entire list. + // This behavior is intentional in order to prevent the tracker's progress from + // jumping back repeatedly when multiple tracked Waitables come to an end. + for(int index = 0; index < this.trackedWaitables.Count; ++index) + if(!this.trackedWaitables[index].WeightedWaitable.Waitable.Ended) + return; + + // All Waitables have finished, get rid of the wrappers and make a + // fresh start for future Waitables to be tracked. No need to call + // Dispose() since, as a matter of fact, when the Waitable + this.trackedWaitables.Clear(); + this.totalWeight = 0.0f; + + // Notify our owner that we're idle now. This line is only reached when all + // Waitables were finished, so it's safe to trigger this here. + setIdle(true); + + } // lock + } + + /// Called when one of the tracked Waitables has achieved progress + private void asyncProgressChanged() { + recalculateProgress(); + } + + /// Changes the idle state + /// Whether or not the tracker is currently idle + /// + /// This method expects to be called during a lock() on trackedWaitables! + /// + private void setIdle(bool idle) { + this.idle = idle; + + OnAsyncIdleStateChanged(idle); + } + + /// Whether the tracker is currently idle + private volatile bool idle; + /// Current summed progress of the tracked Waitables + private volatile float progress; + /// Total weight of all Waitables being tracked + private volatile float totalWeight; + /// Waitables being tracked by this tracker + private List> trackedWaitables; + /// Delegate for the asyncEnded() method + private ObservedWeightedWaitable.ReportDelegate asyncEndedDelegate; + /// Delegate for the asyncProgressUpdated() method + private ObservedWeightedWaitable.ReportDelegate asyncProgressUpdatedDelegate; + + } + +} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/ProgressionTracker.cs b/Source/Tracking/ProgressionTracker.cs deleted file mode 100644 index 441cefc..0000000 --- a/Source/Tracking/ProgressionTracker.cs +++ /dev/null @@ -1,362 +0,0 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2008 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.Collections.Generic; -using System.Threading; - -namespace Nuclex.Support.Tracking { - - /// Helps tracking the progress of one or more progressions - /// - /// - /// This is useful if you want to display a progress bar for multiple - /// progressions but can not guarantee that no additional progressions - /// will appear inmidst of execution. - /// - /// - /// This class does not implement the IProgression interface itself in - /// order to not violate the design principles of progressions which - /// guarantee that a progression will only finish once (whereas the - /// progression tracker might 'finish' any number of times). - /// - /// - public class ProgressionTracker : IDisposable { - - #region class ProgressionMatcher - - /// Matches a progression to a fully wrapped one - private class ProgressionMatcher { - - /// - /// Initializes a new progression matcher that matches against - /// the specified progression - /// - /// Progression to match against - public ProgressionMatcher(Waitable toMatch) { - this.toMatch = toMatch; - } - - /// - /// Checks whether the provided progression matches the comparison - /// progression of the instance - /// - /// Progression to match to the comparison progression - public bool Matches(ObservedWeightedProgression other) { - return ReferenceEquals(other.WeightedProgression.Progression, this.toMatch); - } - - /// Progression this instance compares against - private Waitable toMatch; - - } - - #endregion // class ProgressionMatcher - - /// Triggered when the idle state of the tracker changes - /// - /// The tracker is idle when no progressions are being tracked in it. If you're - /// using this class to feed a progress bar, this would be the event to use for - /// showing or hiding the progress bar. The tracker starts off as idle because, - /// upon construction, its list of progressions will be empty. - /// - public event EventHandler AsyncIdleStateChanged; - - /// Triggered when the total progress has changed - public event EventHandler AsyncProgressUpdated; - - /// Initializes a new progression tracker - public ProgressionTracker() { - - this.trackedProgressions = new List>(); - this.idle = true; - - this.asyncEndedDelegate = - new ObservedWeightedProgression.ReportDelegate(asyncEnded); - this.asyncProgressUpdatedDelegate = - new ObservedWeightedProgression.ReportDelegate(asyncProgressUpdated); - - } - - /// Immediately releases all resources owned by the instance - public void Dispose() { - lock(this.trackedProgressions) { - - // Get rid of all progression we're tracking. This unsubscribes the - // observers from the events of the progressions and stops us from - // being kept alive and receiving any further events if some of the - // tracked progressions are still executing. - for(int index = 0; index < this.trackedProgressions.Count; ++index) - this.trackedProgressions[index].Dispose(); - - // Help the GC a bit by untangling the references :) - this.trackedProgressions.Clear(); - this.trackedProgressions = null; - - } // lock - } - - /// Begins tracking the specified progression - /// Progression to be tracked - public void Track(Waitable progression) { - Track(progression, 1.0f); - } - - /// Begins tracking the specified progression - /// Progression to be tracked - /// Weight to assign to this progression - public void Track(Waitable progression, float weight) { - - // Add the new progression into the tracking list. This has to be done - // inside a lock to prevent issues with the progressUpdate callback, which could - // access the totalWeight field before it has been updated to reflect the - // new progression added to the collection. - lock(this.trackedProgressions) { - - bool wasEmpty = (this.trackedProgressions.Count == 0); - - // This can be done after we registered the wrapper to our delegates because - // any incoming progress updates will be stopped from the danger of a - // division-by-zero from the potentially still zeroed totalWeight by the lock. - this.totalWeight += weight; - - if(progression.Ended) { - - // If the ended progression would become the only progression in the list, - // there's no sense in doing anything at all because it would have to be - // thrown right out again. Only add the progression when there are other - // running progressions to properly sum total progress for consistency. - if(!wasEmpty) { - - // Construct a new observation wrapper. This is done inside the lock - // because as soon as we are subscribed to the events, we can potentially - // receive them. The lock eliminates the risk of processing a progress update - // before the progression has been added to the tracked progressions list. - this.trackedProgressions.Add( - new ObservedWeightedProgression( - new WeightedProgression(progression, weight), - this.asyncProgressUpdatedDelegate, - this.asyncEndedDelegate - ) - ); - - // All done, the total progress is different now, so force a recalculation and - // send out the AsyncProgressUpdated event. - recalculateProgress(); - - } - - } else { // Not ended -- Progression is still running - - // Construct a new progression observer and add the progression to our - // list of tracked progressions. - ObservedWeightedProgression observedProgression = - new ObservedWeightedProgression( - new WeightedProgression(progression, weight), - this.asyncProgressUpdatedDelegate, - this.asyncEndedDelegate - ); - - this.trackedProgressions.Add(observedProgression); - - // If this is the first progression to be added to the list, tell our - // owner that we're idle no longer! - if(wasEmpty) - setIdle(false); - - // All done, the total progress is different now, so force a recalculation and - // send out the AsyncProgressUpdated event. - recalculateProgress(); - - // The progression might have ended before we had registered to its AsyncEnded - // event, so we have to do this to be on the safe side. This might cause - // asyncEnded() to be called twice, but that's not a problem in this - // implementation and improves performance and simplicity for the normal path. - if(progression.Ended) { - asyncEnded(); - observedProgression.Dispose(); - } - - } // if progression ended - - } // lock - - } - - /// Stops tracking the specified progression - /// Progression to stop tracking of - public void Untrack(Waitable progression) { - lock(this.trackedProgressions) { - - // Locate the object to be untracked in our collection - int removeIndex = this.trackedProgressions.FindIndex( - new Predicate>( - new ProgressionMatcher(progression).Matches - ) - ); - if(removeIndex == -1) - throw new InvalidOperationException("Item is not being tracked"); - - // Remove and dispose the progression the user wants to untrack - { - ObservedWeightedProgression wrappedProgression = - this.trackedProgressions[removeIndex]; - - this.trackedProgressions.RemoveAt(removeIndex); - wrappedProgression.Dispose(); - } - - // If the list is empty, then we're back in the idle state - if(this.trackedProgressions.Count == 0) { - - this.totalWeight = 0.0f; - - // If we entered the idle state with this call, report the state change! - setIdle(true); - - } else { - - // Rebuild the total weight from scratch. Subtracting the removed progression's - // weight would work, too, but we might accumulate rounding errors making the sum - // drift slowly away from the actual value. - this.totalWeight = 0.0f; - for(int index = 0; index < this.trackedProgressions.Count; ++index) - this.totalWeight += this.trackedProgressions[index].WeightedProgression.Weight; - - } - - } // lock - } - - /// Whether the tracker is currently idle - public bool Idle { - get { return this.idle; } - } - - /// Current summed progress of the tracked progressions - public float Progress { - get { return this.progress; } - } - - /// Fires the AsyncIdleStateChanged event - /// New idle state to report - protected virtual void OnAsyncIdleStateChanged(bool idle) { - EventHandler copy = AsyncIdleStateChanged; - if(copy != null) - copy(this, new IdleStateEventArgs(idle)); - } - - /// Fires the AsyncProgressUpdated event - /// New progress to report - protected virtual void OnAsyncProgressUpdated(float progress) { - EventHandler copy = AsyncProgressUpdated; - if(copy != null) - copy(this, new ProgressReportEventArgs(progress)); - } - - /// Recalculates the total progress of the tracker - private void recalculateProgress() { - float totalProgress = 0.0f; - - // Lock the collection to avoid trouble when someone tries to remove one - // of our tracked progressions while we're just doing a progress update - lock(this.trackedProgressions) { - - // This is a safety measure. In theory, even after all progressions have - // ended and the collection of tracked progressions is cleared, a waiting - // thread might deliver another progress update causing this method to - // be entered. In this case, the right thing is to do nothing at all. - if(this.totalWeight == 0.0f) - return; - - // Sum up the total progress - for(int index = 0; index < this.trackedProgressions.Count; ++index) { - float weight = this.trackedProgressions[index].WeightedProgression.Weight; - totalProgress += this.trackedProgressions[index].Progress * weight; - } - - // This also needs to be in the lock to guarantee that the totalWeight is - // the one for the number of progressions we just summed -- by design, - // the total weight always has to be updated at the same time as the collection. - totalProgress /= this.totalWeight; - - // Finally, trigger the event - this.progress = totalProgress; - OnAsyncProgressUpdated(totalProgress); - - } // lock - } - - /// Called when one of the tracked progressions has ended - private void asyncEnded() { - lock(this.trackedProgressions) { - - // If any progressions in the list are still going, keep the entire list. - // This behavior is intentional in order to prevent the tracker's progress from - // jumping back repeatedly when multiple tracked progressions come to an end. - for(int index = 0; index < this.trackedProgressions.Count; ++index) - if(!this.trackedProgressions[index].WeightedProgression.Progression.Ended) - return; - - // All progressions have finished, get rid of the wrappers and make a - // fresh start for future progressions to be tracked. No need to call - // Dispose() since, as a matter of fact, when the progression - this.trackedProgressions.Clear(); - this.totalWeight = 0.0f; - - // Notify our owner that we're idle now. This line is only reached when all - // progressions were finished, so it's safe to trigger this here. - setIdle(true); - - } // lock - } - - /// Called when one of the tracked progression has achieved progress - private void asyncProgressUpdated() { - recalculateProgress(); - } - - /// Changes the idle state - /// Whether or not the tracker is currently idle - /// - /// This method expects to be called during a lock() on trackedProgressions! - /// - private void setIdle(bool idle) { - this.idle = idle; - - OnAsyncIdleStateChanged(idle); - } - - /// Whether the tracker is currently idle - private volatile bool idle; - /// Current summed progress of the tracked progressions - private volatile float progress; - /// Total weight of all progressions being tracked - private volatile float totalWeight; - /// Progressions being tracked by this tracker - private List> trackedProgressions; - /// Delegate for the asyncEnded() method - private ObservedWeightedProgression.ReportDelegate asyncEndedDelegate; - /// Delegate for the asyncProgressUpdated() method - private ObservedWeightedProgression.ReportDelegate asyncProgressUpdatedDelegate; - - } - -} // namespace Nuclex.Support.Tracking diff --git a/Source/Tracking/SetProgression.Test.cs b/Source/Tracking/SetProgression.Test.cs index 097c474..0228dd9 100644 --- a/Source/Tracking/SetProgression.Test.cs +++ b/Source/Tracking/SetProgression.Test.cs @@ -164,7 +164,7 @@ namespace Nuclex.Support.Tracking { } ); - testSetProgression.Children[0].Progression.ChangeProgress(0.5f); + testSetProgression.Children[0].Waitable.ChangeProgress(0.5f); this.mockery.VerifyAllExpectationsHaveBeenMet(); } @@ -174,9 +174,9 @@ namespace Nuclex.Support.Tracking { public void TestWeightedSummedProgress() { SetProgression testSetProgression = new SetProgression( - new WeightedProgression[] { - new WeightedProgression(new TestWaitable(), 1.0f), - new WeightedProgression(new TestWaitable(), 2.0f) + new WeightedWaitable[] { + new WeightedWaitable(new TestWaitable(), 1.0f), + new WeightedWaitable(new TestWaitable(), 2.0f) } ); @@ -191,7 +191,7 @@ namespace Nuclex.Support.Tracking { } ); - testSetProgression.Children[0].Progression.ChangeProgress(0.5f); + testSetProgression.Children[0].Waitable.ChangeProgress(0.5f); Expect.Once.On(mockedSubscriber). Method("ProgressChanged"). @@ -202,7 +202,7 @@ namespace Nuclex.Support.Tracking { } ); - testSetProgression.Children[1].Progression.ChangeProgress(0.5f); + testSetProgression.Children[1].Waitable.ChangeProgress(0.5f); this.mockery.VerifyAllExpectationsHaveBeenMet(); } @@ -227,8 +227,8 @@ namespace Nuclex.Support.Tracking { Method("Ended"). WithAnyArguments(); - testSetProgression.Children[0].Progression.End(); - testSetProgression.Children[1].Progression.End(); + testSetProgression.Children[0].Waitable.End(); + testSetProgression.Children[1].Waitable.End(); this.mockery.VerifyAllExpectationsHaveBeenMet(); } diff --git a/Source/Tracking/SetProgression.cs b/Source/Tracking/SetProgression.cs index f8391e1..05743e5 100644 --- a/Source/Tracking/SetProgression.cs +++ b/Source/Tracking/SetProgression.cs @@ -46,10 +46,10 @@ namespace Nuclex.Support.Tracking { // progression and wrap it in an ObservedProgression foreach(ProgressionType progression in childs) { this.children.Add( - new ObservedWeightedProgression( - new WeightedProgression(progression), - new ObservedWeightedProgression.ReportDelegate(asyncProgressUpdated), - new ObservedWeightedProgression.ReportDelegate(asyncEnded) + new ObservedWeightedWaitable( + new WeightedWaitable(progression), + new ObservedWeightedWaitable.ReportDelegate(asyncProgressUpdated), + new ObservedWeightedWaitable.ReportDelegate(asyncEnded) ) ); } @@ -63,17 +63,17 @@ namespace Nuclex.Support.Tracking { /// Initializes a new set progression /// Progressions to track with this set public SetProgression( - IEnumerable> childs + IEnumerable> childs ) : this() { // Construct an ObservedProgression around each of the WeightedProgressions - foreach(WeightedProgression progression in childs) { + foreach(WeightedWaitable progression in childs) { this.children.Add( - new ObservedWeightedProgression( + new ObservedWeightedWaitable( progression, - new ObservedWeightedProgression.ReportDelegate(asyncProgressUpdated), - new ObservedWeightedProgression.ReportDelegate(asyncEnded) + new ObservedWeightedWaitable.ReportDelegate(asyncProgressUpdated), + new ObservedWeightedWaitable.ReportDelegate(asyncEnded) ) ); @@ -85,7 +85,7 @@ namespace Nuclex.Support.Tracking { /// Performs common initialization for the public constructors private SetProgression() { - this.children = new List>(); + this.children = new List>(); } /// Immediately releases all resources owned by the object @@ -106,7 +106,7 @@ namespace Nuclex.Support.Tracking { } /// Childs contained in the progression set - public IList> Children { + public IList> Children { get { // The wrapper is constructed only when needed. Most of the time, users will @@ -160,7 +160,7 @@ namespace Nuclex.Support.Tracking { // scaled to the weight each progression has assigned to it. for(int index = 0; index < this.children.Count; ++index) { totalProgress += - this.children[index].Progress * this.children[index].WeightedProgression.Weight; + this.children[index].Progress * this.children[index].WeightedWaitable.Weight; } // Calculate the actual combined progress @@ -179,7 +179,7 @@ namespace Nuclex.Support.Tracking { // If there's still at least one progression going, don't report that // the SetProgression has finished yet. for(int index = 0; index < this.children.Count; ++index) - if(!this.children[index].WeightedProgression.Progression.Ended) + if(!this.children[index].WeightedWaitable.Waitable.Ended) return; // All child progressions have ended, so the set has now ended as well @@ -188,7 +188,7 @@ namespace Nuclex.Support.Tracking { } /// Progressions being managed in the set - private List> children; + private List> children; /// /// Wrapper collection for exposing the child progressions under the /// WeightedProgression interface diff --git a/Source/Tracking/WeightedProgression.cs b/Source/Tracking/WeightedProgression.cs index ee49e30..df4d0f2 100644 --- a/Source/Tracking/WeightedProgression.cs +++ b/Source/Tracking/WeightedProgression.cs @@ -24,24 +24,24 @@ using System.Collections.Generic; namespace Nuclex.Support.Tracking { /// Progression with an associated weight for the total progress - public class WeightedProgression where ProgressionType : Waitable { + public class WeightedWaitable where ProgressionType : Waitable { /// /// Initializes a new weighted progression with a default weight of 1.0 /// /// Progression whose progress to monitor - public WeightedProgression(ProgressionType progression) : this(progression, 1.0f) { } + public WeightedWaitable(ProgressionType progression) : this(progression, 1.0f) { } /// Initializes a new weighted progression /// Progression whose progress to monitor /// Weighting of the progression's progress - public WeightedProgression(ProgressionType progression, float weight) { + public WeightedWaitable(ProgressionType progression, float weight) { this.progression = progression; this.weight = weight; } /// Progression being wrapped by this weighted progression - public ProgressionType Progression { + public ProgressionType Waitable { get { return this.progression; } }