使用Rx填充DataSet时更新WPF进度条

时间:2012-11-02 19:05:15

标签: c# wpf dataset progress-bar system.reactive

我在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方法解决了类似的问题,但如果可能的话,我真的想用Rx解决这个问题。

我错过了一些明显的东西吗?任何建议都将受到高度赞赏......

3 个答案:

答案 0 :(得分:1)

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

  1. 在UI线程上执行整个文件操作
  2. 获取这些项目,然后生成一个新线程
  3. 在该线程上,Dispatcher.BeginInvoke
  4. 在调用中,添加表格行
  5. 这看起来不像你真正想要的......

答案 1 :(得分:1)

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

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

在递归扫描文件时会有一点延迟(对于21000个文件只有几秒钟),但由于它只是在你第一次这样做时才会发生,我在后来的测试中没有注意到它,而我只是专注于对我来说似乎很慢的部分:填充DataSet。我猜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现有文件中使用.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 => ....

答案 2 :(得分: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。

让我知道是否适合你。