Is it possible to initialize WPF UserControls in different threads?
Asked Answered
M

3

18

We are developing a WPF application which will open a number of reports at the same time (just like a typical MDI application such as Excel or Visual Studio). Although it is possible to have the data context for those reports run in multiple worker threads, we still find that if the number of opened reports is really big, even the rendering of those reports (basically UserControl hosted either in a MDI environment or just in a grid area in the main view) will still make the application less responsive.

So, my idea is to at least have several areas in the main UI, each of whom will have its user control running in different UI threads. Again, imagine a typical view in visual studio, except for the menus, it has the main area of text editor, a side area which hosts for example solution explorer, and a bottom area which hosts for example error list and output. So I want these three areas to be running in three UI threads (but naturally they are hosted in one MainView, that's the part I am not sure about).

I am asking because I know it is possible to have several (top-level) windows running in different UI threads. But somebody said it doesn't apply to the user controls. Is it true? If so, what is the typical solution to my scenario, i.e., the number of opened UserControl is really big, and many of these UserControl are real-time so rendering of them takes huge amount of resources? Thanks!

Multistage answered 12/9, 2012 at 11:58 Comment(5)
I don't think that you need to create so many reports, in one batch, that you can get wpf to slow down. You can always delay user controls loading, use virtualization to minimize the loading time of each individual usercontrol or use delayed creation of each one. Alot of options before considering a threading approach. Which in fact, even if it would be possible, would not be such a big improvement imho.Mulderig
@Mulderig , thanks for the comment. I have seen this less responsive problem in some extreme condition, more like in a stress testing scenario. Like I mentioned, we have many real-time monitors and I admit it is not a every day scenario to have all of them refreshing. I guess I just have to live with single UI thread and optimize it.Multistage
Please post code for how you have several (top-level) windows running in different UI threads. Please define real-time UserControl and why rendering takes a huge amount of resources.Outspeak
@Blam please see the second and third links colinsmith just posted, that's how to have multiple UI threads. And regarding to the real-time UserControl, I mean for example the figures in one report might get changed from time to time and it is making some visual notification to the user. I am not sure how exactly they take huge amount of resources but I just got reported from a colleagueMultistage
Yes. See my answer here: Running a WPF control in another threadExtradition
B
27

Background Information on UI Threading Models

Normally an application has one "main" UI thread...and it may have 0 or more background/worker/non-UI threads where you (or the .NET runtime/framework) does background work.

(...there's a another special thread in WPF called the rendering thread but I will skip that for now...)

For example, a simple WPF Application might have this list of threads:

enter image description here

And a simple WinForms Application might have this list of threads:

enter image description here

When you create an element it is tied (has affinity) to a particular Dispatcher & thread and can only be accessed safely from the thread associated with the Dispatcher.

If you try and access properties or methods of an object from a different thread, you will usually get an exception e.g. in WPF:

enter image description here

In WindowsForms:

enter image description here

Any modifications to the UI need to be performed on the same thread on which a UI element was created...so background threads use Invoke/BeginInvoke to get that work run on the UI thread.

Demo to Demonstrate Issues with Element Creation on non-UI Thread

<Window x:Class="WpfApplication9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <StackPanel x:Name="mystackpanel">

    </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;

namespace WpfApplication9
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Thread m_thread1;
        Thread m_thread2;
        Thread m_thread3;
        Thread m_thread4;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            CreateAndAddElementInDifferentWays();
        }

        void CreateAndAddElementInDifferentWays()
        {
            string text = "created in ui thread, added in ui thread [Main STA]";
            System.Diagnostics.Debug.WriteLine(text);

            CreateAndAddTextChild(text);

            // Do NOT use any Joins with any of these threads, otherwise you will get a
            // deadlock on any "Invoke" call you do.

            // To better observe and focus on the behaviour when creating and
            // adding an element from differently configured threads, I suggest
            // you pick "one" of these and do a recompile/run.

            ParameterizedThreadStart paramthreadstart1 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnThread);
            m_thread1 = new Thread(paramthreadstart1);
            m_thread1.SetApartmentState(ApartmentState.STA);
            m_thread1.Start("[STA]");

            //ParameterizedThreadStart paramthreadstart2 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnUIThread);
            //m_thread2 = new Thread(paramthreadstart2);
            //m_thread2.SetApartmentState(ApartmentState.STA);
            //m_thread2.Start("[STA]");

            //ParameterizedThreadStart paramthreadstart3 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnThread);
            //m_thread3 = new Thread(paramthreadstart3);
            //m_thread3.SetApartmentState(ApartmentState.MTA);
            //m_thread3.Start("[MTA]");

            //ParameterizedThreadStart paramthreadstart4 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnUIThread);
            //m_thread4 = new Thread(paramthreadstart4);
            //m_thread4.SetApartmentState(ApartmentState.MTA);
            //m_thread4.Start("[MTA]");
        }

        //----------------------------------------------------------------------

        void WorkCreatedOnThreadAddedOnThread(object parameter)
        {
            string threadingmodel = parameter as string;

            string text = "created in worker thread, added in background thread, " + threadingmodel;
            System.Diagnostics.Debug.WriteLine(text);

            CreateAndAddTextChild(text);
        }

        void WorkCreatedOnThreadAddedOnUIThread(object parameter)
        {
            string threadingmodel = parameter as string;

            string text = "created in worker thread, added in ui thread via invoke" + threadingmodel;
            System.Diagnostics.Debug.WriteLine(text);

            TextBlock tb = CreateTextBlock(text);
            if (tb != null)
            {
                // You can alternatively use .Invoke if you like!

                DispatcherOperation dispop = Dispatcher.BeginInvoke(new Action(() =>
                {
                    // Get this work done on the main UI thread.

                    AddTextBlock(tb);
                }));

                if (dispop.Status != DispatcherOperationStatus.Completed)
                {
                    dispop.Wait();
                }
            }
        }

        //----------------------------------------------------------------------

        public TextBlock CreateTextBlock(string text)
        {
            System.Diagnostics.Debug.WriteLine("[CreateTextBlock]");

            try
            {
                TextBlock tb = new TextBlock();
                tb.Text = text;
                return tb;
            }
            catch (InvalidOperationException ex)
            {
                // will always exception, using this to highlight issue.
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }

            return null;
        }

        public void AddTextBlock(TextBlock tb)
        {
            System.Diagnostics.Debug.WriteLine("[AddTextBlock]");

            try
            {
                mystackpanel.Children.Add(tb);
            }
            catch (InvalidOperationException ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }

        public void CreateAndAddTextChild(string text)
        {
            TextBlock tb = CreateTextBlock(text);
            if (tb != null)
                AddTextBlock(tb);
        }
    }
}

Secondary UI thread aka "Creating a top-level Window on another thread"

It's possible to create secondary UI-threads, so long as you mark the thread as using the STA apartment model, and create a Dispatcher (e.g. use Dispatcher.Current) and start a "run" loop (Dispatcher.Run()) so the Dispatcher can service messages for the UI elements created on that thread.

BUT an element created in one UI thread can't be put into the logical/visual tree of another element which is created on a different UI thread.

Workaround Technique for mixing elements created on different UI threads

There is a limited workaround technique, which may provide you with some ability to compose the rendering of an element created in one UI thread with the visual tree created in a different thread...by using HostVisual. See this example:

Bloomfield answered 12/9, 2012 at 13:14 Comment(1)
Thanks man. They are exactly the resources I found and indeed they are limited. And thanks for clarifying the logical/visual tree relations.Multistage
S
2

No, UserControls are tied to the UI thread. Even if you were able to initialize them elsewhere, you'd have issues adding them to the main UI as they'd belong to a different thread.

Secondclass answered 12/9, 2012 at 12:21 Comment(4)
Thanks for your reply. Just to be clear it even applies to the situation that this other thread is also UI thread (not a worker thread)? Like I mentioned, I am able to launch different windows in different UI thread, and I'd like to do the same thing for UCs.Multistage
Basically, you can't mix and match UI controls with threads. All UI elements for a top-level window and its children MUST exist in a single thread. Different top-level windows can have different threads, but anything it contains must always use the same thread. It's a limitation of the basic windowing system.Secondclass
Thanks again. I guess it sounds reasonable. But do you care to comment on my second half of the post? What would be a proper solution for our scenario, where many UCs might co-exist, and many of them are real-time, refreshing all the time? Although it is not likely that our customer will open hundreds reports at the same time, but we still need to prepare for the worst.Multistage
You can use BackgroundWorker threads to process the user requests. This will keep the UI responsive.Freeliving
M
0

You CAN split rendering of the visual tree across different threads.

See this article for a good explanation and an example that renders video output. http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx

But doing this is only really justifiable when the actual rendering of the Visual is implemented elsewhere or it's technologically very odd in a WPF application such as rendering a Direct3D scene as a Visual.

The important note here, as is mentioned in the article, is that if the secondary threads render WPF XAML then you loose input events because routed events can't cross the thread boundary.

Matthieu answered 10/10, 2014 at 13:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.