2008-03-26 21:20:52 +00:00
|
|
|
#region CPL License
|
|
|
|
/*
|
|
|
|
Nuclex Framework
|
2010-07-08 12:37:39 +00:00
|
|
|
Copyright (C) 2002-2010 Nuclex Development Labs
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
|
|
/// <summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// Helps tracking the progress of one or more background transactions
|
2008-03-26 21:20:52 +00:00
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// <para>
|
|
|
|
/// This is useful if you want to display a progress bar for multiple
|
2008-12-03 18:58:20 +00:00
|
|
|
/// transactions but can not guarantee that no additional transactions
|
2008-03-26 21:20:52 +00:00
|
|
|
/// will appear inmidst of execution.
|
|
|
|
/// </para>
|
|
|
|
/// <para>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// This class does not implement the <see cref="Transaction" /> interface itself
|
|
|
|
/// in order to not violate the design principles of transactions which
|
|
|
|
/// guarantee that a <see cref="Transaction" /> will only finish once (whereas the
|
2008-03-26 21:20:52 +00:00
|
|
|
/// progress tracker might 'finish' any number of times).
|
|
|
|
/// </para>
|
|
|
|
/// </remarks>
|
|
|
|
public class ProgressTracker : IDisposable, IProgressReporter {
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
#region class TransactionMatcher
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Matches a direct transaction to a fully wrapped one</summary>
|
|
|
|
private class TransactionMatcher {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
/// <summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// Initializes a new transaction matcher that matches against
|
|
|
|
/// the specified transaction
|
2008-03-26 21:20:52 +00:00
|
|
|
/// </summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <param name="toMatch">Transaction to match against</param>
|
|
|
|
public TransactionMatcher(Transaction toMatch) {
|
2008-03-26 21:20:52 +00:00
|
|
|
this.toMatch = toMatch;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// Checks whether the provided transaction matches the comparison
|
|
|
|
/// transaction of the instance
|
2008-03-26 21:20:52 +00:00
|
|
|
/// </summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <param name="other">Transaction to match to the comparison transaction</param>
|
|
|
|
public bool Matches(ObservedWeightedTransaction<Transaction> other) {
|
|
|
|
return ReferenceEquals(other.WeightedTransaction.Transaction, this.toMatch);
|
2008-03-26 21:20:52 +00:00
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Transaction this instance compares against</summary>
|
|
|
|
private Transaction toMatch;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
#endregion // class TransactionMatcher
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
/// <summary>Triggered when the idle state of the tracker changes</summary>
|
|
|
|
/// <remarks>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// The tracker is idle when no transactions are being tracked in it. If you're
|
2008-03-26 21:20:52 +00:00
|
|
|
/// 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,
|
2008-12-03 18:58:20 +00:00
|
|
|
/// upon construction, its list of transactions will be empty.
|
2008-03-26 21:20:52 +00:00
|
|
|
/// </remarks>
|
|
|
|
public event EventHandler<IdleStateEventArgs> AsyncIdleStateChanged;
|
|
|
|
|
|
|
|
/// <summary>Triggered when the total progress has changed</summary>
|
|
|
|
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Initializes a new transaction tracker</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
public ProgressTracker() {
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
this.trackedTransactions = new List<ObservedWeightedTransaction<Transaction>>();
|
2008-03-26 21:20:52 +00:00
|
|
|
this.idle = true;
|
|
|
|
|
|
|
|
this.asyncEndedDelegate =
|
2008-12-03 18:58:20 +00:00
|
|
|
new ObservedWeightedTransaction<Transaction>.ReportDelegate(asyncEnded);
|
2008-03-26 21:20:52 +00:00
|
|
|
this.asyncProgressUpdatedDelegate =
|
2008-12-03 18:58:20 +00:00
|
|
|
new ObservedWeightedTransaction<Transaction>.ReportDelegate(asyncProgressChanged);
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Immediately releases all resources owned by the instance</summary>
|
|
|
|
public void Dispose() {
|
2008-12-03 18:58:20 +00:00
|
|
|
lock(this.trackedTransactions) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// Get rid of all transactions we're tracking. This unsubscribes the
|
|
|
|
// observers from the events of the transactions and stops us from
|
2008-03-26 21:20:52 +00:00
|
|
|
// being kept alive and receiving any further events if some of the
|
2008-12-03 18:58:20 +00:00
|
|
|
// tracked transactions are still executing.
|
|
|
|
for(int index = 0; index < this.trackedTransactions.Count; ++index)
|
|
|
|
this.trackedTransactions[index].Dispose();
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
// Help the GC a bit by untangling the references :)
|
2008-12-03 18:58:20 +00:00
|
|
|
this.trackedTransactions.Clear();
|
|
|
|
this.trackedTransactions = null;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
} // lock
|
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Begins tracking the specified background transactions</summary>
|
|
|
|
/// <param name="transaction">Background transaction to be tracked</param>
|
|
|
|
public void Track(Transaction transaction) {
|
|
|
|
Track(transaction, 1.0f);
|
2008-03-26 21:20:52 +00:00
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Begins tracking the specified background transaction</summary>
|
|
|
|
/// <param name="transaction">Background transaction to be tracked</param>
|
|
|
|
/// <param name="weight">Weight to assign to this background transaction</param>
|
|
|
|
public void Track(Transaction transaction, float weight) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// Add the new transaction into the tracking list. This has to be done
|
2008-03-26 21:20:52 +00:00
|
|
|
// inside a lock to prevent issues with the progressUpdate callback, which could
|
|
|
|
// access the totalWeight field before it has been updated to reflect the
|
2008-12-03 18:58:20 +00:00
|
|
|
// new transaction added to the collection.
|
|
|
|
lock(this.trackedTransactions) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
bool wasEmpty = (this.trackedTransactions.Count == 0);
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
if(transaction.Ended) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// If the ended transaction would become the only transaction in the list,
|
2008-03-26 21:20:52 +00:00
|
|
|
// there's no sense in doing anything at all because it would have to be
|
2008-12-03 18:58:20 +00:00
|
|
|
// thrown right out again. Only add the transaction when there are other
|
|
|
|
// running transactions to properly sum total progress for consistency.
|
2008-03-26 21:20:52 +00:00
|
|
|
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
|
2008-12-03 18:58:20 +00:00
|
|
|
// before the transaction has been added to the tracked transactions list.
|
|
|
|
this.trackedTransactions.Add(
|
|
|
|
new ObservedWeightedTransaction<Transaction>(
|
|
|
|
new WeightedTransaction<Transaction>(transaction, weight),
|
2008-03-26 21:20:52 +00:00
|
|
|
this.asyncProgressUpdatedDelegate,
|
|
|
|
this.asyncEndedDelegate
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2008-12-05 19:28:39 +00:00
|
|
|
} else { // Not ended -- Transaction is still running
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// Construct a new transation observer and add the transaction to our
|
|
|
|
// list of tracked transactions.
|
|
|
|
ObservedWeightedTransaction<Transaction> observedTransaction =
|
|
|
|
new ObservedWeightedTransaction<Transaction>(
|
|
|
|
new WeightedTransaction<Transaction>(transaction, weight),
|
2008-03-26 21:20:52 +00:00
|
|
|
this.asyncProgressUpdatedDelegate,
|
|
|
|
this.asyncEndedDelegate
|
|
|
|
);
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
this.trackedTransactions.Add(observedTransaction);
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// If this is the first transaction to be added to the list, tell our
|
2008-03-26 21:20:52 +00:00
|
|
|
// owner that we're idle no longer!
|
2008-12-05 19:28:39 +00:00
|
|
|
if(wasEmpty) {
|
2008-03-26 21:20:52 +00:00
|
|
|
setIdle(false);
|
2008-12-05 19:28:39 +00:00
|
|
|
}
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
} // if transaction ended
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
// All done, the total progress is different now, so force a recalculation and
|
|
|
|
// send out the AsyncProgressUpdated event.
|
|
|
|
recalculateProgress();
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
} // lock
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Stops tracking the specified background transaction</summary>
|
|
|
|
/// <param name="transaction">Background transaction to stop tracking of</param>
|
|
|
|
public void Untrack(Transaction transaction) {
|
|
|
|
lock(this.trackedTransactions) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
// Locate the object to be untracked in our collection
|
2010-09-17 01:43:00 +00:00
|
|
|
int index;
|
|
|
|
for(index = 0; index < this.trackedTransactions.Count; ++index) {
|
|
|
|
bool same = ReferenceEquals(
|
|
|
|
transaction,
|
|
|
|
this.trackedTransactions[index].WeightedTransaction.Transaction
|
|
|
|
);
|
|
|
|
if(same) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(index == this.trackedTransactions.Count) {
|
2008-12-05 19:28:39 +00:00
|
|
|
throw new ArgumentException("Specified transaction is not being tracked");
|
|
|
|
}
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// Remove and dispose the transaction the user wants to untrack
|
2008-03-26 21:20:52 +00:00
|
|
|
{
|
2008-12-03 18:58:20 +00:00
|
|
|
ObservedWeightedTransaction<Transaction> wrappedTransaction =
|
2010-09-17 01:43:00 +00:00
|
|
|
this.trackedTransactions[index];
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2010-09-17 01:43:00 +00:00
|
|
|
this.trackedTransactions.RemoveAt(index);
|
2008-12-03 18:58:20 +00:00
|
|
|
wrappedTransaction.Dispose();
|
2008-03-26 21:20:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the list is empty, then we're back in the idle state
|
2008-12-03 18:58:20 +00:00
|
|
|
if(this.trackedTransactions.Count == 0) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
this.totalWeight = 0.0f;
|
|
|
|
|
|
|
|
// If we entered the idle state with this call, report the state change!
|
|
|
|
setIdle(true);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// Rebuild the total weight from scratch. Subtracting the removed transaction's
|
2008-03-26 21:20:52 +00:00
|
|
|
// weight would work, too, but we might accumulate rounding errors making the sum
|
|
|
|
// drift slowly away from the actual value.
|
2008-12-05 19:28:39 +00:00
|
|
|
float newTotalWeight = 0.0f;
|
2010-09-17 01:43:00 +00:00
|
|
|
for(index = 0; index < this.trackedTransactions.Count; ++index)
|
2008-12-05 19:28:39 +00:00
|
|
|
newTotalWeight += this.trackedTransactions[index].WeightedTransaction.Weight;
|
|
|
|
|
|
|
|
this.totalWeight = newTotalWeight;
|
|
|
|
|
|
|
|
recalculateProgress();
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} // lock
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Whether the tracker is currently idle</summary>
|
|
|
|
public bool Idle {
|
|
|
|
get { return this.idle; }
|
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Current summed progress of the tracked transactions</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
public float Progress {
|
|
|
|
get { return this.progress; }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Fires the AsyncIdleStateChanged event</summary>
|
|
|
|
/// <param name="idle">New idle state to report</param>
|
|
|
|
protected virtual void OnAsyncIdleStateChanged(bool idle) {
|
|
|
|
EventHandler<IdleStateEventArgs> copy = AsyncIdleStateChanged;
|
|
|
|
if(copy != null)
|
|
|
|
copy(this, new IdleStateEventArgs(idle));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Fires the AsyncProgressUpdated event</summary>
|
|
|
|
/// <param name="progress">New progress to report</param>
|
|
|
|
protected virtual void OnAsyncProgressUpdated(float progress) {
|
|
|
|
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
|
|
if(copy != null)
|
|
|
|
copy(this, new ProgressReportEventArgs(progress));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Recalculates the total progress of the tracker</summary>
|
|
|
|
private void recalculateProgress() {
|
2008-12-05 19:28:39 +00:00
|
|
|
bool progressChanged = false;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
// Lock the collection to avoid trouble when someone tries to remove one
|
2008-12-03 18:58:20 +00:00
|
|
|
// of our tracked transactions while we're just doing a progress update
|
|
|
|
lock(this.trackedTransactions) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// This is a safety measure. In theory, even after all transactions have
|
|
|
|
// ended and the collection of tracked transactions is cleared, a waiting
|
2008-03-26 21:20:52 +00:00
|
|
|
// thread might deliver another progress update causing this method to
|
|
|
|
// be entered. In this case, the right thing is to do nothing at all.
|
2008-12-05 19:28:39 +00:00
|
|
|
if(this.totalWeight != 0.0f) {
|
|
|
|
float totalProgress = 0.0f;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-05 19:28:39 +00:00
|
|
|
// Sum up the total progress
|
|
|
|
for(int index = 0; index < this.trackedTransactions.Count; ++index) {
|
|
|
|
float weight = this.trackedTransactions[index].WeightedTransaction.Weight;
|
|
|
|
totalProgress += this.trackedTransactions[index].Progress * weight;
|
|
|
|
}
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-05 19:28:39 +00:00
|
|
|
// This also needs to be in the lock to guarantee that the total weight
|
|
|
|
// corresponds to the number of transactions we just summed -- by design,
|
|
|
|
// the total weight always has to be updated at the same time as the collection.
|
|
|
|
totalProgress /= this.totalWeight;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-05 19:28:39 +00:00
|
|
|
if(totalProgress != this.progress) {
|
|
|
|
this.progress = totalProgress;
|
|
|
|
progressChanged = true;
|
|
|
|
}
|
2008-12-03 18:58:20 +00:00
|
|
|
}
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
} // lock
|
2008-12-05 19:28:39 +00:00
|
|
|
|
|
|
|
// Finally, trigger the event if the progress has changed
|
|
|
|
if(progressChanged) {
|
|
|
|
OnAsyncProgressUpdated(this.progress);
|
|
|
|
}
|
2008-03-26 21:20:52 +00:00
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Called when one of the tracked transactions has ended</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
private void asyncEnded() {
|
2008-12-03 18:58:20 +00:00
|
|
|
lock(this.trackedTransactions) {
|
2008-03-26 21:20:52 +00:00
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// If any transactions in the list are still going, keep the entire list.
|
2008-03-26 21:20:52 +00:00
|
|
|
// This behavior is intentional in order to prevent the tracker's progress from
|
2008-12-03 18:58:20 +00:00
|
|
|
// jumping back repeatedly when multiple tracked transactions come to an end.
|
|
|
|
for(int index = 0; index < this.trackedTransactions.Count; ++index)
|
|
|
|
if(!this.trackedTransactions[index].WeightedTransaction.Transaction.Ended)
|
2008-03-26 21:20:52 +00:00
|
|
|
return;
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
// All transactions have finished, get rid of the wrappers and make a
|
|
|
|
// fresh start for future transactions to be tracked. No need to call
|
|
|
|
// Dispose() since, as a matter of fact, when the transaction
|
|
|
|
this.trackedTransactions.Clear();
|
2008-03-26 21:20:52 +00:00
|
|
|
this.totalWeight = 0.0f;
|
|
|
|
|
|
|
|
// Notify our owner that we're idle now. This line is only reached when all
|
2008-12-03 18:58:20 +00:00
|
|
|
// transactions were finished, so it's safe to trigger this here.
|
2008-03-26 21:20:52 +00:00
|
|
|
setIdle(true);
|
|
|
|
|
|
|
|
} // lock
|
|
|
|
}
|
|
|
|
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Called when one of the tracked transactions has achieved progress</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
private void asyncProgressChanged() {
|
|
|
|
recalculateProgress();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Changes the idle state</summary>
|
|
|
|
/// <param name="idle">Whether or not the tracker is currently idle</param>
|
|
|
|
/// <remarks>
|
2008-12-03 18:58:20 +00:00
|
|
|
/// This method expects to be called during a lock() on trackedTransactions!
|
2008-03-26 21:20:52 +00:00
|
|
|
/// </remarks>
|
|
|
|
private void setIdle(bool idle) {
|
|
|
|
this.idle = idle;
|
|
|
|
|
|
|
|
OnAsyncIdleStateChanged(idle);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>Whether the tracker is currently idle</summary>
|
|
|
|
private volatile bool idle;
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Current summed progress of the tracked transactions</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
private volatile float progress;
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Total weight of all transactions being tracked</summary>
|
2008-03-26 21:20:52 +00:00
|
|
|
private volatile float totalWeight;
|
2008-12-03 18:58:20 +00:00
|
|
|
/// <summary>Transactions being tracked by this tracker</summary>
|
|
|
|
private List<ObservedWeightedTransaction<Transaction>> trackedTransactions;
|
2008-03-26 21:20:52 +00:00
|
|
|
/// <summary>Delegate for the asyncEnded() method</summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
private ObservedWeightedTransaction<Transaction>.ReportDelegate asyncEndedDelegate;
|
2008-03-26 21:20:52 +00:00
|
|
|
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
|
2008-12-03 18:58:20 +00:00
|
|
|
private ObservedWeightedTransaction<Transaction>.ReportDelegate asyncProgressUpdatedDelegate;
|
2008-03-26 21:20:52 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Nuclex.Support.Tracking
|