FilteredElementCollector


В сегодняшней статье рассмотрим очень важный класс — FilteredElementCollector. Этот класс позволяет нам взять некоторые (или все) элементы из документа, чтобы потом делать с ними определённые действия.

При работе в Dynamo его заменяют ноды All Elements Of Category, All Elements Of Type и некоторые другие. В C# же их всех заменяет FilteredElementCollector.

Для начала внимательно изучим описание класса на revitapidocs.com. Чтобы начать работать с ним, нам надо создать его экземпляр с помощью одного из 3 конструкторов: Конструкторы класса FilteredElementCollector Конструкторы класса FilteredElementCollector

Первый принимает документ (сбор элементов по всему документу), второй — документ и ElementId вида (сбор элементов из документа на данном виде), третий — документ и коллекцию ElementId (фильтрация предварительно собранных элементов).

Как передать документ в нашу команду — вспомним из статьи о создании плагина. Далее мы можем создать экземпляр FilteredElementCollector:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document);
    return Result.Succeeded;
}

В данном случае вместо явного указания типа я использую ключевое слово var. Это означает, что компилятор сам определит тип переменной. Обычно на C# используют имеено это ключевое слово при объявлении переменной.

Строка var collector = new FilteredElementCollector(document); гораздо короче, чем FilteredElementCollector collector = new FilteredElementCollector(document);, при этом в обоих случаях тип переменной понятен (до вызова дополнительных методов).

Теперь рассмотрим методы этого класса из справки: Раскроем список Methods, выделим любой и ознакомимся с его описанием Раскроем список Methods, выделим любой и ознакомимся с его описанием

Данные методы помогают нам применять фильтры к коллектору, объединять отфильтрованные элементы с другими, переводить коллектор в список элементов или их Id, проходить по коллектору в цикле foreach. Рассмотрим наиболее важные из них.

Метод OfCategory принимает значение из enumeration (перечисление) BuiltInCategory и возвращает нам список всех элементов этой категории. То есть мы передаём ему не имя категории, а встроенное имя категории в OST_-формате (независимое от языка Revit). Как же его узнать?

Зная название категории на английском, перейти на страницу BuiltInCategory, нажать Ctrl-F, написать его и найти категорию:

Нашли стены (OST_Walls) Нашли стены (OST_Walls)

Выделить элемент и посмотреть в Revit Lookup.

Нашли двери (OST_Doors) Нашли двери (OST_Doors)

Далее синтаксис такой:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document)
        .OfCategory(BuiltInCategory.OST_Doors);
    return Result.Succeeded;
}

Передаём в метод название перечисления BuiltInCategory, затем через точку — элемент перечисления.

  • public FilteredElementCollector OfCategoryId( ElementId categoryId )

Метод возвращает тоже самое — все элементы категории, но принимает Id категории (будьте внимательны, он всегда со знаком минус, выделить Категорию внутри Ревита нельзя).

Узнаём через Lookup: Id отрицательный Id отрицательный

Синтаксис такой:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document)
        .OfCategoryId(new ElementId(-2000023));
    return Result.Succeeded;
}

Вообще, в конструктор для ElementId можно передать и BuiltInCategory — VisualStudio предложит вам такой вариант. И да, он сработает:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document)
        .OfCategoryId(new ElementId(BuiltInCategory.OST_Doors));
    return Result.Succeeded;
}

На самом деле, OST_Doors имеет внутри этот самый шифр -2000023. Как это устроено — читайте здесь.

  • public FilteredElementCollector OfClass( Type type )

Собирает элементы класса. В чём разница c двумя предыдущими? Объясню на примерах:

Есть категория Стены и класс Wall. Но первые 2 метода вернут нам и экземпляры, и типы стен в одном списке (тип элемента — тоже элемент), и, например, когда мы попытаемся во все элементы этого списка в параметр “Комментарии” написать значение, то получим ошибку, ведь у типа такого параметра нет. Класс может охватывать несколько категорий. MEPCurve — это трубы, лотки и воздуховоды. Так мы можем взять их все в одно действие. Элемент может не иметь категории (например, общий параметр — SharedParameterElement) В общем, класс — это совсем не категория. Синтаксис метода такой:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document)
        .OfClass(typeof(Wall));
    return Result.Succeeded;
}

Обязательно пишем ключевое слово typeof. Мы передаём не имя класса, а его тип (Type), и оно возвращает нам именно тип класса.

public FilteredElementCollector WhereElementIsNotElementType()

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

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector0 = new FilteredElementCollector(document)
        .OfClass(typeof(WallType));
    var collector1 = new FilteredElementCollector(document)
       .OfCategory(BuiltInCategory.OST_Walls)
       .WhereElementIsElementType();
    return Result.Succeeded;
}

То есть в первом случае мы берём все элементы класса WallType (типоразмеры стен), а во втором — все элементы категории “Стены”, а потом из них берём только типоразмеры. Результат (в 99% случаях) будет одинаковый. Пишу 99%, потому что до конца не уверен, как поведут себя типы стен, импортированные из IFC.

  • public IList ToElements() и public IList ToElementIds()

Вызываются в конце. Превращают коллектор в список элементов или их Id. В большинстве случаев я пишу ToElements(). Потому что:

  • Cast()

Этого метода нет в API, это метод расширения LINQ. Он приводит (“кастует”) все элементы к другому типу.

Зачем нам это? Допустим, мы собрали все воздуховоды в список и хотим сделать с ними что-то (вписать габариты в параметр “Комментарии”. Мы взяли все элементы класса Duct, и идём по ним в цикле. Но вот незадача — у элементов нет свойств воздуховодов. А в Lookup мы их видим: Свойства воздуховода (Duct) и MEPCurve (базового класса) Свойства воздуховода (Duct) и MEPCurve (базового класса)

Дело в том, что в цикле мы идём по коллекции, состоящей из Element. Это мы знаем, что в ней воздуховоды, а компилятор — не знает. И такой код не скомпилирует. То есть мы в цикле должны ввести для каждого элемента новую переменную типа Duct, привести её к нужному типу:

Duct duct = element as Duct;

И далее работать с ней. Проблемы особой нет, всего одна лишняя строчка.

Но можно решить элегантнее, и вызвать Cast<Duct>() после ToElements():

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var collector = new FilteredElementCollector(document)
        .OfClass(typeof(Duct))
        .Cast<Duct>()
        .ToList();
    return Result.Succeeded;
}

Данный код вернёт нам список воздуховодов (List<Duct>), и теперь мы можем для элементов списка использовать их свойства и методы. В конце я также вызвал ToList(), так как Cast возвращает IEnumerable, а не List.

А, ну и самое важное — тип, к которому вы приводите, нужно указывать в угловых скобках (вместо <Duct> из примера).

  • public FilteredElementCollector WherePasses( ElementFilter filter )

Этот метод позволяет применить к коллекции любой другой фильтр из множества фильтров RevitAPI. Это тема для ещё одной большой статьи, поэтому подробно рассматривать её мы не будем. Если вам интересна эта тема, пишите в комментарии.

Для использования мы сначала создаём фильтр, а потом передаём его в коллектор:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    var uiDocument = commandData.Application.ActiveUIDocument;
    var document = uiDocument.Document;
    var reference = uiDocument.Selection.PickObject(Autodesk.Revit.UI.Selection.ObjectType.Element);
    var element = document.GetElement(reference);
    var filter = new ElementIntersectsElementFilter(element);
    var collector = new FilteredElementCollector(document)
        .OfClass(typeof(Duct))
        .WherePasses(filter)
        .Cast<Duct>()
        .ToList();
    return Result.Succeeded;
}

А на этом всё. В следующей статье мы научимся создавать транзакции и изменять элементы.

Подписывайтесь на мой телеграм-канал о Revit API. До новых встреч!