How to access separate thread generated WPF UI ele

2019-02-10 12:43发布

问题:

I need to generate a print preview (a long one) using wpf UI elements like FixedDocument, FlowDocument, PageContent, BlockUIContainer and all those. To keep my UI responsive i'm doing this part on a separate Thread class thread (BackgroundWorker won't work since i need an STA thread). Everything is OK upto this point.
But after displaying the print preview now i need to print, and clicking Print icon on the generated preview throws the infamous "The calling thread cannot access this object because a different thread owns it." exception. So, is there any way around?

EDIT (CODE):

Dispatcher.CurrentDispatcher.Invoke(new Action(() =>  
    {  
        Thread thread = new Thread(() =>  
            {  
                FixedDocument document = renderFlowDocumentTemplate(report);  
                PrintPreview preview = new PrintPreview();  
                preview.WindowState = WindowState.Normal;  
                preview.documentViewer.Document = document;  
                preview.ShowDialog();  
            });  
        thread.SetApartmentState(ApartmentState.STA);  
        thread.Start();  
    }));`

Ok here, the RenderFlowDocumentTemplate() generates the print preview (which contains the UI elements) and fills the them with Report data. PrintPreview is a custom window that contains a DocumentViewer element that actually holds and displays the preview, and contains the Print icon, upon clicking which i'm supposd to get the PrintDialog window.

EDIT (XAML):

<cw:CustomWindow x:Class="MyApp.Reports.PrintPreview"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cw="clr-namespace:MyApp.UI.CustomWindows;assembly=MyApp.UI.CustomWindows">    
    <DocumentViewer Margin="0,30,0,0" Name="documentViewer"></DocumentViewer>
</cw:CustomWindow>`

回答1:

The easiest way would be.

Action a = () =>
{
    //Code from another thread.
};
Dispatcher.BeginInvoke(a);


回答2:

I tied this some time ago - and I think found the problem to be that the printpreview dialog needs to be on the mainthread.



回答3:

Found another guy with exactly the same problem - Printing the content of a DocumentViewer in a different UI thread. Just followed the same path. The code here was a real savior.
Now I'm NOT trying to access the secondary thread generated UI element from the Dispatcher thread, instead now the rest of the printing procedure is executed on the secondary thread. No cross-thread "VerifyAccess" of UI elements, and It's working smoothly. :)



回答4:

There is also another "trick"...

Often I run into the same problems. For example I try bind a FrameworkElement to an ContentPresenter. My solution for this, I use a ItemsControl in place of the ContentPresenter and bind my single FrameworkElement over an ObservableCollection<FrameworkElement> with only the one item. After this, no problems



回答5:

I've wrote this simple snippet, I do not have any experiences with it, but I tested it for few things, and it seems to be working fine.

/// <summary>
/// Creates UI element on a seperate thread and transfers it to
/// main UI thread. 
/// 
/// Usage; if you have complex UI operation that takes a lot of time, such as XPS object creation.
/// </summary>
/// <param name="constructObject">Function that creates the necessary UIElement - will be executed on new thread</param>
/// <param name="constructionCompleted">Callback to the function that receives the constructed object.</param>
public void CreateElementOnSeperateThread(Func<UIElement> constructObject, Action<UIElement> constructionCompleted)
{
    VerifyAccess();

    // save dispatchers for future usage.
    // we create new element on a seperate STA thread
    // and then basically swap UIELEMENT's Dispatcher.
    Dispatcher threadDispatcher = null;
    var currentDispatcher = Dispatcher.CurrentDispatcher;

    var ev = new AutoResetEvent(false);
    var thread = new Thread(() =>
        {
            threadDispatcher = Dispatcher.CurrentDispatcher;
            ev.Set();

            Dispatcher.Run();
        });

    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Start();

    ev.WaitOne();

    threadDispatcher.BeginInvoke(new Action(() =>
        {
            var constructedObject = constructObject();
            currentDispatcher.BeginInvoke(new Action(() =>
                {
                    var fieldinfo = typeof (DispatcherObject).GetField("_dispatcher",
                                                                       BindingFlags.NonPublic |
                                                                       BindingFlags.Instance);
                    if (fieldinfo != null)
                        fieldinfo.SetValue(constructedObject, currentDispatcher);

                    constructionCompleted(constructedObject);
                    threadDispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
                }), DispatcherPriority.Normal);
        }), DispatcherPriority.Normal);
}

And here is the usage:

 CreateElementOnSeperateThread(() =>
        {
            // running on new temp dispatcher.
            var loadsOfItems = new List<int>();
            for(var i = 0; i < 100000; i++)
                loadsOfItems.Add(i+12);


            var dataGrid = new DataGrid {ItemsSource = loadsOfItems, Width = 500, Height = 500};

            dataGrid.Measure(new Size(500, 500));
            dataGrid.Arrange(new Rect(0, 0, 500, 500));

            return dataGrid;
        }, result => SampleGrid.Children.Add(result));


回答6:

Use dispatcher class in this case. the Dispatcher class has invoke and beginInvoke methods. which allow to send the request to the current thread using dispatchment process. what you need to do is create a call like below using delegate

  App.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
//you code goes here.
 }));

the begin invoke call process your call asynchronously.