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 } }