Найти в Дзене
IT's BIM | Revit и C#

Как сделать окна плагина Revit отзывчивыми: от зависаний к асинхронности

Год назад у меня была простая мечта: сделать так, чтобы мой плагин для Revit выглядел и ощущался как современное приложение. Я хотел красивые окна, работающие шкалы прогресса и возможность отменить операцию, если что-то пошло не так. Но я тогда столкнулся с непостижимой для меня проблемой: я нажимаю кнопку — и интерфейс замирает. Окно «застывает», курсор начинает крутиться, и пользователь сидит в неведении: «Оно работает или зависло?». Тогда я еще плохо разбирался в тонкостях C#, WPF и программирования в принципе, для меня это казалось таким сложным, и была прям мысль «вот бы кто-то умный рассказал, как это исправить». По итогу, мне пришлось разбираться в этом всем самому. И в этой статье я поделюсь с вами результатом: как же все-таки сделать свои окошки «отзывчивыми». Поговорим о том, причем тут потоки, как с ними работать в контексте Revit и как реализовать всё это по принципу TAP (асинхронный паттерн задач). Чтобы понять, почему всё зависало, нужно заглянуть «под капот». И там мы об
Оглавление

Введение

Год назад у меня была простая мечта: сделать так, чтобы мой плагин для Revit выглядел и ощущался как современное приложение. Я хотел красивые окна, работающие шкалы прогресса и возможность отменить операцию, если что-то пошло не так.

Но я тогда столкнулся с непостижимой для меня проблемой: я нажимаю кнопку — и интерфейс замирает. Окно «застывает», курсор начинает крутиться, и пользователь сидит в неведении: «Оно работает или зависло?».

Тогда я еще плохо разбирался в тонкостях C#, WPF и программирования в принципе, для меня это казалось таким сложным, и была прям мысль «вот бы кто-то умный рассказал, как это исправить».

По итогу, мне пришлось разбираться в этом всем самому. И в этой статье я поделюсь с вами результатом: как же все-таки сделать свои окошки «отзывчивыми». Поговорим о том, причем тут потоки, как с ними работать в контексте Revit и как реализовать всё это по принципу TAP (асинхронный паттерн задач).

Revit API, UI и главный поток

Как работает главный поток

Чтобы понять, почему всё зависало, нужно заглянуть «под капот». И там мы обнаружим главного виновника — Главный Поток Revit (Revit Main Thread).

Представьте, что поток — это однополосная дорога. По ней могут ехать машины.

  • Revit API очень требовательный водитель. Он разрешает себе ехать по этой дороге только одному. Никаких обгонов, никаких встречных полос. Все вызовы к документу (создание стен, получение параметров) должны происходить строго в этом потоке.
  • WPF (наш интерфейс) по умолчанию тоже ездит по этой дороге. То есть, чтобы отрисовать кнопку, обновить текст или показать прогресс-бар, ему нужно проехать по ней.

Где возникает затор?

Когда вы нажимаете кнопку «запуск» в плагине, вы запускаете код. Если этот код обращается к Revit API (например, собирает элементы), то Revit-водитель выезжает на эту дорогу и едет. И тут интерфейс пытается перерисовать кнопку: он выезжает на эту же дорогу, и начинает ехать за Revit-водителем. Но обогнать он его не может — дорога однополосная, и Revit-водитель не хочет сместиться, чтобы пропустить.

В результате:

  1. Окно не реагирует на клики.
  2. Прогресс-бар не обновляется (ему просто не дают «проехать» и сменить кадр).
  3. Windows помечает окно как «Не отвечает».

Создаем «дополнительную дорогу»

Когда я осознал, что интерфейс и Revit дерутся за один поток, решение пришло само: нужно дать интерфейсу свою «полосу движения».

В мире WPF это означает создание отдельного STA-потока.

STA (Single Threaded Apartment) — это режим, в котором потоки работают с визуальными элементами WPF. Если коротко: без него окна просто не создадутся.

Я написал небольшой сервис-хост, который берет на себя всю рутину: создает поток, запускает диспетчер сообщений и корректно закрывает окна. Вот как это примерно выглядит:

Создание отдельного STA-потока для UI с помощью сервиса
Создание отдельного STA-потока для UI с помощью сервиса

Разберем ключевые моменты:

1. ApartmentState.STA — обязательно. Если забыть эту строку, при создании первого окна вы получите исключение. WPF требует, чтобы все визуальные элементы создавались и использовались только в потоке, который пометил себя как STA.

2. Dispatcher.Run() — «мотор» интерфейса. Этот метод запускает цикл обработки сообщений: клики мыши, нажатия клавиш, перерисовка. Без него окно откроется, но будет «мертвым» — не будет реагировать на действия пользователя.

3. IsBackground = true — чтобы не «висел» процесс. Эта настройка говорит системе: «если основное приложение (Revit) закроется, этот поток можно завершить принудительно». Иначе после закрытия Revit ваш плагин может остаться висеть в диспетчере задач.

4. Корректная очистка. В методе Shutdown мы не просто «убиваем» поток, а аккуратно просим диспетчер завершить работу. Это важно, чтобы сработали все события Closed, отписались подписки и освободились ресурсы.

Как это выглядит в использовании:

Пример использования сервиса ModelessUiHost.
Пример использования сервиса ModelessUiHost.

Что это дало нам:

  • Прогресс-бары обновляются плавно, даже когда Revit отрабатывает код.
  • Кнопка «Отмена» реагирует мгновенно — пользователь не чувствует себя заложником.
  • Окна можно перетаскивать, сворачивать, переключаться между ними — интерфейс полностью отзывчив.

Но появилась новая задача: теперь окна живут отдельно от Revit. Более того, я не могу вызывать методы Revit API из этого окна, потому что Revit четко требует, чтобы вызовы к его API выполнялись из главного потока. Как заставить их «общаться»?

«Мост» между потоками, или налаживаем «общение»

Когда я вынес UI в отдельный поток, я получил отзывчивый интерфейс. Но тут же возникла новая проблема: теперь у меня два изолированных мира. Если из потока UI попробовать вызвать doc.GetElement(), Revit выбросит исключение о нарушении потока. Мне нужен был «мост» между этими мирами.


Единственный легальный способ: IExternalEvent

В Revit API есть только один официальный механизм для вызова кода извне —
IExternalEvent.

Как это работает:

  1. Вы создаёте класс, который реализует интерфейс IExternalEvent.
  2. Через ExternalEvent.Create() получаете объект-«кнопку».
  3. Вызываете .Raise() — это как позвонить в дверь.
  4. Revit в свободное время вызывает ваш метод Execute() — уже в своём главном потоке.

Проблема: стандартный IExternalEvent работает по принципу «выстрелил и забыл». Он не возвращает результат. А мне нужно было получить данные обратно в UI.

Решение: Очередь задач + TaskCompletionSource

Я написал класс RevitTask, который работает как «диспетчерская» между потоками. Вот как он устроен:

Пример обертки RevitTask для TAP работы с Revit API.
Пример обертки RevitTask для TAP работы с Revit API.

Код выглядит объёмным, но на самом деле здесь всего четыре ключевых идеи. Разберём каждую.

1. ConcurrentQueue<Action> — потокобезопасная очередь

Это самое важное место. Обратите внимание: в очередь мы кладём не просто «работу», а
работу плюс инструкцию, что делать с результатом:

Что мы кладем в очередь
Что мы кладем в очередь

Почему это важно? Многие думают, что await где-то снаружи опрашивает: «Готово? Готово?».

Нет. Мы заранее упаковываем в очередь команду: «Когда сделаешь — нажми эту кнопку (SetResult)».

Таким образом, весь цикл жизни задачи (выполнение + возврат) происходит внутри Revit. UI просто сидит и ждёт сигнала.

Почему очередь?

  • ExternalEvent может сработать не мгновенно — задачи накапливаются.
  • Очередь гарантирует порядок: первая задача пришла → первая выполнилась.
  • Concurrent означает, что класть задачи можно из любого потока без блокировок.

2. TaskCompletionSource — «чек с номером»

Это объект, который позволяет создать
Task вручную.

Создание TaskCompletionSource<T> для возврата Task с результатом T
Создание TaskCompletionSource<T> для возврата Task с результатом T

UI получает Task мгновенно. Для вызывающего кода это выглядит как обычная асинхронная операция. Но внутри «чека» пусто, пока Revit не вызовет SetResult.

Почему TrySetResult, а не SetResult? Если задача была отменена или уже завершена, SetResult выбросит исключение. TrySetResult безопасно проверяет состояние и ничего не ломает.

3. ExternalEvent.Raise() — звонок в дверь

Вызов ExternalEvent.Raise() для передачи Revit задач на выполнение
Вызов ExternalEvent.Raise() для передачи Revit задач на выполнение

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

4. RevitEventHandler.

Класс RevitEventHandler и его метод Execute.
Класс RevitEventHandler и его метод Execute.

Когда Revit принимает событие, он вызывает Execute(). Здесь мы:

  • Забираем все задачи из очереди (while цикл).
  • Выполняем их в главном потоке Revit.
  • Внутри каждой задачи срабатывает tcs.SetResult — это разблокирует await в UI.

Почему while, а не if? Если пользователь быстро нажал несколько кнопок, в очереди может скопиться несколько задач. while гарантирует, что все они будут обработаны за один вызов Execute().

Как это выглядит в использовании

Пример использования класса RevitTask
Пример использования класса RevitTask

Обратите внимание: код в ViewModel остаётся линейным и читаемым. Вся сложность с потоками скрыта внутри RevitTask

Итоги и пара слов об ограничениях

Нужно уточнить еще раз: это не сделает Revit быстрее. Это не позволит ему одновременно собирать списки элементов в модели и расставлять семейства. Revit всё так же будет выполнять задачи последовательно.

Тогда зачем это всё? Только ради пользователя.

Потому что людей раздражают программы, которые не оповещают о своем состоянии. Когда мы запускаем плагин работать, а он не может даже сказать, на сколько процентов выполнил задачу, мы невольно злимся. В голове появляются мысли:

  • «А вдруг он завис?»
  • «Сколько ему еще это делать?»
  • «Может, перезагрузить, быстрее пойдет?»

Всё, что мы сделали в этой статье, нужно только для того, чтобы ваши окна не блокировались и могли ответить пользователю на эти вопросы. Успокоить его. Показать прогресс. Дать возможность отменить задачу, если он что-то напутал.

UX — не менее важная часть при разработке. И нужно стараться делать свои решения User-Friendly.

--

Понравилась статья? Подписывайтесь, чтобы не пропустить новые материалы по автоматизации и Revit API.

Все анонсы и дополнительные инсайты из разработки я публикую в своём Telegram-канале. Там всегда можно задать вопрос или обсудить решение в комментариях.

Логотип
Логотип