我有一个进度条,其值数据绑定到名为 CurrentTotalVideoSize 的属性。我正在循环中更新 CurrentTotalVideoSize,该循环直接迭代并组合每个文件的大小。每次迭代运行时,文件都会变大,从而改变进度条的值。我不确定这是进度条的最佳方法,但这是我想到的唯一方法。
我遇到的问题是目录中的文件大小仅在资源管理器窗口中定期更新。如果我反复按资源管理器窗口中的刷新按钮。我的进度条会相应地起作用;但是,如果不刷新文件夹,进度条只会每隔一段时间更新一次。有没有一种方法可以检查文件的真实大小,而无需检查 Windows 资源管理器在任何给定时间显示的内容?
以下是遍历目录并组合文件大小的方法:
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);
}
}
任何帮助将非常感激。
您的代码存在严重的性能问题。更糟糕的是,以下所有性能问题都在主线程上执行,因此会导致 UI 冻结。
您多次(两次)迭代目录,这使所需的最小迭代时间加倍:
DirectoryInfo.GetFiles
将枚举完整目录以创建 FileInfo
列表 - 第一次迭代。foreach(var file in vidDir.GetFiles())
将迭代 Getfiles(文件信息列表)的结果 - 第二次迭代。
而是在
FileInfo
收集它时处理 DirectoryInfo
。这会导致一次迭代,其中 DirectoryInfo
查找 FileInfo
并立即将其返回给您进行计算。为此,您必须调用 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())
{}
下一个性能问题是轮询目录大小的方式:
while (IsBusy)
{}
如果您想浪费 CPU 资源,这是一件好事,因为这将使主线程忙于执行冗余工作。
首选方法是使用基于事件的机制或其他类型的信令来通知进度观察者有关更改的信息。在回到资源昂贵的轮询之前,您必须始终评估此类解决方案。
例如,您可以使用
FileSystemWatcher
并监听 FileSystemWatcher.Changed
事件。当目录大小发生更改(相应配置时)时也会引发此事件。
但是由于目录大小预计会不断变化,因此使用
FileSystemWatcher
可能会导致主线程以类似于 while (true) {}
的方式淹没。
因此,我建议轮询目录的大小,但将阻塞操作移至后台线程,并使用允许控制轮询周期的计时器(例如,每 2 秒轮询一次),以减轻主线程的压力。System.Threading.Timer
将在后台线程上执行回调并允许配置间隔。这样,您就可以让主线程用于更重要的任务,例如渲染 UI 或处理输入事件。
另一个性能问题是在控件中使用
INotifyPropertyChanged
。扩展 DependencyObject
的类型应该始终倾向于将属性实现为依赖属性。这意味着,所有充当绑定目标或绑定源(通常涉及数据绑定)的属性(例如 CurrentCompleteVideoSize
属性)必须是依赖属性。 INotifyPropertyChanged
不得由控件实现(或一般意义上的 DependencyObject
)。最后一个小性能问题是字符串连接的使用:
string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\FCS\VidBin";
改为使用字符串插值:
string path = $@"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\FCS\VidBin";
由于构建文件系统路径的特殊情况,您应该使用
System.Io.Path.Concat
辅助方法,它可以安全地连接路径段(例如,消除对路径分隔符的担忧):
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"FCS\VidBin");
在
Timer
的帮助下使用轮询的最终改进解决方案可能如下所示:
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;
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)
{
this.ProgressBar.Value = progress;
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)
{
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);
}
}