Сообщество программистов Autodesk в СНГ

ADN Club => Revit API => Тема начата: maksl от 13-03-2018, 17:05:08

Название: Прерывание тяжелого процесса
Отправлено: maksl от 13-03-2018, 17:05:08
Давайте тут обсудим возможность прерывания какого-то тяжелого процесса.

Как я вижу, стандартными событиями типа button_click это сделать не получается.

Код - C# [Выбрать]
  1.        
  2. private void button1_Click(object sender, EventArgs e)
  3.         {
  4.             FamilyStat.isStop = true;
  5.             MessageBox.Show("pressed");
  6.             this.Close();
  7.         }
  8.  

сообщение из этого примера никогда не появляется.

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

Код - C# [Выбрать]
  1.         void ShowProgBar()
  2.         {
  3.  
  4.             f_progr.Show();
  5.         }
  6.  
  7.  
  8.         public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
  9.         {
  10.                 Thread secThr = new Thread(new ThreadStart(ShowProgBar));
  11.                 secThr.Start();
  12.  
  13.                 foreach (string st in familyFiles)
  14.                 {
  15.                     if (isStop) { isStop = false; return Result.Cancelled; }
  16.  
  17.                         // тяжелый цикл
  18.                  }
  19.  
  20.                 f_progr.Close();
  21.  
  22.         }
  23.  
  24.  

Какие есть идеи?
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Пекшев aka Modis от 13-03-2018, 17:14:27
Вся проблема в том, что окно у вас работает в одном потоке, а тяжелый процесс происходит в другом потоке. Соответственно, поток тяжелого процесса не может узнать о том, что в окне произошло нажатие отмены. Примеры подобных решений можно найти для WPF-проектов, но как реализовать такое в Ревите - я пока не знаю. Не сталкивался и, если честно, работу с потоками до сих пор не понимаю ))
Так что тут я пас
Название: Re: Прерывание тяжелого процесса
Отправлено: maksl от 13-03-2018, 17:18:02
Вся проблема в том, что окно у вас работает в одном потоке, а тяжелый процесс происходит в другом потоке. Соответственно, поток тяжелого процесса не может узнать о том, что в окне произошло нажатие отмены. Примеры подобных решений можно найти для WPF-проектов, но как реализовать такое в Ревите - я пока не знаю. Не сталкивался и, если честно, работу с потоками до сих пор не понимаю ))
Так что тут я пас

в тяжелом цикле проверяется переменная isStop, которая меняется в button_click формы, которая в другом потоке. по идее такая связь должна бы работать, но проблема в том, что даже button_click не срабатывает, messageBox не отображается.
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Пекшев aka Modis от 13-03-2018, 17:20:03
в тяжелом цикле проверяется переменная isStop, которая меняется в button_click формы, которая в другом потоке. по идее такая связь должна бы работать, но проблема в том, что даже button_click не срабатывает, messageBox не отображается.
Все работает совсем не так, как вы себе представляете =)
Вопрос, кстати, интересный. Попробуем с коллегами что-нибудь
Название: Re: Прерывание тяжелого процесса
Отправлено: maksl от 13-03-2018, 17:25:04
сейчас проверил так:

Код - C# [Выбрать]
  1.  
  2.         void ShowProgBar()
  3.         {
  4.             f_progr.Show();
  5.             System.Threading.Thread.Sleep(5000);
  6.  
  7.             isStop = true;
  8.         }
  9.  
  10.  

Т.е. создается новый поток, который показывает форму и через 5 сек меняет переменную isStop (которая в обычном варианте должна меняться в button_click).
Как и предполагалось, тяжелый цикл завершился.

Так что почти все работает именно так, как я себе представляю. :)
Проблема остается только в том, чтобы заставить обработать событие button_click во время работы цикла.
Название: Re: Прерывание тяжелого процесса
Отправлено: Виктор Чекалин от 13-03-2018, 18:06:40
Я бы рекомендовал вместо Thread использовать класс Task (https://msdn.microsoft.com/ru-ru/library/system.threading.tasks.task(v=vs.110).aspx)
Это довольно удобная обертка над Thread Там же есть и способы отмены.
Общая идея в любом случае будет такая как и у вас. В цикле или в других нужных местах делается проверка, не было ли команды отмены.

Но тут будет проблема в другом. Форум то по Revit API. И если мы в этом цикле будем что то делать из другого потока, то ожидаемо рано или поздно получим либо ошибку, что нельзя работать из другого потока, либо Revit тупо вылетит. И это уже будет другая тема.
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Ривилис от 14-03-2018, 00:19:43
Проблема остается только в том, чтобы заставить обработать событие button_click во время работы цикла.
Явно где-то не хватает Application.DoEvents(), чтобы приложение могло обработать другие события. И этот вызов похоже должен быть где-то внутри "тяжелого цикла".
Название: Re: Прерывание тяжелого процесса
Отправлено: Виктор Чекалин от 14-03-2018, 07:37:27
И если мы в этом цикле будем что то делать из другого потока, то ожидаемо рано или поздно получим либо ошибку, что нельзя работать из другого потока, либо Revit тупо вылетит.
Прошу прощения, только сейчас заметил, что в коде только прогресс бар вызывается из другого потока, а Revit API вызывается в основном потоке.

Я такую задачу успешно решил. Алгоритм примерно следующий.
1) Форма открывается в основном потоке
2) При открытии формы или при нажатии на кнопку на этой форме запускается некая команда, в которой выполняется длительный процесс. Этот длительный процесс запускаю с помощью Task в другом потоке.
3) Тут проблема в том, что запустив длительный процесс в другом потоке, нельзя вызывать команды Revit API. Поэтому в тот момент, когда в цикле нужно обратиться к методам Revit API, эти методы я вызываю из ОСНОВНОГО потока. Таким образом, получаются безопасные вызовы к методам Revit API.
4) Кнопка отмены доступна в тот момент, когда не вызываются методы Revit API.

Псевдокод примерно такой
Код - C# [Выбрать]
  1. // метод вызывается при нажатии на кнопку или при запуске формы и запускает долгий процесс
  2. private void btn1_OnClick()
  3. {
  4. Task longTask =
  5.                 new Task(DoSomething);
  6. // запустили долгую задачу в отдельном процессе
  7. longTask.Run();
  8. }
  9.  
  10. void DoSomething()
  11. {
  12.      foreach(var i in collection)
  13.     {
  14.            // тут делаем что то с помощью Revit API, но нужно делать это в ОСНОВНОМ потоке. CallRevitAPI вызывает методы в ОСНОВНОМ потоке
  15.           CallRevitAPI(()=> {doc.GetElement(id);})
  16.          
  17.           if (cancelled)
  18.               return;
  19.     }
  20. }
  21.  
  22. //кнопка Cancel доступна, так как долгая задача в другом потоке.
  23. void btnCancel_OnClick()
  24. {
  25.     cancelled = true;
  26. }
  27.  

Когда я реализовывал подобный подход, основная сложность была как правильно передать основной поток, находясь в другом потоке. Это тоже успешно решается. Я постараюсь набросать тестовый пример, выдрав из моего основного проекта.

Явно где-то не хватает Application.DoEvents()
Рискую начать холивар, но вроде как это bad practice (https://stackoverflow.com/questions/5181777/use-of-application-doevents)
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Пекшев aka Modis от 14-03-2018, 09:57:06
Пытались вчера сделать подобное - Ревит падает, хоть убей)
При попытках я нашел вариант, когда можно даже не использовать ни второй поток, ни Application.DoEvents():
Код - C# [Выбрать]
  1. using System;
  2. using System.Threading;
  3. using System.Windows;
  4. using Autodesk.Revit.Attributes;
  5. using Autodesk.Revit.DB;
  6. using Autodesk.Revit.UI;
  7.  
  8. namespace KRDimensions
  9. {
  10.     [Transaction(TransactionMode.Manual)]
  11.     public class Test : IExternalCommand
  12.     {
  13.         public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
  14.         {
  15.             ProgressWindow progressWindow = new ProgressWindow();
  16.             progressWindow.Show();
  17.             try
  18.             {
  19.                 // long process with progress window
  20.                 for (int i = 0; i < 10; i++)
  21.                 {
  22.                     Thread.Sleep(500);
  23.                     progressWindow.Dispatcher.Invoke(() => progressWindow.TbProgressText.Text = "progress: " + i);
  24.                     if(StopProcess)
  25.                         break;
  26.                 }
  27.             }
  28.             catch (Exception exception)
  29.             {
  30.                 MessageBox.Show(exception.Message);
  31.             }
  32.  
  33.             progressWindow.Dispatcher.Invoke(() => { progressWindow.Close(); });
  34.  
  35.             return Result.Succeeded;
  36.         }
  37.  
  38.         public static bool StopProcess { get; set; } = false;
  39.     }
  40. }
Само окно:
Код - XML [Выбрать]
  1. <Window x:Class="KRDimensions.ProgressWindow"
  2.        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.        SizeToContent="Manual" Width="350" ResizeMode="NoResize" Height="100"
  5.        WindowStartupLocation="CenterScreen">
  6.     <StackPanel Orientation="Vertical">
  7.         <TextBlock Text="Progress text:"></TextBlock>
  8.         <TextBlock x:Name="TbProgressText"></TextBlock>
  9.         <Button Content="Cancel" Name="BtCancel" Click="BtCancel_OnClick"></Button>
  10.     </StackPanel>
  11. </Window>
И Code behind окна:
Код - C# [Выбрать]
  1. using System.Windows;
  2.  
  3. namespace KRDimensions
  4. {
  5.     public partial class ProgressWindow
  6.     {
  7.         public ProgressWindow()
  8.         {
  9.             InitializeComponent();
  10.         }
  11.  
  12.         private void BtCancel_OnClick(object sender, RoutedEventArgs e)
  13.         {
  14.             Dispatcher.Invoke(() => { Test.StopProcess = true; });
  15.         }
  16.     }
  17. }

С виду это все работает так как надо - появляется окно в котором отображается прогресс (в примере просто текстом) и по кнопке "Отмена" долгий процесс прерывается. Самая главная проблема - окно не модальное и пользователь в этот момент может потыкать мышкой в Ревит.
Мой ответ не является вариантом решения. Вдруг просто кому пригодится )
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Ривилис от 14-03-2018, 12:52:43
Цитата: Александр Ривилис от 13-03-2018, 23:19:43

    Явно где-то не хватает Application.DoEvents()

Рискую начать холивар, но вроде как это bad practice
Если есть лучшее решение, то я не против...
Название: Re: Прерывание тяжелого процесса
Отправлено: Александр Пекшев aka Modis от 15-03-2018, 12:12:31
В общем - решение нашли и все достаточно просто. И не нужны никакие Application.DoEvents(). Тестовый проект прикладываю.

В принципе, решение было найдено еще позавчера, но столкнулся с проблемой - у меня есть проект, в который я добавлял новую команду и тестировал в Ревите. Вот из этого проекта если запустить, то Ревит зависает. Но когда сделал новый проект с нуля и повторил в нем все тоже самое - заработало как надо. Разницу в проектах я так и не нашел и причин зависания так и не узнал
Название: Re: Прерывание тяжелого процесса
Отправлено: maksl от 20-03-2018, 09:49:58
навалились другие задачи, пока даже попробовать не могу. но идея хорошая. я чуть погуглил и нашел еще один вариант, народ предлагает использовать async-await:
https://ru.stackoverflow.com/questions/418461/%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0-%D1%81-%D0%BA%D0%BE%D0%BD%D1%82%D1%80%D0%BE%D0%BB%D0%B0%D0%BC%D0%B8-%D0%B8%D0%B7-%D1%84%D0%BE%D0%BD%D0%BE%D0%B2%D0%BE%D0%B3%D0%BE-%D0%BF%D0%BE%D1%82%D0%BE%D0%BA%D0%B0/418463#418463

но пока отложил этот вопрос, вернусь к нему попозже.