I have a progress bar that has its value databinded to a property called CurrentTotalVideoSize. I am updating CurrentTotalVideoSize in a loop that is iterating through a directly and combining the size of each file. Every time the iteration runs, the files are larger and thus changes the value of the progress bar. I am not sure that this is the best approach for a progress bar but it is the only one that I have thought of.

The problem I am having is that the file sizes in the directory only periodically update in the explorer window. If I repeatedly press the refresh button in the explorer window. My progress bar acts accordingly; however, without refreshing the folder, the progress bar only updates every once in a while. Is there a way to check the true size of the file without checking what windows explorer says it is at any given time?

Below is the method for iterating through the directory and combining file sizes:

public async void RenderStatus()
{
    string vidPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\FCS\VidBin";
    DirectoryInfo vidDir = new DirectoryInfo(vidPath);
    while (IsBusy)
    {
        long tempCompleteVideoSize = 0;
        foreach(var file in vidDir.GetFiles())
        {
            tempCompleteVideoSize += file.Length;                    
        }
        await Application.Current.Dispatcher.BeginInvoke(new Action(() =>
        {
            CurrentCompleteVideoSize = tempCompleteVideoSize;
            NotifyPropertyChanged(nameof(CurrentCompleteVideoSize));
            ExportPercentProgress = Math.Round((Convert.ToDouble(CurrentCompleteVideoSize) / Convert.ToDouble(TotalCompleteVideoSize) * 100), 2);
            NotifyPropertyChanged(nameof(ExportPercentProgress));
        }), DispatcherPriority.Render);
    }
}

Any help would be very much appreciated.

1

There are 1 best solutions below

1
BionicCode On BEST ANSWER

The point is, that Windows has multiple layers to manage the filesystem (on top of the actual hardware layer). This enables asynchronous file handling, for example. FileInfo accesses the filesystem through a cache layer. That's why you must call FileSystemInfo.Refresh in order to get the info for the latest filesystem version.

foreach(var file in vidDir.GetFiles())
{
  file.Refresh();
  tempCompleteVideoSize += file.Length;                    
}

But I advise you to improve your code and don't continue to use your current implementation.

Your code has serious performance issues. Making the problem worse, all of the following performance issue are executed on the main thread and will therefore lead to a freezing UI.

You are iterating a directory multiple times (two times), this doubling the minimal required iteration time:
DirectoryInfo.GetFiles will enumerate the complete directory to create a FileInfo list - first iteration.
foreach(var file in vidDir.GetFiles()) will iterate the result of Getfiles (the fileInfo list) - second iteration.

Instead process the FileInfo the moment the DirectoryInfo collects it. This results in a single iteration in which DirectoryInfo looks up a FileInfo and immediately returns it to you for your calculations. To achieve this, you must call DirectoryInfo.EnumerateFiles:

// Bad: 
// Two complete iterations of the directory
foreach(var file in vidDir.GetFiles()) 
{}

// Good
// Single iteration of complete directory
foreach(var file in vidDir.EnumerateFiles()) 
{}

Next performance issue is the way you poll the directory's size:

while (IsBusy)
{}

This is a good thing if you want to waste CPU resource as this will keep the main thread busy with doing redundant work.
The preferred way is to use an event based mechanism or other kinds of signaling to notify the progress observer about changes. You must always evaluate such solutions before falling back to resource expensive polling.
For example, you could use the FileSystemWatcher and listen to the FileSystemWatcher.Changed event. This event is also raised when the size of the directory has changed (when configured accordingly).

But because the directory size is expected to change continuously, using the FileSystemWatcher can lead to flooding the main thread in a similar way while (true) {} does. For that reason, I recommend polling the directory's size but move the blocking operation to a background thread and use a timer that allows to control the polling period (for example, poll every 2 seconds) to relieve the stress on the main thread.
Using the System.Threading.Timer will execute the callback on a background thread and allows to configure the interval. This way, you keep the main thread available for more important tasks like rendering the UI or processing input events.

Another performance issue is the use of INotifyPropertyChanged in a control. A type that extends DependencyObject should always prefer to implement properties as dependency properties. This means, all properties that serve either as binding target or binding source (are involved in data binding in general) like the CurrentCompleteVideoSize property for example, must be dependency properties. INotifyPropertyChanged must not be implemented by a control (or DependencyObject in general).
Dependency properties overview (WPF .NET)
How to implement a dependency property (WPF .NET)

The last minor performance issue is the use of string concatenation:

string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\FCS\VidBin";

In general, use string interpolation instead:

string path = $@"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\FCS\VidBin";

Because of the special case of building a filesystem path, you should use the System.Io.Path.Concat helper method, that concatenates path segments safely (e.g. eliminates the worries about path separators):

string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"FCS\VidBin");

The final and improved solution that uses polling with the help of the Timer could look as follows:

MainWindow.xaml.cs

partial class MainWindow : Window
{
  // The System.Threading.Timer implements IDisposable! 
  // Check if it necessary to dispose it explicitly,
  // for example, if the timer is not reused.
  // Let the declaring type implement IDisposable too
  // and dispose the Timer instance from its Dispose method.
  private Timer ProgressPollingTimer { get; set; }

  private bool IsProgressPollingTimerRunning { get; set; }
  private DirectoryInfo TargetDirectory { get; set; }
  private long TotalCompleteVideoSize { get; set; }

  private void StartFileCreation()
  {
    string appDataFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
    string vidPath = Path.Combine(appDataFolderPath, @"FCS\VidBin");
    DirectoryInfo vidDir = new DirectoryInfo(vidPath);
    this.TargetDirectory = vidDir;

    PrepareProgressBar();
    var progressReporter = new Progress<double>(UpdateProgressBar);
    StartProgressPolling(TimeSpan.FromSeconds(2), progressReporter);

    // TODO::Start file creation process

    // TODO::Stop timer when file process has completed
    //StopProgressPolling();
  }

  private void PrepareProgressBar()
  {
    this.ProgressBar.IsIndeterminate = false;

    // Show percentage
    this.ProgressBar.Maximum = 100;
    this.ProgressBar.Value = 0;
  }

  private void StartProgressPolling(TimeSpan interval, IProgress<double> progressReporter)
  {
    if (this.IsProgressPollingTimerRunning)
    {
      return;
    }

    this.ProgressPollingTimer = new Timer(ReportProgress, progressReporter, TimeSpan.Zero, interval);
    this.IsProgressPollingTimerRunning = true;
  }

  private void StopProgressPolling()
  {
    if (!this.IsProgressPollingTimerRunning)
    {
      return;
    }

    bool isTimerIntervalUpdated = this.ProgressPollingTimer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
    this.IsProgressPollingTimerRunning = !isTimerIntervalUpdated;
  }

  private void UpdateProgressBar(double progress)
  {
    // Clip value at 100% 
    // and show an indeterminate progress bar instead.
    // This ensures correct progress feedback 
    // in terms the estimated total file size was too low 
    // (which would lead the percentage to get bigger than 100%).
    this.ProgressBar.Value = Math.Min(progress, this.ProgressBar.Maximum);
    if (progress > this.ProgressBar.Maximum)
    {
      this.ProgressBar.IsIndeterminate = true;
    }
  }

  private long CalculateDirectorySize(DirectoryInfo directoryInfo, string fileSearchPattern = "*.*", bool isIncludeSubfoldersEnabled = false)
  {
    var enumerationOptions = new EnumerationOptions() 
    { 
      IgnoreInaccessible = true, 
      RecurseSubdirectories = isIncludeSubfoldersEnabled
    };

    long directorySizeInBytes = directoryInfo.EnumerateFiles(fileSearchPattern, enumerationOptions)
      .Sum(fileInfo => fileInfo.Length);

    return directorySizeInBytes;
  }

  // Timer callback is executed on a background thread
  public async void ReportProgress(object? timerState)
  {
    // Allow the DirectoryInfo object to update itself 
    // with the latest filesystem version
    this.TargetDirectory.Refresh();

    long currentDirectorySize = CalculateDirectorySize(this.TargetDirectory, "*.mp4", isIncludeSubfoldersEnabled: false);
    double progressPercentage = Math.Round(currentDirectorySize / (double)this.TotalCompleteVideoSize * 100d, 2);

    if (timerState is IProgress<double> progressReporter)
    {
      // Marshal call to the main thread (Progress<T>) 
      progressReporter.Report(progressPercentage);
    }
  }

MainWindow.xaml

<Window>
  <ProgressBar x:Name="ProgressBar" />
</Window>