0

Есть задача выполнить длительную по времени операция с контролом (WPF), дабы она не блокировала UI решил подсмотреть вариант здесь. По итогу вышел вот такой метод

async void PrintMethod()
        {
            try
            {
                await Task.Factory.StartNew(() => messageView.PrintDirect(), TaskCreationOptions.LongRunning);
            }
            catch(Exception ex)
            {
                logger.Error("Ошибка печати: {0}", ex.Message);
            }
        }

Однако, получаю ошибку следующего содержания: "Вызывающим потоком должен быть STA, поскольку этого требуют большинство компонентов UI". Если бы я создавал поток с использованием Thread, я бы использовал метод SetApartmentState(ApartmentState.STA), а вот что делать в таком случае я не знаю. Есть какие-нибудь вариаты?

  • Создавайте поток руками и устанавливайте ему все, что надо – tym32167 Oct 11 '18 at 12:47
  • Если создам руками, то для работы этого метода придется завернуть его в Dispatcher.Invoke() и все равно UI будет заблокирован – Ivan Kozlov Oct 11 '18 at 12:51
  • зачем это заворачивать в Dispatcher.Invoke? – tym32167 Oct 11 '18 at 13:08
  • потому что messageView - элемент UI, доступ к нему из фонового потока не получить без использования Dispatcher того же потока, в котором он был создан, следовательно, и вся операция будет выполняться в основном GUI-потоке и все встанет колом, пока не выполнится – Ivan Kozlov Oct 11 '18 at 13:10
  • Решение по ссылке - это не "легкий способ сделать, чтобы всегда не блокировался UI". Он предполагает, что "SomeLongOperation" не взаимодействует с UI. – MSDN.WhiteKnight Oct 11 '18 at 13:11
  • пфф, тогда вам Task.Factory.StartNew не поможет по тем же самым причинам – tym32167 Oct 11 '18 at 13:15
  • мм, допустим, а есть ли вариант, не блокируя интрефейс, в моем случае выполнить долгий метод? – Ivan Kozlov Oct 11 '18 at 13:23
  • У вас противоречие в самом вопросе. "Длительная операция с контролом" никак не может "не блокировать UI поток". Разделяйте UI и данные и используйте разные потоки. – dymanoid Oct 11 '18 at 13:25
  • смысл этой операции не подразумевает разделения данных и UI, потому что операция - печать содержимого таблицы, с учетом всего внешнего вида. И это внутренний метод контрола. – Ivan Kozlov Oct 11 '18 at 13:28
  • Тогда вам придётся работать в UI-потоке. Подключайте Dispatcher. Разбивайте ваш метод печати на мелкие части и выполняйте их последовательно как DispatcherOperation через Dispatcher UI-потока, при этом не забывая давать Dispatcherу выполнять другие операциии, чтобы UI не выглядел зависшим. – dymanoid Oct 11 '18 at 13:36
  • если вам надо провести долгую операцию с контролами, вы можете попробовать это делать в отдельном окне, чтобы не мешать контролы между потоками – tym32167 Oct 11 '18 at 13:36
  • @tym32167, у другого окна будет тот же Dispatcher и тот же UI-поток (если специально не создавать окно с другим Dispatcher и в другом потоке - но в этом случае контрол нельзя будет "перетащить" из первого окна во второе во время выполнения). – dymanoid Oct 11 '18 at 13:38
  • @dymanoid я про это и говорю, другое окно, другой поток, другой диспетчер, и не шарить контролы, чтобы тормоза во втором окне не задевали основное приложение – tym32167 Oct 11 '18 at 13:45

2 Answers2

0

Вот так можно показать окно в отдельном потоке и при этом оно будет в режиме STA, да, в этом окне вам придется повторить свой контрол (т.к. просто перенести его на другую форму у вас не получится):

BusyWindow _busyWindow = null;

object _busyWindowSync = new object();

private void ShowBusy()
{
    lock (_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            double left = Dispatcher.Invoke((Func<double>)(() => this.Left + this.Width / 2));
            double top = Dispatcher.Invoke((Func<double>)(() => this.Top + this.Height / 2));
            Thread newWindowThread = new Thread(new ParameterizedThreadStart(AnimationThreadStartingPoint));
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start(new Point() { X = left, Y = top });
        }
    }
}

private void AnimationThreadStartingPoint(object position)
{
    lock (_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            _busyWindow = new BusyWindow();
            _busyWindow.Left = ((Point)position).X;
            _busyWindow.Top = ((Point)position).Y;
            _busyWindow.Show();
        }
    }
    System.Windows.Threading.Dispatcher.Run();
}

private void HideBusy()
{
    lock (_busyWindowSync)
    {
        if (_busyWindow != null)
        {
            _busyWindow.Dispatcher.BeginInvoke((Action)_busyWindow.Close);
        }
    }
}

BusyWindow - это класс окна показываемого в отдельном потоке. Пример вот от сюда, и там это окно использовалось для анимации, которую нельзя отобразить в основном окне. Вы, наоборот, можете в этом окне выполнять длительную операцию, чтобы не подвисало основное окно. Ну или прямо взять этот пример и пока у вас "висит" главное окно, пользователь будет видеть спиннер.

  • Спасибо за информацию. Свою проблему я частично решил с помощью создания в отдельном потоке обычного окна, в котором есть вся требуемая мне разметка. Но я столкнулся с проблемой того, что это окно должно быть невидимым, а оно никак не хочет нормально скрываться и при этом выполнять метод. – Ivan Kozlov Oct 12 '18 at 07:29
  • Что значит нормально скрываться и выполнять метод? При скрытом окне компонент не отрисовывается и вы не можете его вывести на печать? – Алексей Лосев Oct 12 '18 at 07:42
  • Нет. Общая суть того, что я попытался реализовать: создается поток типа STA, в нем создается окно с разметкой, по событию Loaded для этого окна начинается выполнение долгой операции. Но первым делом в обработчике этого события я пытаюсь это дочернее окно сделать невидимым с помощью Visibility = Visibility.Collapsed, но почему-то окно продолжает висеть видимым. – Ivan Kozlov Oct 12 '18 at 07:51
  • Разбейте метод на два. В лоадед оставьте только скрытие и запуск нового метода через Dispatcher.BeginInvoke – Алексей Лосев Oct 12 '18 at 07:55
  • При использовании такого подхода фоновый поток заканчивается сразу после обработчика Loaded и основной метод не выполняется – Ivan Kozlov Oct 12 '18 at 08:14
0

По итогу я решил свою проблему следующим способом:

  1. Создал окно WPF с разметкой из одного требуемого мне контрола - полной копией того, что в основном окне.
  2. Создание этого окна сделал в отдельном потоке, отображал окно с помощью ShowDialog() так как он не дает потоку завершиться раньше времени.

    Thread hidePrintThread = new Thread(PrintMethod); hidePrintThread.SetApartmentState(ApartmentState.STA); hidePrintThread.IsBackground = true; hidePrintThread.Start();

    void PrintMethod()
        {
            try
            {
                CriteriaOperator co=null;
                List<MessageGridContent> mess = Service.Messages.ToList();
                Dispatcher.Invoke(() => co = messageGrid.FilterCriteria);
                HiddenPrintWindow window = new HiddenPrintWindow();
                window.MessagesToPrint = mess;
                window.CriteriaOperator = co;
                Service.Printing = true;
                window.ShowDialog();                
            }
            catch(Exception ex)
            {
                logger.Error("Ошибка печати: {0}", ex.Message);
            }
        }   
    
  3. Так как мне необходимо было, чтобы данная длительная операция проходила незаметно для пользователя, я решил скрыть окно. Стандартные методы работать отказывались. Я пробовал прописывать Visibyliti в разметке, пробовал в обработчике события загрузки окна, пробовал в методе, который запускается из обработчика загрузки, но окно упрямо висело видимым, тогда я убрал его на задний план (тоже подходило по ТЗ) с помощью WinAPI:

[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

Код инициализации и загрузки самого окна:

public HiddenPrintWindow()
        {
            InitializeComponent();    
            Loaded += HiddenPrintWindow_Loaded;
        }

private void HiddenPrintWindow_Loaded(object sender, RoutedEventArgs e)
            {
                SetWindowPos(new System.Windows.Interop.WindowInteropHelper(this).Handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
                Work();
            }

И спасибо всем помогавшим!!!