1

我对 Rx 有点陌生,所以如果这看起来很愚蠢或明显,请原谅......

我有一个应用程序,它在某个时间扫描选定的文件夹并递归检索所有文件,之后它需要将它们存储在数据库中。我想在该过程中显示一个进度条,同时保持 UI 响应当然。取消按钮在稍后阶段也会很好。

我已经使用 Rx 实现了这一点,如下所示:

// 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);
                    });

但是,对于上面的示例,UI 被锁定,并且在此过程中进度条不会更新。我认为这是因为 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 方法,我也看到了 Delegates 解决了类似的问题,但如果可能的话,我真的很想使用 Rx 来解决这个问题。

我在这里遗漏了一些明显的东西吗?任何建议将不胜感激......

4

3 回答 3

1

你写的东西的语义很奇怪:

  1. 在 UI 线程上做整个文件操作
  2. 拿走这些物品,然后产生一个新线程
  3. 在那个线程上, Dispatcher.BeginInvoke
  4. 在调用中,添加表格行

这似乎不是你真正想要的......

于 2012-11-03T01:44:05.400 回答
1

我找到了解决方案,以及有关正在发生的事情的更多详细信息:

在我提到的 DataSet 中插入行时的延迟仅在 Debug 模式下存在,而在没有 Debug 的情况下运行时,应用程序不会显示延迟,并且进度条和项目数的处理速度要快很多倍。愚蠢的我没有早点测试......

递归扫描文件时会有一点延迟(对于 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;
                });

这对我来说似乎很好,感谢所有帮助人员!

编辑:我进一步扩展了上述内容,包括取消按钮功能。如果用户单击“取消”按钮,则该过程立即停止。我试图让它尽可能优雅,所以我从取消按钮的 Click 事件中添加了一个 Observable,然后在上面的现有文件 Observable 中使用了 .TakeUntil。代码现在如下所示:

// 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 => ....
于 2012-11-04T11:19:12.093 回答
0

阅读您的代码,您似乎在后台做了很多工作,然后最后更新了 UI。对于这种工作,最好这样定义enumeratedFiles变量:

var enumeratedFiles =
    Observable
        .Start(() =>
            Directory
                .EnumerateFiles(
                    targetDirectory, "*.*", SearchOption.AllDirectories),
                        Scheduler.TaskPool)
        .ObserveOnDispatcher();

您将获得一个相对较快的后台操作,然后是一次 UI 更新。这是您当前方法的更好实施。

如果你能弄清楚如何为每个返回的文件更新 UI,那么试试这个 observable:

var enumeratedFiles =
    Directory
        .EnumerateFiles(targetDirectory, "*.*", SearchOption.AllDirectories)
        .ToObservable(Scheduler.TaskPool)
        .ObserveOnDispatcher();

使用此选项,您肯定需要弄清楚如何为找到的每个文件更新 UI。

让我知道是否适合您。

于 2012-11-03T10:01:12.510 回答