Всем привет! Сегодня статья об архитектуре больших плагинов под Revit. Тема большая и сложная, много скриншотов, так что устраивайтесь поудобнее, читайте внимательно и делитесь вашим опытом в комментариях.
Эта статья о создании приложения, в котором будут условно 5 и более кнопок. Я не рассматриваю здесь архитектурные вопросы самих плагинов (IExternalCommand), только создание общей архитектуры (IExternalApplication).
Шаг 1.
Самый важный шаг — выбрать хороший шаблон проекта. Чтобы вам не пришлось каждый раз вручную писать addin-файлы, жонглировать зависимостями, думать, как опять подключить Community.Toolkit.Mvvm или добавить сборку. Я возьму шаблон от Романа Карповича Nice3Point. Он имеет следующий преимущества:
- Встроенная поддержка нескольких версий Revit.
- Встроенная поддержка сборки msi локально и на GitHub.
- Встроенная поддержка WPF и автоматическое создание ViewModel.
- Удобный класс Сontext для доступа к текущему документу в любом месте.
Так как на основе этого шага мы будем делать все дальнейшие действия, распишу его подробно:
- Открываем Visual Studio и нажимаем "Продолжить без кода":
4. Запускаем терминал:
5. Пишем там "dotnet new install Nice3point.Revit.Templates" и жмём Enter
Шаг 2. Создание решения
Шаблоны установлены, можно создавать проект:
1. Создаём проект на шаблоне Revit Addin Solution:
2. Настраиваем проект
Шаг 3. Создание проекта для решения
Мы будем создавать приложения по принципу: один модуль — один проект. В большинстве случаев будет так: одна команда — один модуль, но, вообще, это не обязательно.
Подробнее о проектах и решениях.
В C# мы имеем Решение: файл .sln. В состав Решения входят Проекты .csproj. Проекты могут ссылаться друг на друга, но только в одну сторону
Вот тут начинается интересный момент. Проекты не могут ссылаться друг на друга в обе стороны. То есть один проект может знать о существовании другого, но тогда второй ничего не знает о первом.
Поэтому я предлагаю такое решение проблемы нескольких команд в одном приложении:
1. Создаём проект для приложения Application, который знает всё о всех кнопках — других проектах.
2. Создаём проекты-кнопки.
3. Создаём общий проект CommonProject для вынесения общей логики в одно место для уменьшения дублирования кода.
4. При необходимости создаём новые общие проекты с другими задачами.
На диаграмме зависимостей это будет выглядеть так:
Вернёмся к нашему решению. Оно сейчас выглядит так:
Создадим проект для IExternalApplication. Жмём на решение правой кнопкой и выбираем Добавить — Создать проект. Выбираем Revit Addin Application:
У нас получилось приложение:
У него есть addin-файл и уже создана панель. Отлично, минус одна задача для нас.
Шаг 4. Создание остальных проектов
Я создам 3 проекта: 2 для команд, и один проект для вынесения общей для всех проектов логики. Для этого я использую шаблон Revit Addin Module. Он не создаёт addin-файла и лишних папок:
Для общего проекта создадим проект без пользовательского интерфейса. Для команд использую модальное окно:
Вот такой результат получился:
Шаг 5. Настройка связей между проектами
Сделаем так:
1. В проекте CommonProject определим класс RevitShell со статическим методом SayHello, который будет выводить сообщение "Hello world!"
2. В проекте Application создадим кнопки для команд из других проектов
3. Внутри команд определим внутреннюю логику команды, а также выведем сообщение из проекта CommonProject.
RevitShell в CommonProject теперь выглядит так:
Здесь я использую одну из фишек шаблона: обращаюсь к текущему документу из удобного места через класс Сontext, не задумываясь о том, как передать этот документ через ExternalCommandData.
Вот так мы добавляем ссылки на команды и их проекты в класс Application:
И точно так же создаём ссылку на RevitShell в проектах командах:
Важно: ваши проекты не обязаны ссылаться на общий проект, может быть, у вас там и не будет никакой логики, подходящей для вынесения в общий проект, и вы обойдётесь без него, или конкретный проект обойдётся без ссылки на CommonProject. Но я рекомендую создать его заранее, чтобы в случае чего легко можно было его подключить и не дублировать код. И самое важное: CommonProject не должен ссылаться на проекты-команды.
Итак, я добавил всё нужную мне логику, и все связи между проектами. Теперь могу запустить приложение.
Шаг 6. Запуск
Шаблон от Nice3Point поддерживает удобную сборку и отладку проекта. Выберем активную конфигурацию решения Application, конфигурацию Debug R23 и запустим проект в режиме отладки. Ревит откроется автоматически:
Результат:
В папке %username\AppData\Roaming\Autodesk\Revit\Addins\2023\Application появились созданные нами dll и все нужные зависимости. Addin-файл также скопировался:
Шаг 7 (бонусный) Получение msi
Я уже писал про развёртывание сборки, которое мы делаем сами от начала до конца. Шаблоны от Романа поддерживают сборку "из коробки". В Visual Studio откроем терминал и напишем "nuke CreateInstaller" и нажмём Enter.
Если ранее не пользовались nuke, то надо его установить, написав в терминале команду "dotnet tool install Nuke.GlobalTool --global", а затем запускать установку через "nuke CreateInstaller".
Сначала вылезет ошибка, потому что в файле Build.Configuration у нас указано название решение, не совпадающие с название проекта Application. Меняем:
Перезапустим команду "nuke CreateInstaller" в терминале. В папке решения появилась папка output с msi-сборкой.
Заключение
Итак, у меня получилось создать решение, в которое легко можно добавить новые проекты для других команд, вынести общую логику в отдельный проект. В такой конфигурации очень сложно запутаться в папках и файлах — если один проект будет сложный с кучей всего кроме Views и ViewModels, то всё это останется в рамках одного проекта. Однако, данное решение не претендует на то, чтобы быть истиной в последней инстанции: если у вас есть опыт разработки приложения для Revit с большим числом кнопок, то делитесь им в комментариях, особенно, если считаете, что данную задачу можно решить другим способом.
На этом на сегодня всё. Итоговый код — на Github. Проект, скорее всего, будет "обрастать мясом", но общая концепция — останется. Не забывайте ставить звёзды на мои репозитории и подписываться на мой телеграм-канал о Revit API. До новых встреч!