diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj
index ea87d51..6fe0931 100644
--- a/Nuclex.Support (PC).csproj
+++ b/Nuclex.Support (PC).csproj
@@ -170,6 +170,11 @@
false
QueueOperation
+
+ false
+ QueueOperation.Test
+ QueueOperation.cs
+
false
ThreadOperation
diff --git a/Source/Scheduling/Operation.cs b/Source/Scheduling/Operation.cs
index dc031ef..9363474 100644
--- a/Source/Scheduling/Operation.cs
+++ b/Source/Scheduling/Operation.cs
@@ -33,7 +33,9 @@ namespace Nuclex.Support.Scheduling {
/// Waits for the background operation to end
///
/// Any exceptions raised in the background operation will be thrown
- /// in this method.
+ /// in this method. If you decide to override this method, you should
+ /// call End() first (and let any possible exception through to your
+ /// caller).
///
public virtual void End() {
@@ -83,7 +85,7 @@ namespace Nuclex.Support.Scheduling {
// We allow the caller to set the exception multiple times. While I certainly
// can't think of a scenario where this would happen, throwing an exception
// in that case seems worse. The caller might just be executing an exception
- // handling block and locking + throwing here could cause even more problems.
+ // handling block and locking + throwing here could cause all kinds of problems.
this.occuredException = exception;
}
diff --git a/Source/Scheduling/QueueOperation.Test.cs b/Source/Scheduling/QueueOperation.Test.cs
new file mode 100644
index 0000000..d7893c3
--- /dev/null
+++ b/Source/Scheduling/QueueOperation.Test.cs
@@ -0,0 +1,193 @@
+#region CPL License
+/*
+Nuclex Framework
+Copyright (C) 2002-2007 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.IO;
+
+#if UNITTEST
+
+using NUnit.Framework;
+using NMock2;
+
+using Nuclex.Support.Tracking;
+
+namespace Nuclex.Support.Scheduling {
+
+ /// Unit Test for the queue operation class
+ [TestFixture]
+ public class QueueOperationTest {
+
+ #region interface IQueueOperationSubscriber
+
+ /// Interface used to test the set progression.
+ public interface IQueueOperationSubscriber {
+
+ /// Called when the queue operations's progress changes
+ /// Queue operation whose progress has changed
+ /// Contains the new progress achieved
+ void ProgressUpdated(object sender, ProgressUpdateEventArgs e);
+
+ /// Called when the queue operation has ended
+ /// Queue operation that as ended
+ /// Not used
+ void Ended(object sender, EventArgs e);
+
+ }
+
+ #endregion // interface IQueueOperationSubscriber
+
+ #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 TestOperation
+
+ /// Progression used for testing in this unit test
+ private class TestOperation : Operation {
+
+ /// Begins executing the operation. Yeah, sure :)
+ public override void Begin() { }
+
+ /// Moves the operation into the ended state
+ public void SetEnded() {
+ SetEnded(null);
+ }
+
+ /// Moves the operation into the ended state with an exception
+ /// Exception
+ public void SetEnded(Exception exception) {
+ SetException(exception);
+ OnAsyncEnded();
+ }
+
+ /// Changes the testing progression's indicated progress
+ ///
+ /// New progress to be reported by the testing progression
+ ///
+ public void ChangeProgress(float progress) {
+ OnAsyncProgressUpdated(progress);
+ }
+
+ }
+
+ #endregion // class TestOperation
+
+ /// Initialization routine executed before each test is run
+ [SetUp]
+ public void Setup() {
+ this.mockery = new Mockery();
+ }
+
+ /// Validates that the queue executes operations sequentially
+ [Test]
+ public void TestSequentialExecution() {
+ TestOperation operation1 = new TestOperation();
+ TestOperation operation2 = new TestOperation();
+
+ QueueOperation testQueueOperation =
+ new QueueOperation(
+ new TestOperation[] { operation1, operation2 }
+ );
+
+ IQueueOperationSubscriber mockedSubscriber = mockSubscriber(testQueueOperation);
+
+ testQueueOperation.Begin();
+
+ Expect.Once.On(mockedSubscriber).
+ Method("ProgressUpdated").
+ With(
+ new Matcher[] {
+ new NMock2.Matchers.TypeMatcher(typeof(QueueOperation)),
+ new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.25f))
+ }
+ );
+
+ operation1.ChangeProgress(0.5f);
+
+ Expect.Once.On(mockedSubscriber).
+ Method("ProgressUpdated").
+ With(
+ new Matcher[] {
+ new NMock2.Matchers.TypeMatcher(typeof(QueueOperation)),
+ new ProgressUpdateEventArgsMatcher(new ProgressUpdateEventArgs(0.5f))
+ }
+ );
+
+ operation1.SetEnded();
+
+ this.mockery.VerifyAllExpectationsHaveBeenMet();
+ }
+
+ /// Mocks a subscriber for the events of an operation
+ /// Operation to mock an event subscriber for
+ /// The mocked event subscriber
+ private IQueueOperationSubscriber mockSubscriber(Operation operation) {
+ IQueueOperationSubscriber mockedSubscriber =
+ this.mockery.NewMock();
+
+ operation.AsyncEnded += new EventHandler(mockedSubscriber.Ended);
+ operation.AsyncProgressUpdated +=
+ new EventHandler(mockedSubscriber.ProgressUpdated);
+
+ return mockedSubscriber;
+ }
+
+ /// Mock object factory
+ private Mockery mockery;
+ }
+
+} // namespace Nuclex.Support.Tracking
+
+#endif // UNITTEST
diff --git a/Source/Scheduling/QueueOperation.cs b/Source/Scheduling/QueueOperation.cs
index 0922f14..49389f4 100644
--- a/Source/Scheduling/QueueOperation.cs
+++ b/Source/Scheduling/QueueOperation.cs
@@ -32,36 +32,156 @@ namespace Nuclex.Support.Scheduling {
where OperationType : Operation {
/// Initializes a new queue operation
+ private QueueOperation() {
+ this.asyncOperationEndedDelegate = new EventHandler(asyncOperationEnded);
+ this.asyncOperationProgressUpdatedDelegate = new EventHandler(
+ asyncOperationProgressUpdated
+ );
+
+ this.childs = new List>();
+ }
+
+ /// Initializes a new queue operation with default weights
/// Child operations to execute in this operation
///
/// All child operations will have a default weight of 1.0
///
- public QueueOperation(IEnumerable childs) {
- this.setProgression = new SetProgression(childs);
+ public QueueOperation(IEnumerable childs) : this() {
+
+ // Construct a WeightedProgression with the default weight for each
+ // progression and wrap it in an ObservedProgression
+ foreach(OperationType operation in childs)
+ this.childs.Add(new WeightedProgression(operation));
+
+ // 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 queue operation with custom weights
/// Child operations to execute in this operation
- public QueueOperation(IEnumerable> childs) {
- this.setProgression = new SetProgression(childs);
+ public QueueOperation(IEnumerable> childs) : this() {
+
+ // Construct an ObservedProgression around each of the WeightedProgressions
+ foreach(WeightedProgression operation in childs) {
+ this.childs.Add(operation);
+
+ // Sum up the total weight
+ this.totalWeight += operation.Weight;
+ }
+
}
-
+
+ /// Provides access to the child operations of this queue
+ public IList> Childs {
+ get { return this.childs; }
+ }
+
/// Launches the background operation
public override void Begin() {
- this.setProgression.AsyncProgressUpdated +=
- new EventHandler(setProgressionProgressUpdated);
-
-
-
- //this.setProgression.Childs[0].Progression.Begin();
+ beginCurrentOperation();
}
- private void setProgressionProgressUpdated(object sender, ProgressUpdateEventArgs e) {
- throw new Exception("The method or operation is not implemented.");
+ /// Prepares the current operation and calls its Begin() method
+ ///
+ /// This subscribes the queue to the events of to the current operation
+ /// and launches the operation by calling its Begin() method.
+ ///
+ private void beginCurrentOperation() {
+ OperationType operation = this.childs[this.currentOperationIndex].Progression;
+
+ operation.AsyncEnded += this.asyncOperationEndedDelegate;
+ operation.AsyncProgressUpdated += this.asyncOperationProgressUpdatedDelegate;
+
+ operation.Begin();
}
- /// SetProgression used internally to handle progress reports
- private volatile SetProgression setProgression;
+ /// Disconnects from the current operation and calls its End() method
+ ///
+ /// This unsubscribes the queue from the current operation's events, calls End()
+ /// on the operation and, if the operation didn't have an exception to report,
+ /// counts up the accumulated progress of the queue.
+ ///
+ private void endCurrentOperation() {
+ OperationType operation = this.childs[this.currentOperationIndex].Progression;
+
+ // Disconnect from the operation's events
+ operation.AsyncEnded -= this.asyncOperationEndedDelegate;
+ operation.AsyncProgressUpdated -= this.asyncOperationProgressUpdatedDelegate;
+
+ try {
+ operation.End();
+
+ // Add the operations weight to the total amount of completed weight in the queue
+ this.completedWeight += this.childs[this.currentOperationIndex].Weight;
+
+ // Trigger another progress update
+ OnAsyncProgressUpdated(this.completedWeight / this.totalWeight);
+ }
+ catch(Exception exception) {
+ SetException(exception);
+ }
+ }
+
+ /// Called when the current executing operation ends
+ /// Operation that ended
+ /// Not used
+ private void asyncOperationEnded(object sender, EventArgs e) {
+
+ // Unsubscribe from the current operation's events and update the
+ // accumulating progress counter
+ endCurrentOperation();
+
+ // Only jump to the next operation if no exception occured
+ if(OccuredException == null) {
+
+ ++this.currentOperationIndex;
+
+ // Execute the next operation unless we reached the end of the queue
+ if(this.currentOperationIndex < this.childs.Count) {
+ beginCurrentOperation();
+ return;
+ }
+
+ }
+
+ // Either an exception has occured or we reached the end of the operation
+ // queue. In any case, we need to report that the operation is over.
+ OnAsyncEnded();
+
+ }
+
+ /// Called when currently executing operation makes progress
+ /// Operation that has achieved progress
+ /// Not used
+ private void asyncOperationProgressUpdated(object sender, ProgressUpdateEventArgs e) {
+
+ // Determine the completed weight of the currently executing operation
+ float currentOperationCompletedWeight =
+ e.Progress * this.childs[this.currentOperationIndex].Weight;
+
+ // Build the total normalized amount of progress for the queue
+ float progress =
+ (this.completedWeight + currentOperationCompletedWeight) / this.totalWeight;
+
+ // Done, we can send the actual progress to any event subscribers
+ OnAsyncProgressUpdated(progress);
+
+ }
+
+ /// Delegate to the asyncOperationEnded() method
+ private EventHandler asyncOperationEndedDelegate;
+ /// Delegate to the asyncOperationProgressUpdated() method
+ private EventHandler asyncOperationProgressUpdatedDelegate;
+ /// Operations being managed in the queue
+ private List> childs;
+ /// Summed weight of all operations in the queue
+ private float totalWeight;
+ /// Accumulated weight of the operations already completed
+ private float completedWeight;
+ /// Index of the operation currently executing
+ private int currentOperationIndex;
}
diff --git a/Source/Tracking/ProgressionTracker.cs b/Source/Tracking/ProgressionTracker.cs
index 25f2bdc..1d82e2e 100644
--- a/Source/Tracking/ProgressionTracker.cs
+++ b/Source/Tracking/ProgressionTracker.cs
@@ -34,7 +34,7 @@ namespace Nuclex.Support.Tracking {
/// 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 tracking might finish any number of times).
+ /// progression tracker might 'finish' any number of times).
///
///
public class ProgressionTracker : IDisposable {
@@ -98,6 +98,10 @@ namespace Nuclex.Support.Tracking {
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();
diff --git a/Source/Tracking/SetProgression.Test.cs b/Source/Tracking/SetProgression.Test.cs
index e6a6fbf..b64b14b 100644
--- a/Source/Tracking/SetProgression.Test.cs
+++ b/Source/Tracking/SetProgression.Test.cs
@@ -180,20 +180,6 @@ namespace Nuclex.Support.Tracking {
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
///
@@ -220,6 +206,20 @@ namespace Nuclex.Support.Tracking {
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;
+ }
+
/// Mock object factory
private Mockery mockery;
diff --git a/Source/Tracking/SetProgression.cs b/Source/Tracking/SetProgression.cs
index 09745c2..358fd22 100644
--- a/Source/Tracking/SetProgression.cs
+++ b/Source/Tracking/SetProgression.cs
@@ -167,7 +167,7 @@ namespace Nuclex.Support.Tracking {
/// WeightedProgression interface
///
private volatile WeightedProgressionWrapperCollection wrapper;
- /// Summed weight of all progression in the set
+ /// Summed weight of all progressions in the set
private float totalWeight;
}