Обработка фонового рисунка в собственном классе

Ну, у меня есть следующая проблема, и я надеюсь, что вы можете мне помочь:

Я хотел бы создать приложение WPF с фоновым рабочим для обновления richtextboxes и других элементов пользовательского интерфейса. Этот фоновый работник должен обрабатывать некоторые данные, например. обрабатывать содержимое папки, выполнять парсинг и многое другое. Поскольку я хотел бы перемещать как можно больше кода вне основного класса, я создал класс под названием MyProcess.cs, как вы можете видеть ниже (на самом деле этот класс не имеет большого смысла до сих пор, он будет заполнен гораздо более если эта проблема решена). Общая функциональность должна быть:

  • MainWindow: будет создан массив строк (с именем this.folderContent)
  • MainWindow: фоновый работник запускает этот массив в качестве аргумента
  • MainWindow: будет вызван метод DoWork() (я знаю, этот теперь выполняется в новом потоке)
  • MyProcess: создает (пока неформатированный) абзац на основе заданного массива строк
  • MainWindow: если рабочий фон заканчивается, вызывается метод RunWorkerCompleted() (выполняется в потоке пользовательского интерфейса), который должен обновлять WPF RichTextBox с помощью аргумента return метода

Этот последний шаг вызывает исключение 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;
    }
}

Ответ 1

По-видимому, объект 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, но это, безусловно, стоит того. aync-wait 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.

Ответ 2

Вы пытались использовать диспетчера для вызова последнего блока кода?

Пример:

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