wpfmultithreadingui-thread

Is it possible to initialize WPF UserControls in different threads?


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!


Solution

  • 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: