source: trunk/eraser/Eraser.Util/ProgressManager.cs @ 2202

Revision 2202, 21.4 KB checked in by lowjoel, 4 years ago (diff)

Unset the Indeterminate flag when:

  • The Completed property is set
  • The MarkComplete? function is called
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1/*
2 * $Id$
3 * Copyright 2008-2010 The Eraser Project
4 * Original Author: Joel Low <lowjoel@users.sourceforge.net>
5 * Modified By:
6 *
7 * This file is part of Eraser.
8 *
9 * Eraser is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later
12 * version.
13 *
14 * Eraser is distributed in the hope that it will be useful, but WITHOUT ANY
15 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 *
18 * A copy of the GNU General Public License can be found at
19 * <http://www.gnu.org/licenses/>.
20 */
21
22using System;
23using System.Collections.Generic;
24using System.Linq;
25using System.Text;
26
27namespace Eraser.Util
28{
29    /// <summary>
30    /// Manages the progress for any operation.
31    /// </summary>
32    public abstract class ProgressManagerBase
33    {
34        /// <summary>
35        /// Constructor.
36        ///
37        /// This sets the starting time of this task to allow the computation of
38        /// the estimated end time by extrapolating collected data based on the
39        /// amount of time already elapsed.
40        /// </summary>
41        protected ProgressManagerBase()
42        {
43            StartTime = DateTime.Now;
44        }
45
46        /// <summary>
47        /// Resets the starting time of the task. The speed measurement is
48        /// automatically started when the ProgressManagerBase object is created.
49        /// </summary>
50        public void Restart()
51        {
52            StartTime = DateTime.Now;
53            lock (Speeds)
54                Speeds.Reset();
55        }
56
57        /// <summary>
58        /// Gets the percentage of the operation completed.
59        /// </summary>
60        /// <remarks>If the <see cref="ProgressIndeterminate"/> property is true, this
61        /// property will return <see cref="System.Float.NaN"/>.</remarks>
62        public abstract float Progress
63        {
64            get;
65        }
66
67        /// <summary>
68        /// Gets whether the current progress is undefined.
69        /// </summary>
70        public abstract bool ProgressIndeterminate
71        {
72            get;
73        }
74
75        /// <summary>
76        /// Computes the speed of the erase, in percentage of completion per second,
77        /// based on the information collected in the previous 15 seconds.
78        /// </summary>
79        public abstract float Speed
80        {
81            get;
82        }
83
84        /// <summary>
85        /// Calculates the estimated amount of time left based on the total
86        /// amount of information to erase and the current speed of the erase
87        /// </summary>
88        public abstract TimeSpan TimeLeft
89        {
90            get;
91        }
92
93        /// <summary>
94        /// The starting time of this task.
95        /// </summary>
96        public DateTime StartTime
97        {
98            get;
99            private set;
100        }
101
102        /// <summary>
103        /// Samples the current speed of the task.
104        /// </summary>
105        /// <param name="speed">The speed of the task.</param>
106        protected void SampleSpeed(float speed)
107        {
108            lock (Speeds)
109            {
110                IList<KeyValuePair<DateTime, double>> outliers = Speeds.GetOutliers(0.95);
111                if (outliers != null)
112                {
113                    List<KeyValuePair<DateTime, double>> recentOutliers = outliers.Where(
114                        sample => (DateTime.Now - sample.Key).TotalSeconds <= 60).ToList();
115                    if (recentOutliers.Count >= 5)
116                    {
117                        Speeds.Reset();
118                        recentOutliers.ForEach(sample => Speeds.Add(sample.Value));
119                    }
120                }
121
122                Speeds.Add(speed);
123                PredictedSpeed = Speeds.Predict(0.95);
124            }
125        }
126
127        /// <summary>
128        /// Predicts the speed of the operation, given the current samples.
129        /// </summary>
130        protected Interval PredictedSpeed
131        {
132            get;
133            private set;
134        }
135
136        /// <summary>
137        /// The speed sampler.
138        /// </summary>
139        private Sampler Speeds = new Sampler();
140    }
141
142    /// <summary>
143    /// Manages progress based only on one input, set through the Completed and Total
144    /// properties.
145    /// </summary>
146    public class ProgressManager : ProgressManagerBase
147    {
148        /// <summary>
149        /// Marks this task's progress as indeterminate.
150        /// </summary>
151        public void MarkIndeterminate()
152        {
153            progressIndeterminate = true;
154        }
155
156        /// <summary>
157        /// Marks this task as complete.
158        /// </summary>
159        public void MarkComplete()
160        {
161            progressIndeterminate = false;
162            if (total == 0)
163                completed = total = 1;
164            else
165                completed = total;
166        }
167
168        /// <summary>
169        /// Gets or sets the number of work units already completed.
170        /// </summary>
171        /// <remarks>This unsets the Indeterminate flag for the progress of this Task.</remarks>
172        public long Completed
173        {
174            get
175            {
176                return completed;
177            }
178            set
179            {
180                if (value > Total)
181                    throw new ArgumentOutOfRangeException("value", value, "The Completed " +
182                        "property of the Progress Manager cannot exceed the total work units for " +
183                        "the task.");
184
185                completed = value;
186                progressIndeterminate = false;
187            }
188        }
189
190        /// <summary>
191        /// Gets or sets the total number of work units that this task has.
192        /// </summary>
193        public long Total
194        {
195            get
196            {
197                return total;
198            }
199            set
200            {
201                if (value < Completed)
202                    throw new ArgumentOutOfRangeException("value", value, "The Total property " +
203                        "of the Progress Manager must be greater than or equal to the completed " +
204                        "work units for the task.");
205
206                total = value;
207            }
208        }
209
210        public override float Progress
211        {
212            get
213            {
214                if (Total == 0)
215                    return 0.0f;
216                else if (ProgressIndeterminate)
217                    return float.NaN;
218
219                return (float)((double)Completed / Total);
220            }
221        }
222
223        public override bool ProgressIndeterminate
224        {
225            get
226            {
227                return progressIndeterminate;
228            }
229        }
230
231        /// <summary>
232        /// Stores whether the progress of the current task cannot be determined.
233        /// </summary>
234        /// <see cref="ProgressIndeterminate"/>
235        private bool progressIndeterminate;
236
237        public override float Speed
238        {
239            get
240            {
241                if ((DateTime.Now - lastSpeedCalc).Seconds <= 1 && lastSpeed != 0)
242                    return lastSpeed;
243
244                //Calculate how much time has passed
245                double timeElapsed = (DateTime.Now - lastSpeedCalc).TotalSeconds;
246                if (timeElapsed == 0.0)
247                    return 0;
248
249                //Then compute the speed of the calculation
250                long progressDelta = Completed - lastCompleted;
251                float currentSpeed = (float)(progressDelta / timeElapsed / total);
252                lastSpeedCalc = DateTime.Now;
253                lastCompleted = Completed;
254
255                //We won't update the speed of the task if the current speed is within
256                //the lower and upper prediction interval.
257                Interval interval = PredictedSpeed;
258                if (interval != null)
259                {
260                    if (currentSpeed < interval.Minimum)
261                    {
262                        Restart();
263                        lastSpeed = currentSpeed;
264                    }
265                    else if (currentSpeed > interval.Maximum)
266                    {
267                        Restart();
268                        lastSpeed = currentSpeed;
269                    }
270                    else if (lastSpeed == 0.0f)
271                    {
272                        lastSpeed = currentSpeed;
273                    }
274                }
275
276                SampleSpeed(currentSpeed);
277                return lastSpeed;
278            }
279        }
280
281        public override TimeSpan TimeLeft
282        {
283            get
284            {
285                float speed = Speed;
286                if (speed == 0.0)
287                    return TimeSpan.MinValue;
288
289                return TimeSpan.FromSeconds((1.0f - Progress) / speed);
290            }
291        }
292
293        /// <summary>
294        /// The last time a speed calculation was computed so that speed is not
295        /// computed too often.
296        /// </summary>
297        private DateTime lastSpeedCalc;
298
299        /// <summary>
300        /// The amount of the operation completed at the last speed computation.
301        /// </summary>
302        private long lastCompleted;
303
304        /// <summary>
305        /// The last calculated speed of the operation.
306        /// </summary>
307        private float lastSpeed;
308
309        /// <summary>
310        /// The backing field for <see cref="Completed"/>
311        /// </summary>
312        private long completed;
313
314        /// <summary>
315        /// The backing field for <see cref="Total"/>
316        /// </summary>
317        private long total;
318    }
319
320    /// <summary>
321    /// Manages progress based on sub-tasks.
322    /// </summary>
323    public abstract class ChainedProgressManager : ProgressManagerBase
324    {
325    }
326
327    /// <summary>
328    /// Manages progress based on sub-tasks, taking each sub-task to be a step
329    /// in which the next step will not be executed until the current step is
330    /// complete. Each step is also assign weights so that certain steps which
331    /// take more time are given a larger amount of progress-bar space for finer
332    /// grained progress reporting.
333    /// </summary>
334    public class SteppedProgressManager : ChainedProgressManager
335    {
336        /// <summary>
337        /// The class which manages the steps which comprise the overall progress.
338        /// </summary>
339        private class StepsList : IList<SteppedProgressManagerStep>
340        {
341            public StepsList(SteppedProgressManager manager)
342            {
343                List = new List<SteppedProgressManagerStep>();
344                ListLock = manager.ListLock;
345            }
346
347            #region IList<SteppedProgressManagerStep> Members
348
349            public int IndexOf(SteppedProgressManagerStep item)
350            {
351                lock (ListLock)
352                    return List.IndexOf(item);
353            }
354
355            public void Insert(int index, SteppedProgressManagerStep item)
356            {
357                lock (ListLock)
358                {
359                    List.Insert(index, item);
360                    TotalWeights += item.Weight;
361                }
362            }
363
364            public void RemoveAt(int index)
365            {
366                lock (ListLock)
367                {
368                    TotalWeights -= List[index].Weight;
369                    List.RemoveAt(index);
370                }
371            }
372
373            public SteppedProgressManagerStep this[int index]
374            {
375                get
376                {
377                    lock (ListLock)
378                        return List[index];
379                }
380                set
381                {
382                    lock (ListLock)
383                    {
384                        TotalWeights -= List[index].Weight;
385                        List[index] = value;
386                        TotalWeights += value.Weight;
387                    }
388                }
389            }
390
391            #endregion
392
393            #region ICollection<SteppedProgressManagerStep> Members
394
395            public void Add(SteppedProgressManagerStep item)
396            {
397                lock (ListLock)
398                {
399                    List.Add(item);
400                    TotalWeights += item.Weight;
401                }
402            }
403
404            public void Clear()
405            {
406                lock (ListLock)
407                {
408                    List.Clear();
409                    TotalWeights = 0;
410                }
411            }
412
413            public bool Contains(SteppedProgressManagerStep item)
414            {
415                lock (ListLock)
416                    return List.Contains(item);
417            }
418
419            public void CopyTo(SteppedProgressManagerStep[] array, int arrayIndex)
420            {
421                lock (ListLock)
422                    List.CopyTo(array, arrayIndex);
423            }
424
425            public int Count
426            {
427                get
428                {
429                    lock (ListLock) 
430                        return List.Count;
431                }
432            }
433
434            public bool IsReadOnly
435            {
436                get { return false; }
437            }
438
439            public bool Remove(SteppedProgressManagerStep item)
440            {
441                int index = List.IndexOf(item);
442                if (index != -1)
443                    TotalWeights -= List[index].Weight;
444
445                return List.Remove(item);
446            }
447
448            #endregion
449
450            #region IEnumerable<SteppedProgressManagerStep> Members
451
452            public IEnumerator<SteppedProgressManagerStep> GetEnumerator()
453            {
454                return List.GetEnumerator();
455            }
456
457            #endregion
458
459            #region IEnumerable Members
460
461            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
462            {
463                return List.GetEnumerator();
464            }
465
466            #endregion
467
468            /// <summary>
469            /// The total weights of all the steps.
470            /// </summary>
471            private float TotalWeights
472            {
473                get
474                {
475                    return totalWeights;
476                }
477                set
478                {
479                    if (value >= 1.1f || value < 0.0f)
480                        throw new ArgumentOutOfRangeException("value", "The total weights of " +
481                            "all steps in the task must be within the range [0.0, 1.0]");
482
483                    totalWeights = value;
484                }
485            }
486
487            /// <summary>
488            /// The list storing the steps for this instance.
489            /// </summary>
490            private List<SteppedProgressManagerStep> List;
491
492            /// <summary>
493            /// The lock object guarding the list against parallel writes.
494            /// </summary>
495            private object ListLock;
496
497            /// <summary>
498            /// The backing variable for the total weights of all the steps.
499            /// </summary>
500            private float totalWeights;
501        }
502
503        /// <summary>
504        /// Constructor.
505        /// </summary>
506        public SteppedProgressManager()
507        {
508            ListLock = new object();
509            Steps = new StepsList(this);
510        }
511
512        public override float Progress
513        {
514            get
515            {
516                lock (ListLock)
517                    return Steps.Sum(step => step.Progress.Progress * step.Weight);
518            }
519        }
520
521        public override bool ProgressIndeterminate
522        {
523            get
524            {
525                lock (ListLock)
526                    return Steps.Any(x => x.Progress.ProgressIndeterminate);
527            }
528        }
529
530        public override float Speed
531        {
532            get
533            {
534                if ((DateTime.Now - lastSpeedCalc).TotalSeconds < SpeedCalcInterval)
535                    return lastSpeed;
536
537                //Calculate how much time has passed
538                double timeElapsed = (DateTime.Now - lastSpeedCalc).TotalSeconds;
539
540                //Then compute the speed of the calculation
541                float currentProgress = Progress;
542                float progressDelta = currentProgress - lastCompleted;
543                float currentSpeed = (float)(progressDelta / timeElapsed);
544                lastSpeedCalc = DateTime.Now;
545                lastCompleted = Progress;
546
547                //If the progress delta is zero, it usually means that the amount
548                //completed within the calculation interval is too short -- lengthen
549                //the interval so we can get a small difference, significant to make
550                //a speed calculation. Likewise, if it is too great a difference,
551                //we need to shorten the interval to get more accurate calculations
552                if (progressDelta == 0.0)
553                    SpeedCalcInterval += SpeedCalcInterval / 3;
554                else if (progressDelta > 0.01 && SpeedCalcInterval > 6)
555                    SpeedCalcInterval -= 3;
556
557                //We won't update the speed of the task if the current speed is within
558                //the lower and upper prediction interval.
559                Interval interval = PredictedSpeed;
560                if (interval != null)
561                {
562                    if (currentSpeed < interval.Minimum)
563                    {
564                        Restart();
565                        lastSpeed = currentSpeed;
566                    }
567                    else if (currentSpeed > interval.Maximum)
568                    {
569                        Restart();
570                        lastSpeed = currentSpeed;
571                    }
572                    else if (lastSpeed == 0.0f)
573                    {
574                        lastSpeed = currentSpeed;
575                    }
576                }
577
578                SampleSpeed(currentSpeed);
579                return lastSpeed;
580            }
581        }
582
583        public override TimeSpan TimeLeft
584        {
585            get
586            {
587                float speed = Speed;
588                float remaining = 1.0f - Progress;
589
590                if (speed == 0)
591                    return TimeSpan.MinValue;
592                else if (remaining <= 0)
593                    return TimeSpan.Zero;
594
595                try
596                {
597                    return TimeSpan.FromSeconds(remaining / speed);
598                }
599                catch (OverflowException)
600                {
601                    return TimeSpan.MaxValue;
602                }
603            }
604        }
605
606        /// <summary>
607        /// The list of steps involved in completion of the task.
608        /// </summary>
609        public IList<SteppedProgressManagerStep> Steps
610        {
611            get;
612            private set;
613        }
614
615        /// <summary>
616        /// Gets the current step which is executing. This property is null if
617        /// no steps are executing (also when the task is complete)
618        /// </summary>
619        public SteppedProgressManagerStep CurrentStep
620        {
621            get
622            {
623                lock (ListLock)
624                {
625                    if (Steps.Count == 0)
626                        return null;
627
628                    foreach (SteppedProgressManagerStep step in Steps)
629                        if (step.Progress.Progress < 1.0f)
630                            return step;
631
632                    //Return the last step since we don't have any
633                    return Steps[Steps.Count - 1];
634                }
635            }
636        }
637
638        /// <summary>
639        /// The lock object guarding the list of steps against concurrent read and write.
640        /// </summary>
641        private object ListLock;
642
643        /// <summary>
644        /// The amount of time elapsed before a new speed calculation is made.
645        /// </summary>
646        private int SpeedCalcInterval = 15;
647
648        /// <summary>
649        /// The last time a speed calculation was computed so that speed is not
650        /// computed too often.
651        /// </summary>
652        private DateTime lastSpeedCalc;
653
654        /// <summary>
655        /// The amount of the operation completed at the last speed computation.
656        /// </summary>
657        private float lastCompleted;
658
659        /// <summary>
660        /// The last calculated speed of the operation.
661        /// </summary>
662        private float lastSpeed;
663    }
664
665    /// <summary>
666    /// Represents one step in the list of steps to complete.
667    /// </summary>
668    public class SteppedProgressManagerStep
669    {
670        /// <summary>
671        /// Constructor.
672        /// </summary>
673        /// <param name="progress">The <see cref="ProgressManagerBase"/> instance
674        /// which measures the progress of this step.</param>
675        /// <param name="weight">The weight of this step. The weight is a decimal
676        /// number in the range [0.0, 1.0] which represents the percentage of the
677        /// entire process this particular step is.</param>
678        public SteppedProgressManagerStep(ProgressManagerBase progress, float weight)
679            : this(progress, weight, null)
680        {
681        }
682
683        /// <summary>
684        /// Constructor.
685        /// </summary>
686        /// <param name="progress">The <see cref="ProgressManagerBase"/> instance
687        /// which measures the progress of this step.</param>
688        /// <param name="weight">The weight of this step. The weight is a decimal
689        /// number in the range [0.0, 1.0] which represents the percentage of the
690        /// entire process this particular step is.</param>
691        /// <param name="name">A user-specified value of the name of this step.
692        /// This value is not used by the class at all.</param>
693        public SteppedProgressManagerStep(ProgressManagerBase progress, float weight, string name)
694        {
695            if (float.IsInfinity(weight) || float.IsNaN(weight))
696                throw new ArgumentException(S._("The weight of a progress manager step must be " +
697                    "a valid floatint-point value."));
698
699            Progress = progress;
700            Weight = weight;
701            Name = name;
702        }
703
704        /// <summary>
705        /// The <see cref="ProgressManagerBase"/> instance which measures the
706        /// progress of the step.
707        /// </summary>
708        public ProgressManagerBase Progress
709        {
710            get;
711            set;
712        }
713
714        /// <summary>
715        /// The weight associated with this step.
716        /// </summary>
717        public float Weight
718        {
719            get;
720            private set;
721        }
722
723        /// <summary>
724        /// The name of this step.
725        /// </summary>
726        public string Name
727        {
728            get;
729            set;
730        }
731    }
732
733    /// <summary>
734    /// Manages progress based on sub-tasks, assuming each sub-task to be independent
735    /// of the rest.
736    /// </summary>
737    public class ParallelProgressManager : ChainedProgressManager
738    {
739        /// <summary>
740        /// The class which manages the progress of each dependent task.
741        /// </summary>
742        private class SubTasksList : IList<ProgressManagerBase>
743        {
744            public SubTasksList(ParallelProgressManager manager)
745            {
746                List = new List<ProgressManagerBase>();
747                ListLock = manager.TaskLock;
748            }
749
750            #region IList<SubTasksList> Members
751
752            public int IndexOf(ProgressManagerBase item)
753            {
754                lock (ListLock)
755                    return List.IndexOf(item);
756            }
757
758            public void Insert(int index, ProgressManagerBase item)
759            {
760                lock (ListLock)
761                    List.Insert(index, item);
762            }
763
764            public void RemoveAt(int index)
765            {
766                lock (ListLock)
767                    List.RemoveAt(index);
768            }
769
770            public ProgressManagerBase this[int index]
771            {
772                get
773                {
774                    lock (ListLock)
775                        return List[index];
776                }
777                set
778                {
779                    lock (ListLock)
780                        List[index] = value;
781                }
782            }
783
784            #endregion
785
786            #region ICollection<SteppedProgressManagerStep> Members
787
788            public void Add(ProgressManagerBase item)
789            {
790                lock (ListLock)
791                    List.Add(item);
792            }
793
794            public void Clear()
795            {
796                lock (ListLock)
797                    List.Clear();
798            }
799
800            public bool Contains(ProgressManagerBase item)
801            {
802                return List.Contains(item);
803            }
804
805            public void CopyTo(ProgressManagerBase[] array, int arrayIndex)
806            {
807                lock (ListLock)
808                    List.CopyTo(array, arrayIndex);
809            }
810
811            public int Count
812            {
813                get
814                {
815                    lock (ListLock) 
816                        return List.Count;
817                }
818            }
819
820            public bool IsReadOnly
821            {
822                get { return false; }
823            }
824
825            public bool Remove(ProgressManagerBase item)
826            {
827                lock (ListLock)
828                    return List.Remove(item);
829            }
830
831            #endregion
832
833            #region IEnumerable<ProgressManagerBase> Members
834
835            public IEnumerator<ProgressManagerBase> GetEnumerator()
836            {
837                return List.GetEnumerator();
838            }
839
840            #endregion
841
842            #region IEnumerable Members
843
844            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
845            {
846                return List.GetEnumerator();
847            }
848
849            #endregion
850
851            /// <summary>
852            /// The list storing the steps for this instance.
853            /// </summary>
854            private List<ProgressManagerBase> List;
855
856            /// <summary>
857            /// The lock object guarding the list from concurrent read/writes.
858            /// </summary>
859            private object ListLock;
860        }
861
862        /// <summary>
863        /// Constructor.
864        /// </summary>
865        public ParallelProgressManager()
866        {
867            Tasks = new SubTasksList(this);
868            TaskLock = new object();
869        }
870
871        public override float Progress
872        {
873            get
874            {
875                lock (TaskLock)
876                    return Tasks.Sum(task => task.Progress * (1.0f / Tasks.Count));
877            }
878        }
879
880        public override bool ProgressIndeterminate
881        {
882            get
883            {
884                lock (TaskLock)
885                    return Tasks.Any(x => x.ProgressIndeterminate);
886            }
887        }
888
889        public override float Speed
890        {
891            get
892            {
893                lock (TaskLock)
894                    return Tasks.Max(task => task.Speed);
895            }
896        }
897
898        public override TimeSpan TimeLeft
899        {
900            get
901            {
902                lock (TaskLock)
903                    return Tasks.Max(task => task.TimeLeft);
904            }
905        }
906
907        /// <summary>
908        /// Gets the list of tasks which must complete execution before the task
909        /// is completed.
910        /// </summary>
911        public IList<ProgressManagerBase> Tasks
912        {
913            get;
914            private set;
915        }
916
917        /// <summary>
918        /// The lock object guarding the list of tasks against concurrent read and write.
919        /// </summary>
920        private object TaskLock;
921    }
922
923    /// <summary>
924    /// Provides data for the Eraser.Manager.ProgressChanged event.
925    /// </summary>
926    public class ProgressChangedEventArgs : EventArgs
927    {
928        /// <summary>
929        /// Constructor.
930        /// </summary>
931        /// <param name="progress">The ProgressManagerBase object that stores the progress
932        /// for the given task.</param>
933        /// <param name="userState">A client-specified state object.</param>
934        public ProgressChangedEventArgs(ProgressManagerBase progress, object userState)
935        {
936            Progress = progress;
937            UserState = userState;
938        }
939
940        /// <summary>
941        /// The ProgressManagerBase object that stores the progress for the given
942        /// task.
943        /// </summary>
944        public ProgressManagerBase Progress { get; private set; }
945
946        /// <summary>
947        /// A client-specified state object.
948        /// </summary>
949        public object UserState { get; private set; }
950    }
951
952    /// <summary>
953    /// Represents the method that will handle the ProgressChanged event from
954    /// the <see cref="ProgressManagerBase"/> class.
955    /// </summary>
956    /// <param name="sender">The source of the event.</param>
957    /// <param name="e">A <see cref="ProgressChangedEventArgs"/> event that
958    /// stores the event data.</param>
959    public delegate void ProgressChangedEventHandler(object sender, ProgressChangedEventArgs e);
960}
Note: See TracBrowser for help on using the repository browser.