0

i'm currently facing an issue in C# WPF. I wrote an application, that generates long running reports in a background task. I am using prism with MVVM and trying to run the expensive background task with a Async ICommand implementation and a BackgroundWorker. But when i try to retrieve the resulting report

Report = asyncTask.Result;

i get an InvalidOperationException stating "The calling thread cannot access this object because a different thread owns it.".

Yes, i have already tried to invoke a dispatcher (its the first thing you'll find on google, stackoverflow etc when you search for the exception message). I have tried several variants like for instance:

Dispatcher.CurrentDispatcher.Invoke(() => Report = asyncTaks.Result);

or

Report.Dispatcher.Invoke(() => Report = asyncTask.Result);

but each time i get this exception.

I am suspecting that the way i am calling the report UI is not adequate.

The structure looks in brief as follows:

MainWindowViewModel
  -> SubWindowCommand
      SubWindowViewModel
        -> GenerateReportCommand
            ReportViewModel
              -> GenerateReportAsyncCommand
              <- Exception on callback

I am out of ideas, does anybody have a clue what i might be doing wrong?

Below are a few code fragments


Report Generator View Model:

public class ReportFlowDocumentViewModel : BindableBase
{
    private IUnityContainer _container;
    private bool _isReportGenerationInProgress;
    private FlowDocument _report;

    public FlowDocument Report
    {
        get { return _report; }
        set
        {
            if (object.Equals(_report, value) == false)
            {
                SetProperty(ref _report, value);
            }
        }
    }

    public bool IsReportGenerationInProgress
    {
        get { return _isReportGenerationInProgress; }
        set
        {
            if (_isReportGenerationInProgress != value)
            {
                SetProperty(ref _isReportGenerationInProgress, value);
            }
        }
    }


    public ReportFlowDocumentView View { get; set; }

    public DelegateCommand PrintCommand { get; set; }

    public AsyncCommand GenerateReportCommand { get; set; }

    public ReportFlowDocumentViewModel(ReportFlowDocumentView view, IUnityContainer c)
    {
        _container = c;

        view.DataContext = this;
        View = view;
        view.ViewModel = this;

        InitializeGenerateReportAsyncCommand();

        IsReportGenerationInProgress = false;
    }

    private void InitializeGenerateReportAsyncCommand()
    {
        GenerateReportCommand = new CreateReportAsyncCommand(_container);
        GenerateReportCommand.RunWorkerStarting += (sender, args) =>
        {
            IsReportGenerationInProgress = true;

            var reportGeneratorService = new ReportGeneratorService();
            _container.RegisterInstance<ReportGeneratorService>(reportGeneratorService);
        };

        GenerateReportCommand.RunWorkerCompleted += (sender, args) =>
        {
            IsReportGenerationInProgress = false;
            var report = GenerateReportCommand.Result as FlowDocument;
            var dispatcher = Application.Current.MainWindow.Dispatcher;

            try
            {
                dispatcher.VerifyAccess();

                if (Report == null)
                {
                    Report = new FlowDocument();
                }

                Dispatcher.CurrentDispatcher.Invoke(() =>
                {
                    Report = report;
                });

            }
            catch (InvalidOperationException inex)
            {
                // here goes my exception
            }
        };
    }

    public void TriggerReportGeneration()
    {
        GenerateReportCommand.Execute(null);
    }

}

This is how i start the ReportView Window

var reportViewModel = _container.Resolve<ReportFlowDocumentViewModel>();

View.ReportViewerWindowAction.WindowContent = reportViewModel.View;

reportViewModel.TriggerReportGeneration();

var popupNotification = new Notification()
{
    Title = "Report Viewer",
};
ShowReportViewerRequest.Raise(popupNotification);

with

ShowReportViewerRequest = new InteractionRequest<INotification>();

AsyncCommand definition

public abstract class AsyncCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public event EventHandler RunWorkerStarting;
    public event RunWorkerCompletedEventHandler RunWorkerCompleted;

    public abstract object Result { get; protected set; }
    private bool _isExecuting;
    public bool IsExecuting
    {
        get { return _isExecuting; }
        private set
        {
            _isExecuting = value;
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }
    }

    protected abstract void OnExecute(object parameter);

    public void Execute(object parameter)
    {
        try
        {
            onRunWorkerStarting();

            var worker = new BackgroundWorker();
            worker.DoWork += ((sender, e) => OnExecute(e.Argument));
            worker.RunWorkerCompleted += ((sender, e) => onRunWorkerCompleted(e));
            worker.RunWorkerAsync(parameter);
        }
        catch (Exception ex)
        {
            onRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
        }
    }

    private void onRunWorkerStarting()
    {
        IsExecuting = true;
        if (RunWorkerStarting != null)
            RunWorkerStarting(this, EventArgs.Empty);
    }

    private void onRunWorkerCompleted(RunWorkerCompletedEventArgs e)
    {
        IsExecuting = false;
        if (RunWorkerCompleted != null)
            RunWorkerCompleted(this, e);
    }

    public virtual bool CanExecute(object parameter)
    {
        return !IsExecuting;
    }
}

CreateReportAsyncCommand:

public class CreateReportAsyncCommand : AsyncCommand
{
    private IUnityContainer _container;

    public CreateReportAsyncCommand(IUnityContainer container)
    {
        _container = container;
    }

    public override object Result { get; protected set; }
    protected override void OnExecute(object parameter)
    {
        var reportGeneratorService = _container.Resolve<ReportGeneratorService>();
        Result = reportGeneratorService?.GenerateReport();
    }

}
Michael
  • 153
  • 1
  • 14
  • You're generating the report on a background thread using the `BackgroundWorker.DoWork` event handler but are trying to access it on the main thread later. Hence the exception. – dymanoid Nov 24 '17 at 09:26
  • `Dispatcher.CurrentDispatcher.Invoke` uses the UI thread. It should only be used if you are updating UI elements but AFAIK it's not helpful in MVVM since you have `INotifyPropertyChanged` in data binding. – jegtugado Nov 24 '17 at 09:27
  • Report is bound to a FlowDocument UI element, therefore i thought i can use it. If i cant use Dispatcher.CurrentDispatcher.Invoke, what else? – Michael Nov 24 '17 at 09:31
  • There are classes, whose objects does not designed to be shared in several threads, GUI classes for instance. So if you create such object in one thread and try to use it in another, then you have the issue. What you can do? Calculate all things in bg thread and put them to the non GUI containers, then in dispatcher for gui create GUI(ore another "single thread" objects) and just take your data over. – Rekshino Nov 24 '17 at 09:49
  • I understand what you mean, but i don't know how to implement that. Could you provide an example? – Michael Nov 24 '17 at 09:57
  • 2
    This is no MvvM! `FlowDocument` is a part of UI! Sacrilege. – XAMlMAX Nov 24 '17 at 10:01

2 Answers2

1

I think i understand my problem now. I cannot use FlowDocument in a BackgroundThread and update it afterwards, right?

So how can i create a FlowDocument within a background thread, or at least generate the document asynchronously?

The FlowDocument i am creating contains a lot of tables and when i run the report generation synchronously, the UI freezes for about 30seconds, which is unacceptable for regular use.

EDIT: Found the Solution here: Creating FlowDocument on BackgroundWorker thread

In brief: I create a flow document within my ReportGeneratorService and then i serialize the FlowDocument to string. In my background worker callback i receive the serialized string and deserialize it - both with XamlWriter and XmlReader as shown here

Michael
  • 153
  • 1
  • 14
0

Your Problem is that you create FlowDocument in another thread. Put your data to the non GUI container and use them after bg comes back in UI thread.

Rekshino
  • 6,954
  • 2
  • 19
  • 44
  • I have now modified my ReportGeneratorService to take a FlowDocument as a constructor parameter. I create a new instance of the Report within my ViewModel and pass it to the ReportGeneratorService. Afterwards i pass the ReportGeneratorService instance to the GenerateReportCommand and via GenerateReportCommandAsync.Execute(..). But still i get the exception when the GenerateReportCommandAsync returns by the callback – Michael Nov 24 '17 at 10:11
  • To make it more clear to you - remove all references to GUI(presentationframework, System.Windows, etc.) dll's from your VM project, then try to compile, you will see the errors, correct them. – Rekshino Nov 24 '17 at 10:25