C# 在填充数据集时更新WPF进度条,全部使用Rx
我在Rx有点新,所以请原谅我,如果这看起来很愚蠢或明显 我有一个应用程序,它在特定时间扫描选定的文件夹并递归检索所有文件,然后需要将它们存储在数据库中。我希望在这个过程中显示一个进度条,同时保持UI的响应性。一个取消按钮在以后的阶段也很好 我使用Rx实现了这一点,如下所示:C# 在填充数据集时更新WPF进度条,全部使用Rx,c#,wpf,dataset,progress-bar,system.reactive,C#,Wpf,Dataset,Progress Bar,System.reactive,我在Rx有点新,所以请原谅我,如果这看起来很愚蠢或明显 我有一个应用程序,它在特定时间扫描选定的文件夹并递归检索所有文件,然后需要将它们存储在数据库中。我希望在这个过程中显示一个进度条,同时保持UI的响应性。一个取消按钮在以后的阶段也很好 我使用Rx实现了这一点,如下所示: // Get a list of all the files var enumeratedFiles = Directory.EnumerateFiles(targetDirectory, "*.*", SearchOpti
// Get a list of all the files
var enumeratedFiles = Directory.EnumerateFiles(targetDirectory, "*.*", SearchOption.AllDirectories);
// prepare the progress bar
double value = 0;
progBar.Minimum = 0;
progBar.Maximum = enumeratedFiles.Count();
progBar.Value = value;
progBar.Height = 15;
progBar.Width = 100;
statusBar.Items.Add(progBar);
var files = enumeratedFiles.ToObservable()
.SubscribeOn(TaskPoolScheduler.Default)
.ObserveOnDispatcher()
.Subscribe(x =>
{
myDataSet.myTable.AddTableRow(System.IO.Path.GetFileNameWithoutExtension(x));
value++;
},
() =>
{
myDataSetTableAdapter.Update(myDataSet.myTable);
myDataSetTableAdapter.Fill(myDataSet.myTable);
statusBar.Items.Remove(progBar);
});
但是,在上面的示例中,用户界面被锁定,进程栏在此过程中不会更新。我假设这是因为AddTableRow方法阻塞了线程,尽管我认为SubscribeOn(TaskPoolScheduler)应该在新线程上运行任务
我也尝试过几种不同的方法,结果也不尽相同。例如,添加.Do行:
var files = enumeratedFiles.ToObservable()
.Do(x => myDataSet.myTable.AddTableRow(System.IO.Path.GetFileNameWithoutExtension(x)))
.SubscribeOn(TaskPoolScheduler.Default)
.ObserveOnDispatcher()
.Subscribe(x =>
{
value++;
},
() =>
{
myDataSetTableAdapter.Update(myDataSet.myTable);
myDataSetTableAdapter.Fill(myDataSet.myTable);
statusBar.Items.Remove(progBar);
btnCancel.Visibility = Visibility.Collapsed;
});
这实际上显示了进度条的更新,UI并没有完全锁定,但它是不稳定的,性能会下降
我曾尝试使用BackgroundWorker来完成相同的工作,但性能远不如上面的Rx方法(例如,对于21000个文件,Rx方法需要几秒钟,而BackgroundWorker需要几分钟才能完成)
我还看到,进度条的ValueProperty方法的代表也遇到了类似的问题,但如果可能的话,我真的很想使用Rx解决这个问题
我是不是漏掉了什么明显的东西?如果您有任何建议,我们将不胜感激……您所写内容的语义非常奇怪:
这似乎不是您真正想要的…阅读您的代码看起来您在后台做了很多工作,最后更新了UI。对于此类工作,最好将
enumeratedFiles
变量定义为:
var enumeratedFiles =
Observable
.Start(() =>
Directory
.EnumerateFiles(
targetDirectory, "*.*", SearchOption.AllDirectories),
Scheduler.TaskPool)
.ObserveOnDispatcher();
您将得到一个相对快速的后台操作,然后是一个UI更新。这是您当前方法的更好实现
如果您能找到如何更新每个返回文件的UI,请尝试以下可观察的方法:
var enumeratedFiles =
Directory
.EnumerateFiles(targetDirectory, "*.*", SearchOption.AllDirectories)
.ToObservable(Scheduler.TaskPool)
.ObserveOnDispatcher();
使用此选项,您肯定需要了解如何为找到的每个文件更新UI
让我知道其中一个是否对您有效。我找到了解决方案,并提供了更多有关发生情况的详细信息: 在我提到的数据集中插入行时的延迟仅在调试模式下存在,而在不调试的情况下运行时,应用程序不会显示延迟,进度条和项目数的处理速度会快很多倍。我太傻了,没早点测试 在递归扫描文件时,会有一点延迟(21000个文件会延迟几秒钟),但由于这只发生在您第一次这样做时,我在随后的测试中没有注意到这一点,我只关注对我来说似乎很慢的部分:数据集的填充。我猜Directory.EnumerateFiles会缓存内存中的所有内容,因此任何其他读取相同文件的尝试都会立即完成 此外,似乎不需要myDataSetTableAdapter.Fill(myDataSet.myTable)行,因为.Update方法已经将内容保存在数据库本身中 适用于我的最后一段代码如下:
progBar.Height = 15;
progBar.Width = 100;
progBar.IsIndeterminate = true;
statusBar.Items.Add(progBar);
var files = Directory.EnumerateFiles(targetDirectory, "*.*", SearchOption.AllDirectories)
.Where(s => extensions.Contains(System.IO.Path.GetExtension(s))) // "extensions" has been specified further above in the code
.ToObservable(TaskPoolScheduler.Default)
.Do(x => myDataSet.myTable.AddTableRow(System.IO.Path.GetFileNameWithoutExtension(x), x, "default")) // my table has 3 columns
.TakeLast(1)
.Do(_ => myDataSetmyTableTableAdapter.Update(myDataSet.myTable))
.ObserveOnDispatcher()
.Subscribe(xy =>
{
//progBar.Value++; //commented out since I've switched to a marquee progress bar
},
() =>
{
statusBar.Items.Remove(progBar);
btnCancel.Visibility = Visibility.Collapsed;
});
这对我来说似乎很好,谢谢大家的帮助
编辑:我进一步扩展了上面的功能,包括取消按钮功能。如果用户单击“取消”按钮,则进程将立即停止。我尽量保持它的优雅,所以我添加了一个来自Cancel按钮的Click事件的Observable,然后在上面的Observable现有文件中使用.takeTill。代码现在如下所示:
// Show the Cancel button to allow the user to abort the process
btnCancel.Visibility = Visibility.Visible;
// Set the Cancel click event as an observable so we can monitor it
var cancelClicked = Observable.FromEventPattern<EventArgs>(btnCancel, "Click");
// Use Rx to pick the scanned files from the IEnumerable collection, fill them in the DataSet and finally save the DataSet in the DB
var files = Directory.EnumerateFiles(targetDirectory, "*.*", SearchOption.AllDirectories)
.Where(s => extensions.Contains(System.IO.Path.GetExtension(s)))
.ToObservable(TaskPoolScheduler.Default)
.TakeUntil(cancelClicked)
.Do(x => ....
//显示取消按钮以允许用户中止进程
btnCancel.Visibility=可见性.Visibility;
//将取消单击事件设置为可观察事件,以便我们可以监视它
var cancelClicked=可观察的。FromEventPattern(btnCancel,“点击”);
//使用Rx从IEnumerable集合中拾取扫描的文件,将其填充到数据集中,最后将数据集保存到数据库中
var files=Directory.EnumerateFiles(targetDirectory,“*.*”,SearchOption.AllDirectories)
.Where(s=>extensions.Contains(System.IO.Path.GetExtension)))
.ToObservable(TaskPoolScheduler.Default)
.TakeUntil(单击取消)
.Do(x=>。。。。
这不是一种实际任务需要几毫秒的情况,但实现进度条会将其变成一个扩展的噩梦?(我没有答案,只是发出嘶嘶声)不,即使我拿出进度条,完成任务的时间与上面的Rx示例中的时间相同。但在该过程中,UI仍然被锁定…:(感谢您的回复。我理解这可能看起来很奇怪,但我已尝试在单独的线程上完成大部分工作。文件枚举不会占用最多的时间,而是填充数据表(在使用此方法结束之前,我已分别测量了这两个任务)。感谢您的回复。我确实测试了您的建议,但问题仍然存在。在我看来,您似乎认为占用最多资源和时间的进程是Directory.enumerated Files,但实际上不是。锁定UI线程并花费最多时间完成的进程是将每个枚举文件插入到ataSet,使用AddRow方法。我用计时器测量了每个步骤,并隔离了每个任务以得出这个结论…@MiDWaN-那么我的第二个选项对您来说是更好的选择,但是您需要更新每个文件的UI。不要一次将所有行添加到数据集,一次只添加一行。这样行吗?经过更多的测试后,这似乎有效耽搁了多久