那么,我有以下问题,希望你能帮助我:
我想创建一个 WPF 应用程序,其中包含一个用于更新富文本框和其他 UI 元素的后台工作程序。这个后台 worker 应该处理一些数据,例如处理文件夹的内容,进行一些解析等等。由于我想将尽可能多的代码移到 Main 类之外,因此我创建了一个名为 MyProcess.cs
的类正如你在下面看到的(事实上,这个类到目前为止没有多大意义,如果这个问题已经解决,它将填充更多的处理元素)。一般功能应该是:
- MainWindow:将创建一个字符串数组(名为
this.folderContent
) - MainWindow:后台工作人员开始将此数组作为参数
- 主窗口:
DoWork()
方法将被调用(我知道,这个现在在一个新线程中运行) - MyProcess:根据给定的字符串数组生成一个(到目前为止未格式化的)段落
- MainWindow:如果后台 worker 完成,
RunWorkerCompleted()
方法被调用(在 UI 线程中运行),它应该通过方法的返回参数更新 WPF RichTextBox
这最后一步导致 InvalidOperationsException 并附注“调用线程无法访问此对象,因为另一个线程拥有它。”我阅读了一些关于后台 worker 类及其功能的内容。所以我认为它与 this.formatedFilenames.Inlines.Add(new Run(...))
有关调用Execute()
MyProcess
的方法| .如果我用字符串列表或类似的东西替换 Paragraph 属性(没有额外的 new()
调用),我可以通过 get 方法毫无问题地访问这个成员。我发现的所有与后台 worker 相关的示例都只返回基本类型或简单类。
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
this.process = new MyProcess();
this.worker = new BackgroundWorker();
this.worker.DoWork += worker_DoWork;
this.worker.RunWorkerCompleted += worker_RunWorkerCompleted;
}
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
this.process.Execute((string[])e.Argument);
e.Result = this.process.Paragraph();
}
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.rtbFolderContent.Document.Blocks.Clear();
// the next line causes InvalidOperationsException:
// The calling thread cannot access this object because a different thread owns it.
this.rtbFolderContent.Document.Blocks.Add((Paragraph)e.Result);
}
...
// folderContent of type string[]
this.worker.RunWorkerAsync(this.folderContent);
...
编辑:由于有人提出这个问题:RunWorkerAsync 被称为例如在按钮单击事件上或通过对话框选择文件夹之后,因此在 UI 线程中。
MyProcess.cs
class MyProcess
{
Paragraph formatedFilenames;
public MyProcess ()
{
this.formatedFilenames = new Paragraph();
}
public void Execute(string[] folderContent)
{
this.formatedFilenames = new Paragraph();
if (folderContent.Length > 0)
{
for (int f = 0; f < folderContent.Length; ++f)
{
this.formatedFilenames.Inlines.Add(new Run(folderContent[f] + Environment.NewLine));
// some dummy waiting time
Thread.Sleep(500);
}
}
}
public Paragraph Paragraph()
{
return this.formatedFilenames;
}
}
最佳答案
显然,Paragraph
对象(及其子对象)需要线程关联。也就是说,它不是线程安全的,只能在创建它的同一线程上使用。
据推测,您正在调用 RunWorkerAsync
来自主 UI 线程,这就是 worker_RunWorkerCompleted
的地方最终被调用。因此,您访问了 Paragraph
的实例工作完成后在主线程上。但是,它是在 process.Execute
内的后台工作线程上创建的.这就是为什么你得到 InvalidOperationsException
当您从主线程触摸它时出现异常。
如果以上对问题的理解是正确的,你大概应该放弃BackgroundWorker
了。 .使用后台线程运行 for
没有多大意义循环,其唯一目的是通过 Dispatcher.Invoke
将回调编码到 UI 线程.那只会增加额外的开销。
相反,您应该在 UI 线程上逐个运行后台操作。你可以使用 DispatcherTimer
为此,或者您可以使用 async/await 方便地运行它(使用 Microsoft.Bcl.Async
和 VS2012+ 定位 .NET 4.5 或 .NET 4.0):
public async Task Execute(string[] folderContent, CancellationToken token)
{
this.formatedFilenames = new Paragraph();
if (folderContent.Length > 0)
{
for (int f = 0; f < folderContent.Length; ++f)
{
token.ThrowIfCancellationRequested();
// yield to the Dispatcher message loop
// to keep the UI responsive
await Dispatcher.Yield(DispatcherPriority.Background);
this.formatedFilenames.Inlines.Add(
new Run(folderContent[f] + Environment.NewLine));
// don't do this: Thread.Sleep(500);
// optionally, throttle it;
// this step may not be necessary as we use Dispatcher.Yield
await Task.Delay(500, token);
}
}
}
关于 async/await
有一些学习曲线,但这当然值得一试。 async-await tag wiki列出一些很棒的资源,作为开始。
调用async
实现Execute
像上面一样,你需要接受 "Async all the way"规则。通常,这意味着您会调用 Execute
来自顶级事件或命令处理程序,它也是 async
, 和 await
它的结果,例如:
CancellationTokenSource _cts = null;
async void SomeCommand_Executed(object sender, RoutedEventArgs e)
{
if (_cts != null)
{
// request cancellation if already running
_cts.Cancel();
_cts = null;
}
else
{
// start a new operation and await its result
try
{
_cts = new CancellationTokenSource();
await Execute(this.folderContent, _cts.Token);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
也可以使用事件模式,使代码流更类似于您处理 RunWorkerCompleted
的原始场景。 :
// fire ExecuteCompleted and pass TaskCompletedEventArgs
class TaskCompletedEventArgs : EventArgs
{
public TaskCompletedEventArgs(Task task)
{
this.Task = task;
}
public Task Task { get; private set; }
}
EventHandler<TaskCompletedEventArgs> ExecuteCompleted = (s, e) => { };
CancellationTokenSource _cts = null;
Task _executeTask = null;
// ...
_cts = new CancellationTokenSource();
_executeTask = DoUIThreadWorkLegacyAsync(_cts.Token);
// don't await here
var continutation = _executeTask.ContinueWith(
task => this.ExecuteCompleted(this, new TaskCompletedEventArgs(task)),
_cts.Token,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.FromCurrentSynchronizationContext());
在这种情况下,您应该明确检查 Task
对象属性,如 Task.IsCancelled
, Task.IsFaulted
, Task.Exception
, Task.Result
在你的里面ExecuteCompleted
事件处理程序。
关于c# - Backgroundworker 在自己的类中处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21763639/