using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HdSystemLibrary.IO
{
public class ProgressEventArgs : EventArgs
{
public ProgressEventArgs(ProgressStatistic progressStatistic)
{
if (progressStatistic == null)
throw new ArgumentNullException("progressStatistic");
ProgressStatistic = progressStatistic;
}
public ProgressStatistic ProgressStatistic { get; private set; }
}
[Serializable]
public class OperationAlreadyStartedException : Exception
{
public OperationAlreadyStartedException() { }
public OperationAlreadyStartedException(string message) : base(message) { }
public OperationAlreadyStartedException(string message, Exception inner) : base(message, inner) { }
protected OperationAlreadyStartedException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context) { }
}
///
/// A class which calculates progress statistics like average bytes per second or estimated finishing time.
/// To use it, call the ProgressChange method in regular intervals with the actual progress.
///
public class ProgressStatistic
{
public ProgressStatistic()
{
StartingTime = DateTime.MinValue;
FinishingTime = DateTime.MinValue;
progressChangedArgs = new ProgressEventArgs(this); //Event args can be cached
}
private bool hasStarted = false;
///
/// Gets whether the operation has started
///
public bool HasStarted { get { return hasStarted; } }
///
/// Gets whether the operation has finished
///
public bool HasFinished { get { return FinishingTime != DateTime.MinValue; } }
///
/// Gets whether the operation is still running
///
public bool IsRunning { get { return HasStarted && !HasFinished; } }
#region Time
///
/// Gets the date time when the operation has started
///
public DateTime StartingTime { get; private set; }
///
/// Gets the date time when the operation has finished
///
public DateTime FinishingTime { get; private set; }
///
/// Gets the duration of the operation.
/// If the operation is still running, the time since starting is returned.
/// If the operation has not started, TimeSpan.Zero is returned.
/// If the operation has finished, the time between starting and finishing is returned.
///
public TimeSpan Duration
{
get
{
if (!HasStarted)
return TimeSpan.Zero;
else if (!HasFinished)
return DateTime.Now - StartingTime;
else
return FinishingTime - StartingTime;
}
}
///
/// The method which will be used for estimating duration and finishing time
///
public enum EstimatingMethod
{
///
/// Current bytes per second will be used for estimating.
///
CurrentBytesPerSecond,
///
/// Average bytes per second will be used for estimating
///
AverageBytesPerSecond
}
private EstimatingMethod estimatingMethod = EstimatingMethod.CurrentBytesPerSecond;
///
/// Gets or sets which method will be used for estimating.
/// Can only be set before the operation has started, otherwise an OperationAlreadyStartedException will be thrown.
///
public EstimatingMethod UsedEstimatingMethod
{
get { return estimatingMethod; }
set
{
if (HasStarted)
throw new OperationAlreadyStartedException();
estimatingMethod = value;
}
}
///
/// Gets the estimated duration. Use UsedEstimatingMethod to specify which method will be used for estimating.
/// If the operation will take more than 200 days, TimeSpan.MaxValue is returned.
///
public TimeSpan EstimatedDuration
{
get
{
if (HasFinished)
return Duration;
if (TotalBytesToRead == -1)
return TimeSpan.MaxValue;
double bytesPerSecond = 1;
if (UsedEstimatingMethod == EstimatingMethod.AverageBytesPerSecond)
bytesPerSecond = AverageBytesPerSecond;
else if (UsedEstimatingMethod == EstimatingMethod.CurrentBytesPerSecond)
bytesPerSecond = CurrentBytesPerSecond;
double seconds = (TotalBytesToRead - BytesRead) / bytesPerSecond;
if (seconds > 60 * 60 * 24 * 200) //over 200 Days -> infinite
return TimeSpan.MaxValue;
else
return Duration + TimeSpan.FromSeconds(seconds);
}
}
///
/// Gets the estimated finishing time based on EstimatedDuration.
/// If the operation will take more than 200 days, DateTime.MaxValue is returned.
/// If the operation has finished, the actual finishing time is returned.
///
public DateTime EstimatedFinishingTime
{
get
{
if (EstimatedDuration == TimeSpan.MaxValue)
return DateTime.MaxValue;
return StartingTime + EstimatedDuration;
}
}
#endregion
///
/// Gets the amount of bytes already read.
///
public long BytesRead { get; private set; }
///
/// Gets the amount of total bytes to read. Can be -1 if unknown.
///
public long TotalBytesToRead { get; private set; }
///
/// Gets the progress in percent between 0 and 1.
/// If the amount of total bytes to read is unknown, -1 is returned.
///
public double Progress
{
get
{
if (TotalBytesToRead == -1)
return -1;
else
return (double)BytesRead / (double)TotalBytesToRead;
}
}
///
/// Gets the average bytes per second.
///
public double AverageBytesPerSecond { get { return (double)BytesRead / Duration.TotalSeconds; } }
#region CurrentBytesPerSecond
///
/// Gets the approximated current count of bytes processed per second
///
public double CurrentBytesPerSecond { get; private set; }
private TimeSpan currentBytesCalculationInterval = TimeSpan.FromSeconds(0.5);
///
/// Gets or sets the interval used for the calculation of the current bytes per second. Default is 500 ms.
///
///
/// Thrown when trying to set although the operation has already started.
public TimeSpan CurrentBytesCalculationInterval
{
get { return currentBytesCalculationInterval; }
set
{
if (HasStarted)
throw new InvalidOperationException("Task has already started!");
currentBytesCalculationInterval = value;
}
}
KeyValuePair[] currentBytesSamples = new KeyValuePair[6];
///
/// Gets or sets the number of samples in CurrentBytesPerSecondInterval used for current bytes per second approximation
///
///
/// Thrown when trying to set although the operation has already started.
public int CurrentBytesSampleCount
{
get { return currentBytesSamples.Length; }
set
{
if (HasStarted)
throw new InvalidOperationException("Task has already started!");
if (value != currentBytesSamples.Length)
{
currentBytesSamples = new KeyValuePair[value];
}
}
}
int currentSample = 0; //current sample index in currentBytesSamples
DateTime lastSample;
private void ProcessSample(long bytes)
{
if ((DateTime.Now - lastSample).Ticks > CurrentBytesCalculationInterval.Ticks / currentBytesSamples.Length)
{
lastSample = DateTime.Now;
KeyValuePair current = new KeyValuePair(DateTime.Now, bytes);
var old = currentBytesSamples[currentSample];
currentBytesSamples[currentSample] = current;
if (old.Key == DateTime.MinValue)
CurrentBytesPerSecond = AverageBytesPerSecond;
else
CurrentBytesPerSecond = (double)(current.Value - old.Value) / (current.Key - old.Key).TotalSeconds;
currentSample++;
if (currentSample >= currentBytesSamples.Length)
currentSample = 0;
}
}
#endregion
///
/// This method can be called to report progress changes.
/// The signature of this method is compliant with the ProgressChange-delegate
///
/// The amount of bytes already read
/// The amount of total bytes to read. Can be -1 if unknown.
/// Thrown if bytesRead has not changed or even shrunk.
/// Thrown if the operation has finished already.
public virtual void ProgressChange(long bytesRead, long totalBytesToRead)
{
if (bytesRead <= BytesRead)
throw new ArgumentException("Operation cannot go backwards!", "bytesRead");
if (HasFinished)
throw new InvalidOperationException("Operation has finished already!");
if (!hasStarted)
{
StartingTime = DateTime.Now;
hasStarted = true;
OnStarted();
}
BytesRead = bytesRead;
TotalBytesToRead = totalBytesToRead;
ProcessSample(bytesRead);
OnProgressChanged();
if (bytesRead == TotalBytesToRead)
{
FinishingTime = DateTime.Now;
OnFinished();
}
}
///
/// This method can be called to finish an aborted operation.
/// If the operation does not reach 100%, "Finished" will be never raised, so this method should be called.
///
public virtual void Finish()
{
if (!HasFinished)
{
FinishingTime = DateTime.Now;
OnFinished();
}
}
#region Events
private readonly ProgressEventArgs progressChangedArgs;
protected virtual void OnStarted()
{
if (Started != null)
Started(this, progressChangedArgs);
}
protected virtual void OnProgressChanged()
{
if (ProgressChanged != null)
ProgressChanged(this, progressChangedArgs);
}
protected virtual void OnFinished()
{
if (Finished != null)
Finished(this, progressChangedArgs);
}
///
/// Will be raised when the operation has started
///
public event EventHandler Started;
///
/// Will be raised when the progress has changed
///
public event EventHandler ProgressChanged;
///
/// Will be raised when the operation has finished
///
public event EventHandler Finished;
#endregion
}
}