Управление общими параметрами с помощью Revit API
Всем привет! Сегодня рассмотрим довольно сложную и интересную тему: добавление общих параметров, а также изменение назначенных им категорий. Тема сложная, потому что на некоторые категории я так и не смог программно добавить параметры. Впрочем, обо всё по порядку.
Исходные данные
- Файл на основе пустого шаблона
- Привязанный к Revit файл общих параметров с 3 тестовыми параметрами, первый из которых имеет тип “Денежная единица”, чтобы изменяться по экземплярам групп Первый параметр накинут на категорию стены, остальные просто существуют в ФОПе
- Первый параметр накинут только на стены и не может изменяться по экземплярам групп, ещё 2 не накинуты
Параметры, которые будем использовать, добавлены в активный ФОП
Первый параметр накинут только на стены и не может изменяться по экземплярам групп, ещё 2 не накинуты
Задачи:
- Добавленный Параметр 1 накинуть на все категории и подкатегории, сделать изменяемым по группам.
- Параметр 2 накинуть на все категории, по экземпляру.
- Параметр 3 накинуть на все категории, по типу.
Порядок решения задачи
Сначала найдём всё, что нам нужно, в Revit Lookup и на https://www.revitapidocs.com/2023/. Затем я покажу код, который выполняет задачу, и подробно распишу его. Ну а потом посмотрим результат.
Поиск решения с помощью Revit Lookup
Настоятельно советую скачать самую новую версию Revit Lookup. Помимо обновлённого дизайна, она более удобная, практичная и понятная. Как и где качать.
С помощью Revit Lookup мы можем решить только первую часть задачи: накинуть категории на существующий параметр. Для второй части чуть сложнее: Revit Lookup не может показать то, чего ещё нет в документе.
Итак, данные обо всех параметрах и накинутых на них категорий отличаются от документа к документу.
Поэтому выбираем Revit Lookup — Snoop Document (или выбираем любой элемент, у него есть свойство Document, и переходим по нему).
У Документа есть свойство ParameterBindings. В общем-то, там всё и хранится:

Мы видим объект BindingMap. У меня добавлен один параметр, поэтому он и содержит только один элемент с тем самым именем параметра.
Рассмотрим класс BindingMap.
В этой статье нас интересуют методы Insert (добавляет параметр) и ReInsert (обновляет параметр).

Для вызова этого метода нам нужны экземпляры класса Definition и Binding. Давайте разбираться по порядку.
Если вы уже имели опыт работы с параметрами через API,
то вы, наверное, заметили, что у параметра есть свойство Definition.
Да, с существующим параметром всё просто: находим его и берём это свойство. А как взять Definition от ещё не созданного параметра?
Вспомним статью про наследование и посмотрим, что это за Definition.
У этого класса есть 2 наследника: InternalDefinition
и ExternalDefinition.
При этом в описании к свойству Definition класса Parameter указано, что оно всегда возвращает InternalDefinition. А в описании класса ExternalDefinition сказано:
The ExternalDefinition object can be created by a definition Group object from a shared parameters file.
То есть на самом деле всё просто. InternalDefinition — это внутреннее определение существующего внутри Revit параметра (любого). А ExternalDefinition — это внешнее определение параметра (только общего, существующего пока только в виде строки в ФОП для данного документа).
PS1. Если слово Definition было написано слишком часто, пишите в комментарии, постараюсь исправится
PS2. На самом деле, у этих классов может быть более подробное предназначение и более сильная разница, которую я могу не знать. Если знаете, в чём дело — тоже пишите в комментарии.
Итак, что делать с существующим параметром, понятно. Как взять параметр из ФОПа, рассмотрим чуть позже.
Далее идёт класс Binding. У него сложная структура наследования. Нас интересуют классы InstanceBinding
и TypeBinding, и их конструкторы, принимающие CategorySet.
В чём суть:
Нам нужно создать сет из категорий, на которые мы хотим накинуть параметры
На основании этого сета создать соответствующий Binding
Давайте рассмотри код, который создаёт сет из всех категорий:
private static CategorySet CreateFullCategorySet(Document doc)
{
var categorySet = new CategorySet();
foreach (Category category in doc.Settings.Categories)
{
if (category.AllowsBoundParameters)
{
categorySet.Insert(category);
}
else
continue;
if (category.SubCategories.IsEmpty == false)
{
foreach (var value in category.SubCategories)
{
if (value is Category subCategory)
{
if (subCategory.AllowsBoundParameters)
categorySet.Insert(subCategory);
}
}
}
}
return categorySet;
}
Потом мы вызовем этот метод из основного кода команды Создаём пустой сет.
Получаем список всех категорий документа по свойствам Settings.Categories — строка 5 (попробуйте сами найти их в Lookup).
Проверяем, позволяет ли категория привязывать параметры (AllowsBoundParameters == true) — строка 7. Если да, добавляем сет. Если нет, идём к следующей.
Далее проверяем подкатегории. Если подкатегория позволяет привязку параметров, добавляем её в сет.
Почему так странно проверяем категории?
Дело в том, что цикл foreach может проходить только с объектами, наследующими интерфейс IEnumerable (перечисляемое). Как работает цикл foreach: создается объект Enumerator (перечислитель), он вызывает метод MoveNext(), пока ему есть куда двигаться, а затем возвращает объект по свойству Current. В свойстве SubCategories использован не обобщённый Enumerator, поэтому он возвращает Object вместо Category.
Поэтому я пишу строку
if (value is Category subCategory)
Которая проверяет тип и объявляет переменную
Подробнее читайте здесь
Добавление в проект параметра из файла общих параметров
Откроем файл общих параметров:
Application application = commandData.Application.Application;
var parametersFile = application.OpenSharedParameterFile();
А далее рассмотрим метод поиска:
private static Definition FindDefinition(
string parameterName,
DefinitionFile parametersFile)
{
foreach (var definitionGroup in parametersFile.Groups)
{
foreach (ExternalDefinition definition in definitionGroup.Definitions)
{
if (definition.Name == parameterName)
return definition;
}
}
return null;
}
DefinitionFile
содержит свойство Groups. Это группы параметров. Внутри каждой есть свойство Definitions, которое представляет собой набор ExternalDefinition. Далее нам нужно просто выбрать тот, что нам нужен.
Примечание: делать такой код — неправильно. Лучше проверять не Name, а GUID. Имя может совпасть или поменяться, GUID же уникален.
Я дважды вызову этот метод в коде, и получу Definition для второго и третьего параметров по их имени.
Итоговый код
public Result Execute(
ExternalCommandData commandData,
ref string message,
ElementSet elements)
{
Application application = commandData.Application.Application;
Document doc = commandData.Application.ActiveUIDocument.Document;
string parameterName1 = "SN_Параметр 1";
string parameterName2 = "SN_Параметр 2";
string parameterName3 = "SN_Параметр 3";
var parametersFile = application.OpenSharedParameterFile();
Definition def2 = FindDefinition(parameterName2, parametersFile);
Definition def3 = FindDefinition(parameterName3, parametersFile);
var sharedParameters = new FilteredElementCollector(doc)
.OfClass(typeof(SharedParameterElement))
.ToElements()
.Cast<SharedParameterElement>();
var bindings = doc.ParameterBindings;
CategorySet categorySet = CreateFullCategorySet(doc);
InstanceBinding binding = new InstanceBinding(categorySet);
using (Transaction t = new Transaction(doc, "Изменение категорий параметра"))
{
t.Start();
foreach (var parameter in sharedParameters)
{
if (parameter.GetDefinition().Name == parameterName1)
{
bindings.ReInsert(parameter.GetDefinition(), binding);
parameter.GetDefinition().SetAllowVaryBetweenGroups(doc, true);
}
}
var instanceBinding = new InstanceBinding(categorySet);
var typeBinding = new TypeBinding(categorySet);
bindings.Insert(def2, instanceBinding);
bindings.Insert(def3, typeBinding);
t.Commit();
}
return Result.Succeeded;
}
Ссылка на гитхаб будет в конце статьи. Ранее указанные методы там тоже будут
Опишу тут то, о чём не говорил ранее:
В 18 я нахожу все общие параметры с помощью FilteredElementCollector.
Созданный на строке 24 сет категорий я применю во всех 3 параметрах. Это необязательно, если у вас другие задачи, то создавайте отдельные сеты под каждый параметр.
В строке 36 я вызываю
parameter.GetDefinition().SetAllowVaryBetweenGroups(doc, true);
Это нужно, чтобы параметр изменялся по экземплярам групп. Отмечу, что метод ReInsert почему-то сбивал у меня эту настройку, поэтому я вызвал SetAllowVaryBetweenGroups после него.
Добавление категорий обязательно оборачиваем в транзакцию — это модификация документа
Результат работы плагина:

Итак, 3 параметра, с заявленными настройками, появились в проекте. Также на скрине вы можете видеть проблему, о которой я говорил в самом начале статьи:
- Категория “Короба” в Ревите как бы одна, а в окошке “Свойства параметров” их две. Одна для коробов с соединительными деталями, другая для коробов без них. А накинуть параметр программно можно только на первую.
- Почему не активна галочка “Опоры”? А вы найдите её в Revit Lookup (Document—Settings—Categories). У неё
AllowsBoundParameters == false. Руками при этом привязать к ней параметры вполне возможно. Если вы знаете рабочее решение, как пофиксить это, пишите в комментарии.
Возможные ошибки
Плагин может не сработать, если:
- В Revit не выбран файл общих параметров.
- В ФОП нет того параметра, который написан в коде.
- В
CategorySetпопала категория сAllowsBoundParameters== false. Если у вас не сработал по другой причине, и вы разобрались почему, и считаете важным сообщить другим, пишите мне в телеграм-канал. Ладно, это я для разнообразия, шучу, пишите здесь в комментарии (но и там тоже можете).
Итоговый код в моём репозитории на GitHub
А на этом всё. Пробуйте воспроизвести это самостоятельно с поправкой на свои нужды, делитесь мыслями и результатами с коллегами под этой статьёй, и подписывайтесь на мой телеграм-канал. Всем успехов в изучении Revit API!