Найти в Дзене

Создание Revit-приложения с немодальным окном

Оглавление

Всем привет! Сегодня разберём создание приложения с немодальным окном и возникающие при этом сложности. Постараюсь всё рассказать на базовом уровне, без сторонних библиотек, с подробным объяснением.

Если вы ещё не знакомы с созданием WPF-окон для плагинов к Revit, то рекомендую ознакомиться с подборкой о WPF, потому что в итоговом коде будут описанные там вещи, которые я повторно описывать в этой статье не буду.

Немодальное окно

Появляющееся внутри приложения окно может быть модальным и немодальным. В чём разница? Модальное окно блокирует основное приложение и возвращается к нему только после закрытия окна. Пример — окно открытия файла в Ревите. Если мы его вызвали, мы не можем модифицировать открытый документ и вообще что-либо делать с Revit до тех пор, пока окно не закроется. До сих пор все окна плагинов, о которых я писал, были только модальными. Немодальное окно, в свою очередь, не блокирует вызвавшее его приложения. Пример немодального окна — Revit Lookup. Мы можем открыть его, передвинуть на второй экран и спокойно работать в Ревите.

Вызов окон

Модальное окно вызывается методом ShowDialog(), а немодальное — методом Show(). Одно и тоже окно может быть запросто вызвано как в модальном, так и в немодальном режиме. Эти методы работают по разному:

Метод ShowDialog
Метод ShowDialog
Метод Show
Метод Show

Это означает, что строки, следующие в коде за ShowDialog, будут выполнены только после закрытия окна. А строки, следующие за Show, будут выполнены сразу же. Для нас это важно тем, что IExternalCommand при использовании метода Show сразу уйдёт в return Result.Succeded и завершится.

Контекст API

А для нас это важно тем, что если мы в таком сценарии (после завершения выполнения IExternalCommand попробуем изменить модель по нажатию кнопки в окне, то получим попытку изменения модели вне контекста API. Это необрабатываемое исключение, которое приводит к фатальной ошибке Revit.

-3

Тут смысл, наверное, в том, что в контексте API разработчики предусмотрели встроенную обработку ошибок и возможность вернуть неудачный результат, а вне контекста — нет. Поэтому немодальные окна довольно требовательны к отладке — не обработанные ошибки приводят к крашу Ревита. Поэтому стоит все команды для немодальных окон оборачивать в try-catch.

Когда мы находимся в контексте API:

Вот именно этот хендлер нам и надо реализовать, чтобы у нас работало немодальное окно.

Далее разберёмся на примере:

Задача

(Задача актуальна для Ревита 2023 и позднее, в более ранних нет SelectionChanged)

Напишем немодальное окно, такое, чтобы при изменении выбранного в Ревите элемента отображалась его марка. Марку можно заменить в текстовом поле, нажать на кнопку и она обновится.

Решение

Ссылку на репозиторий с кодом приведу в конце статьи.

Вот такое окно получилось:

-4

Вот таким методом я обрабатываю событие SelectionChanged:

-5

На событие я подписываюсь в конструкторе ВьюМодели, который вызывается внутри IEXternalCommand (в контексте). Но мне нужно будет ещё и отписаться от события, а отписка будет происходить уже после завершения команды. Поэтому я передаю ViewModel в мой хэндлер, чтобы отписаться после выполнения команды в хендлере (находясь в контексте):

-6

Теперь давайте подробно рассмотрим сам ModelessWindowHandler:

IExternalEventHandler

Это интерфейс, предназначенный для обработки внешних событий. Внешние события в Revit API представлены классом ExternalEvent.

Суть тут такая: мы создаём внешнее событие ExternalEvent. В его конструктор по умолчанию заложен IExternalEventHandler. Затем мы в любом месте кода (вне контекста) можем вызвать это ExternalEvent через его метод Raise(). Вызов этого метода приведёт к тому, что указанный в событии IExternalEventHandler выполнит свой метод Execute(). Это обработка события из API, поэтому мы попадём в контекст API, то есть можем модифицировать модель Ревита.

-7

Создавать ExternalEvent (строка 17) мы можем только в контексте API. Я создаю Handler внутри команды, тут всё окей. Вызывать это событие (строка 26) мы можем где угодно. А вот метод Execute, хоть он и public, мы не вызываем вручную (0 ссылок), потому что вызывать его вручную можно, но только из контекста. Но зачем нам это: он сам вызовется при Raise у ExternalEvent, который мы можем вызывать из любого места.

Таким образом, выполнение команды по клику кнопки у немодального окна выполняется за счёт того, что вызывается метод Execute() у назначенного для этого хендлера, и мы вновь в контексте API тогда, когда нам нужно.

Метод Execute выглядит так:

-8

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

Ревитовская команда
Ревитовская команда
Команды из ViewModel
Команды из ViewModel

Минусы представленного решения

Данный плагин, конечно, работает. Вы можете поменять исполняемый код, и он будет работать у вас. И теперь вы знаете, что, как и почему с немодальными окнами в Ревите. Но именно так, как у меня, я делать не советую, и вот почему:

1. Нюансы подписки и отписки на события

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

Логично, что в таком сценарии отписку нужно поместить в событие Closed окна. Закрыли — отписались, проблемы нет. Но отписываться мы можем только в контексте API, а это событие вне контекста. То есть нам надо либо второй Handler, либо что-то придумать. Я придумал отписку сразу после завершения Execute. Но после этого перестаёт работать изменение выбранного элемента в окне, поэтому я закрываю окно. А в чём тогда разница между сценарием "выбрали элемент и показали окно" и тем, что в статье? Было бы круто, чтобы подписка отключалась именно по завершении жизненного цикла плагина (окна), и можно было бы менять параметр для любого числа элементов

2. Неуниверсальность хендлера

Это просто обобщённое описание проблемы из пункта 1. Если мы захотим сделать какое-то другое действие, то нам надо писать отдельный хендлер. А если действий много? Даже на отписку на событие, 1 строчку кода — отдельный класс с передачей в него кучи других объектов.

3. Передача ViewModel в Handler и обратно

Вообще, на первый взгляд у нас получился классический паттерн MVVM. Модель — это Handler, туда вынесен весь код, отвечающий за бизнес-логику (действия). ViewModel определяет содержимое View, и передаёт команду модели на выполнение действие.

Но Handler — не есть Model из паттерна. Во-первых, мы не можем в него вместить всю логику, у нас пока отдельный хендлер на каждое действие (просто именно тут оно одно). Во-вторых, передавать ViewModel во все подряд классы (кроме View) — не самое лучшее действие с точки зрения архитектуры (хотя это вопрос дискуссионный). Но очевидный минус моего кода в том, что я передаю ViewModel в Handler внутри конструктора ViewModel при инициализации. Это не очень красиво, да и из конструктора хендлера не очень понятно, откуда там берётся ViewModel, об этом знает только сама ViewModel

-11

4. В моём коде нет проблемы запустить сколько угодно окон одновременно. Это запросто может привести к багам.

Решение части этих проблем опубликовано во второй части статьи.

Заключение

Итоговый репозиторий лежит здесь. Решение той же задачи, но уже без указанных в конце минусов, я опубликую в одной из следующих статей. Не забывайте подписываться на мой телеграм-канал. Успехов в изучении Revit API, следите за качеством своего кода и до новых встреч!

-12