diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj
index 6823b43..4a20885 100644
--- a/Nuclex.Support (PC).csproj
+++ b/Nuclex.Support (PC).csproj
@@ -164,6 +164,11 @@
false
ThreadedMethodOperation
+
+ false
+ SetProgression.Test
+ SetProgression.cs
+
false
WeightedProgression
diff --git a/Source/Collections/ObservableCollection.Test.cs b/Source/Collections/ObservableCollection.Test.cs
index 7cb1afd..2137d36 100644
--- a/Source/Collections/ObservableCollection.Test.cs
+++ b/Source/Collections/ObservableCollection.Test.cs
@@ -49,35 +49,36 @@ namespace Nuclex.Support.Collections {
new EventHandler.ItemEventArgs>(
this.mockedSubscriber.ItemRemoved
);
+
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// Tests whether the Clearing event is fired
[Test]
public void TestClearingEvent() {
-
Expect.Once.On(this.mockedSubscriber).
Method("Clearing");
this.observedCollection.Clear();
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// Tests whether the ItemAdded event is fired
[Test]
public void TestItemAddedEvent() {
-
Expect.Once.On(this.mockedSubscriber).
Method("ItemAdded").
WithAnyArguments();
this.observedCollection.Add(123);
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// Tests whether the ItemRemoved event is fired
[Test]
public void TestItemRemovedEvent() {
-
Expect.Once.On(this.mockedSubscriber).
Method("ItemAdded").
WithAnyArguments();
@@ -89,6 +90,7 @@ namespace Nuclex.Support.Collections {
this.observedCollection.Add(123);
this.observedCollection.Remove(123);
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
}
/// Mock object factory
diff --git a/Source/Tracking/Internal/ObservedProgression.cs b/Source/Tracking/Internal/ObservedProgression.cs
index 0da1b75..aab0d8d 100644
--- a/Source/Tracking/Internal/ObservedProgression.cs
+++ b/Source/Tracking/Internal/ObservedProgression.cs
@@ -8,34 +8,105 @@ namespace Nuclex.Support.Tracking {
///
/// Type of the progression that is being observed
///
- internal class ObservedProgression
+ internal class ObservedProgression : IDisposable
where ProgressionType : Progression {
+ /// Delegate for reporting progress updates
+ public delegate void ReportDelegate();
+
/// Initializes a new observed progression
/// Weighted progression being observed
+ ///
+ /// Callback to invoke when the progression's progress changes
+ ///
+ ///
+ /// Callback to invoke when the progression has ended
+ ///
internal ObservedProgression(
- WeightedProgression weightedProgression
+ WeightedProgression weightedProgression,
+ ReportDelegate progressUpdateCallback,
+ ReportDelegate endedCallback
) {
this.weightedProgression = weightedProgression;
+ this.endedCallback = endedCallback;
+ this.progressUpdateCallback = progressUpdateCallback;
+
+ this.weightedProgression.Progression.AsyncEnded +=
+ new EventHandler(asyncEnded);
+
+ this.weightedProgression.Progression.AsyncProgressUpdated +=
+ new EventHandler(asyncProgressUpdated);
}
-
+
+ /// Immediately releases all resources owned by the object
+ public void Dispose() {
+ disconnectEvents();
+ }
+
/// Weighted progression being observed
public WeightedProgression WeightedProgression {
get { return this.weightedProgression; }
}
-/*
- internal void AsyncProgressUpdated(object sender, ProgressUpdateEventArgs e) {
- this.Progress = e.Progress;
+ /// Amount of progress this progression has achieved so far
+ public float Progress {
+ get { return this.progress; }
}
- internal void AsyncEnded(object sender, EventArgs e) { }
-*/
+
+ /// Called when the observed progression has ended
+ /// Progression that has ended
+ /// Not used
+ private void asyncEnded(object sender, EventArgs e) {
+ this.progress = 1.0f;
+
+ disconnectEvents(); // We don't need those anymore!
+
+ this.progressUpdateCallback();
+ }
+
+ /// Called when the progress of the observed progression changes
+ /// Progression whose progress has changed
+ /// Contains the updated progress
+ private void asyncProgressUpdated(object sender, ProgressUpdateEventArgs e) {
+ this.progress = e.Progress;
+
+ this.progressUpdateCallback();
+ }
+
+ /// Unscribes from all events of the observed progression
+ private void disconnectEvents() {
+
+ // Make use of the double check locking idiom to avoid the costly lock when
+ // the events have already been unsubscribed
+ if(this.endedCallback != null) {
+
+ // This is an internal class with special knowledge that there
+ // is no risk of deadlock involved, so we don't need a fancy syncRoot!
+ lock(this) {
+ if(this.endedCallback != null) {
+ this.weightedProgression.Progression.AsyncEnded -=
+ new EventHandler(asyncEnded);
+
+ this.weightedProgression.Progression.AsyncProgressUpdated -=
+ new EventHandler(asyncProgressUpdated);
+
+ this.endedCallback = null;
+ this.progressUpdateCallback = null;
+ }
+ }
+
+ } // endedCallback != null
+
+ }
+
/// The weighted progression that is being observed
private WeightedProgression weightedProgression;
-/*
- /// Amount of progress this progression has achieved so far
+ /// Callback to invoke when the progress updates
+ private volatile ReportDelegate progressUpdateCallback;
+ /// Callback to invoke when the progression ends
+ private volatile ReportDelegate endedCallback;
+ /// Progress achieved so far
private volatile float progress;
-*/
}
} // namespace Nuclex.Support.Tracking
diff --git a/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs b/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs
index 97e6598..550cad9 100644
--- a/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs
+++ b/Source/Tracking/Internal/WeightedProgressionWrapperCollection.cs
@@ -27,7 +27,7 @@ namespace Nuclex.Support.Tracking {
/// SetProgression or QueueOperation classes was created.
///
///
- internal class WeightedProgressionCollection :
+ internal class WeightedProgressionWrapperCollection :
TransformingReadOnlyCollection<
ObservedProgression, WeightedProgression
>
@@ -35,7 +35,7 @@ namespace Nuclex.Support.Tracking {
/// Initializes a new weighted progression collection wrapper
/// Items to be exposed as weighted progressions
- internal WeightedProgressionCollection(
+ internal WeightedProgressionWrapperCollection(
IList> items
)
: base(items) { }
diff --git a/Source/Tracking/ProgressUpdateEventArgs.cs b/Source/Tracking/ProgressUpdateEventArgs.cs
index 7e50889..c44a8d0 100644
--- a/Source/Tracking/ProgressUpdateEventArgs.cs
+++ b/Source/Tracking/ProgressUpdateEventArgs.cs
@@ -19,7 +19,7 @@ namespace Nuclex.Support.Tracking {
}
/// Achieved progress
- protected float progress;
+ private float progress;
}
diff --git a/Source/Tracking/Progression.cs b/Source/Tracking/Progression.cs
index 66c0447..05a20cd 100644
--- a/Source/Tracking/Progression.cs
+++ b/Source/Tracking/Progression.cs
@@ -52,17 +52,21 @@ namespace Nuclex.Support.Tracking {
public event EventHandler AsyncEnded;
/// Whether the progression has ended already
- public virtual bool Ended {
+ public bool Ended {
get { return this.ended; }
}
/// WaitHandle that can be used to wait for the progression to end
public WaitHandle WaitHandle {
get {
-
+
// The WaitHandle will only be created when someone asks for it!
// See the Double-Check Locking idiom on why the condition is checked twice
// (primarily, it avoids an expensive lock when it isn't needed)
+ //
+ // We can *not* optimize this lock away since we absolutely must not create
+ // two doneEvents -- someone might call .WaitOne() on the first one when only
+ // the second one is assigned to this.doneEvent and gets set in the end.
if(this.doneEvent == null) {
lock(this.syncRoot) {
@@ -109,16 +113,27 @@ namespace Nuclex.Support.Tracking {
/// seperately.
///
protected virtual void OnAsyncEnded() {
+
+ // TODO: Find a way around this. Interlocked.Exchange would be best!
+ // We do not lock here since we require this method to be called only once
+ // in the object's lifetime. If someone really badly wanted to break this
+ // he'd probably have a one-in-a-million chance of getting through.
+ if(this.ended)
+ throw new InvalidOperationException("The progression has already been ended");
+
this.ended = true;
- lock(this.syncRoot) {
- if(this.doneEvent != null)
- this.doneEvent.Set();
- }
+ // Doesn't need a lock. If another thread wins the race and creates the event
+ // after we just saw it being null, it would be created in an already set
+ // state due to the ended flag (see above) being set to true beforehand!
+ if(this.doneEvent != null)
+ this.doneEvent.Set();
+ // Finally, fire the AsyncEnded event
EventHandler copy = AsyncEnded;
if(copy != null)
copy(this, EventArgs.Empty);
+
}
/// Used to synchronize multithreaded accesses to this object
diff --git a/Source/Tracking/SetProgression.Test.cs b/Source/Tracking/SetProgression.Test.cs
new file mode 100644
index 0000000..f39658f
--- /dev/null
+++ b/Source/Tracking/SetProgression.Test.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+#if UNITTEST
+
+using NUnit.Framework;
+using NMock2;
+
+namespace Nuclex.Support.Tracking {
+
+ /// Unit Test for the progression set class
+ [TestFixture]
+ public class SetProgressionTest {
+
+ #region interface ISetProgressionSubscriber
+
+ /// Interface used to test the set progression.
+ public interface ISetProgressionSubscriber {
+
+ /// Called when the set progression's progress changes
+ /// Set progression whose progress has changed
+ /// Contains the new progress achieved
+ void ProgressUpdated(object sender, ProgressUpdateEventArgs e);
+
+ /// Called when the set progression has ended
+ /// Set progression that as ended
+ /// Not used
+ void Ended(object sender, EventArgs e);
+
+ }
+
+ #endregion // interface ISetProgressionSubscriber
+
+ #region class ProgressUpdateEventArgsMatcher
+
+ /// Compares two ProgressUpdateEventArgsInstances for NMock validation
+ private class ProgressUpdateEventArgsMatcher : Matcher {
+
+ /// Initializes a new ProgressUpdateEventArgsMatcher
+ /// Expected progress update event arguments
+ public ProgressUpdateEventArgsMatcher(ProgressUpdateEventArgs expected) {
+ this.expected = expected;
+ }
+
+ ///
+ /// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
+ ///
+ /// Actual value to compare to the expected value
+ ///
+ /// True if the actual value matches the expected value; otherwise false
+ ///
+ public override bool Matches(object actualAsObject) {
+ ProgressUpdateEventArgs actual = (actualAsObject as ProgressUpdateEventArgs);
+ if(actual == null)
+ return false;
+
+ return (actual.Progress == this.expected.Progress);
+ }
+
+ /// Creates a string representation of the expected value
+ /// Writer to write the string representation into
+ public override void DescribeTo(TextWriter writer) {
+ writer.Write(this.expected.Progress.ToString());
+ }
+
+ /// Expected progress update event args value
+ private ProgressUpdateEventArgs expected;
+
+ }
+
+ #endregion // class ProgressUpdateEventArgsMatcher
+
+ #region class TestProgression
+
+ /// Progression used for testing in this unit test
+ private class TestProgression : Progression {
+
+ /// Changes the testing progression's indicated progress
+ ///
+ /// New progress to be reported by the testing progression
+ ///
+ public void ChangeProgress(float progress) {
+ OnAsyncProgressUpdated(progress);
+ }
+
+ /// Transitions the progression into the ended state
+ public void End() {
+ OnAsyncEnded();
+ }
+
+ }
+
+ #endregion // class TestProgression
+
+ /// Initialization routine executed before each test is run
+ [SetUp]
+ public void Setup() {
+ this.mockery = new Mockery();
+ }
+
+ /// Validates that the set progression properly sums the progress
+ [Test]
+ public void TestSummedProgress() {
+ SetProgression testSetProgression =
+ new SetProgression(
+ new TestProgression[] {
+ new TestProgression(), new TestProgression()
+ }
+ );
+
+ ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
+
+ Expect.Once.On(mockedSubscriber).
+ Method("ProgressUpdated").
+ With(
+ new Matcher[] {
+ new NMock2.Matchers.TypeMatcher(typeof(SetProgression)),
+ new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.25f))
+ }
+ );
+
+ testSetProgression.Childs[0].Progression.ChangeProgress(0.5f);
+
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
+ }
+
+ /// Validates that the set progression respects the weights
+ [Test]
+ public void TestWeightedSummedProgress() {
+ SetProgression testSetProgression =
+ new SetProgression(
+ new WeightedProgression[] {
+ new WeightedProgression(new TestProgression(), 1.0f),
+ new WeightedProgression(new TestProgression(), 2.0f)
+ }
+ );
+
+ ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
+
+ Expect.Once.On(mockedSubscriber).
+ Method("ProgressUpdated").
+ With(
+ new Matcher[] {
+ new NMock2.Matchers.TypeMatcher(typeof(SetProgression)),
+ new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f / 3.0f))
+ }
+ );
+
+ testSetProgression.Childs[0].Progression.ChangeProgress(0.5f);
+
+ Expect.Once.On(mockedSubscriber).
+ Method("ProgressUpdated").
+ With(
+ new Matcher[] {
+ new NMock2.Matchers.TypeMatcher(typeof(SetProgression)),
+ new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f))
+ }
+ );
+
+ testSetProgression.Childs[1].Progression.ChangeProgress(0.5f);
+
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
+ }
+
+ /// Mocks a subscriber for the events of a progression
+ /// Progression to mock an event subscriber for
+ /// The mocked event subscriber
+ private ISetProgressionSubscriber mockSubscriber(Progression progression) {
+ ISetProgressionSubscriber mockedSubscriber =
+ this.mockery.NewMock();
+
+ progression.AsyncEnded += new EventHandler(mockedSubscriber.Ended);
+ progression.AsyncProgressUpdated +=
+ new EventHandler(mockedSubscriber.ProgressUpdated);
+
+ return mockedSubscriber;
+ }
+
+ ///
+ /// Validates that the ended event is triggered when the last progression ends
+ ///
+ [Test]
+ public void TestEndedEvent() {
+ SetProgression testSetProgression =
+ new SetProgression(
+ new TestProgression[] {
+ new TestProgression(), new TestProgression()
+ }
+ );
+
+ ISetProgressionSubscriber mockedSubscriber = mockSubscriber(testSetProgression);
+
+ Expect.Exactly(2).On(mockedSubscriber).
+ Method("ProgressUpdated").
+ WithAnyArguments();
+
+ Expect.Once.On(mockedSubscriber).
+ Method("Ended").
+ WithAnyArguments();
+
+ testSetProgression.Childs[0].Progression.End();
+ testSetProgression.Childs[1].Progression.End();
+
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
+ }
+
+ /// Mock object factory
+ private Mockery mockery;
+
+ }
+
+} // namespace Nuclex.Support.Tracking
+
+#endif // UNITTEST
diff --git a/Source/Tracking/SetProgression.cs b/Source/Tracking/SetProgression.cs
index 29bdba0..c2ce6da 100644
--- a/Source/Tracking/SetProgression.cs
+++ b/Source/Tracking/SetProgression.cs
@@ -11,31 +11,139 @@ namespace Nuclex.Support.Tracking {
public class SetProgression : Progression
where ProgressionType : Progression {
+ /// Performs common initialization for the public constructors
+ private SetProgression() {
+ this.childs = new List>();
+ this.asyncProgressUpdatedDelegate =
+ new ObservedProgression.ReportDelegate(asyncProgressUpdated);
+ this.asyncEndedDelegate =
+ new ObservedProgression.ReportDelegate(asyncEnded);
+ }
+
/// Initializes a new set progression
- /// Progressions to track with this set
+ /// Progressions to track with this set
///
/// Uses a default weighting factor of 1.0 for all progressions.
///
- public SetProgression(IEnumerable progressions) {
+ public SetProgression(IEnumerable childs)
+ : this() {
+
+ // Construct a WeightedProgression with the default weight for each
+ // progression and wrap it in an ObservedProgression
+ foreach(ProgressionType progression in childs) {
+ this.childs.Add(
+ new ObservedProgression(
+ new WeightedProgression(progression),
+ this.asyncProgressUpdatedDelegate, this.asyncEndedDelegate
+ )
+ );
+ }
+
+ // Since all progressions have a weight of 1.0, the total weight is
+ // equal to the number of progressions in our list
+ this.totalWeight = (float)this.childs.Count;
+
}
/// Initializes a new set progression
- /// Progressions to track with this set
- public SetProgression(IEnumerable> progressions) {
+ /// Progressions to track with this set
+ public SetProgression(
+ IEnumerable> childs
+ )
+ : this() {
+
+ // Construct an ObservedProgression around each of the WeightedProgressions
+ float totalWeight = 0.0f;
+ foreach(WeightedProgression progression in childs) {
+ this.childs.Add(
+ new ObservedProgression(
+ progression,
+ this.asyncProgressUpdatedDelegate, this.asyncEndedDelegate
+ )
+ );
+
+ // Sum up the total weight
+ totalWeight += progression.Weight;
+ }
+
+ // Take over the summed weight of all progressions we were given
+ this.totalWeight = totalWeight;
+
}
/// Childs contained in the progression set
- public ReadOnlyCollection> Childs {
- get { return null; }
+ public IList> Childs {
+ get {
+
+ // The wrapper is constructed only when needed. Most of the time, users will
+ // just create a SetProgression and monitor its progress without ever using
+ // the Childs collection.
+ if(this.wrapper == null) {
+
+ // This doesn't need a lock because it's only a stateless wrapper. If it
+ // is constructed twice, then so be it.
+ this.wrapper = new WeightedProgressionWrapperCollection(
+ this.childs
+ );
+
+ }
+
+ return this.wrapper;
+
+ }
}
-/*
+
+ ///
+ /// Called when the progress of one of the observed progressions changes
+ ///
+ private void asyncProgressUpdated() {
+
+ // Calculate the sum of the progress reported by our child progressions,
+ // scaled to the weight each progression has assigned to it.
+ float totalProgress = 0.0f;
+ for(int index = 0; index < this.childs.Count; ++index) {
+ totalProgress +=
+ this.childs[index].Progress * this.childs[index].WeightedProgression.Weight;
+ }
+
+ // Calculate the actual combined progress
+ if(this.totalWeight > 0.0f)
+ totalProgress /= this.totalWeight;
+
+ // Send out the progress update
+ OnAsyncProgressUpdated(totalProgress);
+
+ }
+
+ ///
+ /// Called when an observed progressions ends
+ ///
+ private void asyncEnded() {
+
+ for(int index = 0; index < this.childs.Count; ++index)
+ if(!this.childs[index].WeightedProgression.Progression.Ended)
+ return;
+
+ OnAsyncEnded();
+
+ }
+
/// Progressions being managed in the set
- private ReadOnlyCollection progressions;
- /// whether the progress needs to be recalculated
- private volatile bool needProgressRecalculation;
- /// Total progress achieved by the progressions in this collection
- private volatile float totalProgress;
-*/
+ private List> childs;
+ ///
+ /// Wrapper collection for exposing the child progressions under the
+ /// WeightedProgression interface
+ ///
+ private volatile WeightedProgressionWrapperCollection wrapper;
+ /// Summed weight of all progression in the set
+ private float totalWeight;
+
+ /// Delegate for the asyncProgressUpdated() method
+ private ObservedProgression.ReportDelegate asyncProgressUpdatedDelegate;
+ /// Delegate for the asyncEnded() method
+ private ObservedProgression.ReportDelegate asyncEndedDelegate;
+
+
}
} // namespace Nuclex.Support.Tracking