Фоновая обработка в собственном классе
Ну, у меня есть следующая проблема, и я надеюсь, что вы можете мне помочь:
Я хотел бы создать приложение WPF с фоновым рабочим для обновления richtextboxes и других элементов пользовательского интерфейса. Этот фоновый работник должен обработать некоторые данные, например, обработать содержимое папки, выполнить анализ и многое другое. Поскольку я хотел бы вынести как можно больше кода за пределы класса Main, я создал класс под названием MyProcess.cs
как вы можете видеть ниже (на самом деле, этот класс пока не имеет особого смысла, он будет заполнен гораздо большим количеством элементов обработки, если эта проблема будет решена). Общая функциональность должна быть:
- MainWindow: будет создан массив строк
this.folderContent
) - MainWindow: фоновый работник начинает принимать этот массив в качестве аргумента
- Главное окно:
DoWork()
будет вызван метод (я знаю, этот теперь работает в новом потоке) - MyProcess: генерирует (пока не отформатированный) абзац на основе заданного массива строк
- MainWindow: если фоновый рабочий закончен,
RunWorkerCompleted()
вызывается метод (запущенный в потоке пользовательского интерфейса), который должен обновить WPF RichTextBox через аргумент возврата метода
Этот последний шаг вызывает InvalidOperationsException с примечанием, что "Вызывающий поток не может получить доступ к этому объекту, потому что другой поток владеет им". Я прочитал немного о фоновом рабочем классе и его функциональности. Так что я думаю, что это как-то связано с this.formatedFilenames.Inlines.Add(new Run(...))
позвонить в Execute()
метод MyProcess
, Если я заменю атрибут Paragraph списком строк или чем-то подобным (без дополнительных new()
звонки) Я могу получить доступ к этому члену без проблем с помощью метода get. Все примеры, относящиеся к фоновому работнику, которые я обнаружил, возвращают только базовые типы или простые классы.
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 вызывается, например, при событии нажатия кнопки или после выбора папки с помощью диалога, то есть в потоке пользовательского интерфейса.
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;
}
}
2 ответа
По-видимому, Paragraph
объект (и его подобъекты) требует соответствия потоков. То есть он не является поточно-ориентированным и предназначен для использования только в том же потоке, в котором был создан.
Предположительно, вы звоните RunWorkerAsync
из основного потока пользовательского интерфейса, и вот где worker_RunWorkerCompleted
в конце концов называется. Таким образом, вы получаете доступ к экземпляру Paragraph
на основной ветке по окончании работ. Тем не менее, он был создан в фоновом рабочем потоке, внутри process.Execute
, Вот почему вы получаете InvalidOperationsException
исключение при касании его из основного потока.
Если приведенное выше понимание проблемы является правильным, вы, вероятно, должны отказаться от BackgroundWorker
, Нет смысла использовать фоновый поток для запуска for
цикл, единственной целью которого будет маршалинг обратных вызовов в поток пользовательского интерфейса через Dispatcher.Invoke
, Это только добавило бы дополнительные накладные расходы.
Вместо этого вы должны запускать фоновую операцию в потоке пользовательского интерфейса, шаг за шагом. Вы могли бы использовать DispatcherTimer
для этого, или вы могли бы удобно запустить его с помощью async / await (для.NET 4.5 или.NET 4.0 с Microsoft.Bcl.Async
и VS2012+):
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 перечисляет несколько замечательных ресурсов для начала.
Чтобы позвонить 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
обработчик события.
Вы пытались использовать диспетчер для вызова последнего блока кода?
Пример:
private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Action action = () =>
{
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);
};
Dispatcher.Invoke(DispatcherPriority.Normal, action);
}
Более подробная информация о диспетчере здесь: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher(v=vs.110).aspx