Управление общими параметрами с помощью Revit API


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

Исходные данные

  • Файл на основе пустого шаблона
  • Привязанный к Revit файл общих параметров с 3 тестовыми параметрами, первый из которых имеет тип “Денежная единица”, чтобы изменяться по экземплярам групп Первый параметр накинут на категорию стены, остальные просто существуют в ФОПе
  • Первый параметр накинут только на стены и не может изменяться по экземплярам групп, ещё 2 не накинуты Параметры, которые будем использовать, добавлены в активный ФОП Параметры, которые будем использовать, добавлены в активный ФОП

Первый параметр накинут только на стены и не может изменяться по экземплярам групп, ещё 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. В общем-то, там всё и хранится: Parameter Bindings

Мы видим объект BindingMap. У меня добавлен один параметр, поэтому он и содержит только один элемент с тем самым именем параметра.

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

Для вызова этого метода нам нужны экземпляры класса 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!