Можно ли инициализировать WPF UserControls в разных потоках?

мы разрабатываем приложение WPF, которое одновременно откроет несколько отчетов (как и типичное приложение MDI, такое как Excel или Visual Studio). Хотя контекст данных для этих отчетов может выполняться в нескольких рабочих потоках, мы по-прежнему обнаруживаем, что если количество открытых отчетов действительно велико, даже рендеринг этих отчетов (в основном UserControl, размещенный либо в среде MDI, либо только в области сетки в главном представлении) все равно сделает приложение менее отзывчивое.

Итак, моя идея состоит в том, чтобы иметь, по крайней мере, несколько областей в основном пользовательском интерфейсе, каждый из которых будет иметь свой пользовательский контроль в разных потоках пользовательского интерфейса. Опять же, представьте себе типичное представление в visual studio, за исключением меню, оно имеет основную область текстового редактора, боковую область, в которой размещен, например, обозреватель решений, и нижнюю область, в которой размещен, например, список ошибок и вывод. Поэтому я хочу, чтобы эти три области работали в трех потоках пользовательского интерфейса (но, естественно, они размещенный в одном MainView, это часть, в которой я не уверен).

Я спрашиваю, потому что я знаю, что можно иметь несколько (верхнего уровня) окон, работающих в разных потоках пользовательского интерфейса. Но кто-то сказал, что это не относится к пользовательским элементам управления. Это правда? Если да, то какое типичное решение для моего сценария, т. е. количество открытых UserControl действительно велико, и многие из этих UserControl находятся в режиме реального времени, поэтому их рендеринг занимает огромное количество ресурсов? Спасибо!

3 ответов


Справочная информация по моделям UI Threading

обычно приложение имеет один основной поток пользовательского интерфейса...и он может иметь 0 или более фоновых/рабочих / не-UI потоков, где вы (или среда выполнения .NET/framework) выполняете фоновую работу.

(...в WPF есть еще один специальный поток, называемый потоком рендеринга, но я пока пропущу его...)

например, простое приложение WPF может иметь этот список темы:

enter image description here

и простое приложение WinForms может иметь этот список потоков:

enter image description here

при создании элемента он привязан (имеет сродство) к определенному Dispatcher & thread и может быть безопасно доступен только из потока, связанного с Dispatcher.

если вы попытаетесь получить доступ к свойствам или методам объекта из другого потока, вы обычно получаете исключение, например, в В WPF:

enter image description here

В WindowsForms:

enter image description here

любые изменения пользовательского интерфейса должны выполняться в том же потоке, в котором был создан элемент пользовательского интерфейса...поэтому фоновые потоки используют Invoke/BeginInvoke получить эта работа выполняется в потоке UI.

демо, чтобы продемонстрировать проблемы с созданием элемента не-UI потока

<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);
        }
    }
}

вторичный поток пользовательского интерфейса aka "создание окна верхнего уровня в другом потоке"

можно создать вторичные UI-потоки, если вы помечаете поток как использующий модель квартиры STA и создаете Dispatcher (например,Dispatcher.Current) и запустить цикл" run" (Dispatcher.Run()) так Dispatcher может обслуживать сообщения для элементов пользовательского интерфейса, созданный на нитка.

но элемент, созданный в одном потоке пользовательского интерфейса, не может быть помещен в логическое / визуальное дерево другого элемент, который создается на другом Поток пользовательского интерфейса.

обходной метод для смешивания элементов, созданных на разных потоках пользовательского интерфейса

существует ограниченный метод обхода, который может предоставить вам некоторую возможность составить рендеринг элемента, созданного в одном потоке пользовательского интерфейса, с визуальным деревом, созданным в другом thread...by использование HostVisual. Видеть это пример:


нет, UserControls привязаны к потоку пользовательского интерфейса. Даже если бы вы могли инициализировать их в другом месте, у вас возникли бы проблемы с добавлением их в основной пользовательский интерфейс, поскольку они принадлежат другому потоку.


вы можете разделить визуализацию визуального дерева на разные потоки.

см. эту статью для хорошего объяснения и примера, который отображает видеовыход. http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx

но это действительно оправданно только тогда, когда фактический рендеринг визуального реализован в другом месте или технологически очень странно в приложении WPF, таком как рендеринг сцены Direct3D как Визуальный.

важное замечание здесь, как указано в статье, заключается в том, что если вторичные потоки отображают WPF XAML, то вы теряете входные события, потому что перенаправленные события не могут пересечь границу потока.