Revit API — работа с Extensible Storage

Механизм Extensible Storage в Revit API позволяет записывать и хранить информацию прямо в элементах модели. Это удобнее, чем хранить информацию где-то отдельно — не потеряется. А по сравнению в записью в параметры проекта или семейства — пользователь напрямую эту информацию не увидит и не сможет так просто поменять.
Но работа с этим "Хранилищем" организована не очень естественным образом, поэтому и написана данная заметка.

Сохранять информацию можно практически в любом элементе модели.
Для работы необходимо подключить пространство имен  Autodesk.Revit.DB.ExtensibleStorage.
Посмотреть сохраненную в элементе информацию можно через RevitLookup.

 

1. Создаем описание хранилища

Для начала создадим некий "шаблон" хранилища — т.н. "Schema". Нужно задать уникальный GUID для хранилища, его свойства, имена и типы полей для хранения.
GUID нам будет нужен еще неоднократно, поэтому можно объявить его где-нибудь в общедоступном месте:
public static string schemaGuid = "720080CB-DA99-40DC-9415-E53F280AA1F8";
Построение будет выполняться через "промежуточный" класс SchemaBuilder. Создаем его, используя GUID, задаем уровень доступа:

SchemaBuilder sb = new SchemaBuilder(new Guid(schemaGuid));
sb.SetReadAccessLevel(AccessLevel.Public);

Далее создаем поля для хранилища, опять же через промежуточный класс FieldBuilder. Изменить количество, тип, имя полей после создания не получится, надо продумать это заранее. Поля могут хранить типы string, int, double и т.д.
Также есть возможность создать поля-списки, но я не очень разобрался, как они работают.
Создадим два простых поля:
FieldBuilder fbName = sb.AddSimpleField("Username"typeof(string));
FieldBuilder fbAge = sb.AddSimpleField("UserAge"typeof(int));

Имена полей нам тоже в дальнейшем еще понадобятся для записи и считывания значений, поэтому их тоже лучше где-то отдельно хранить.
Задаем имя для хранилища:
sb.SetSchemaName("MyStorage");
И "запекаем" SchemaBuilder, получая Schema:
Schema sch = sb.Finish();
 

2. Выбираем элемент для хранения

Для того, чтобы работать с элементом, нужно получить его из документа. Рекомендую сделать для этой операции отдельный приватный метод.
Например, можно записать в ProjectInfo:
private Element GetStorageElement(Document doc)
        {
ProjectInfo pi = doc.ProjectInformation;
return pi as Element;
}

В выбранный элемент:
private Element GetStorageElement(UIDocument uiDoc)
        {
Document doc = uiDoc.Document;
Element elem = doc.GetElement(uiDoc.Selection.GetElementIds().First());
return elem;
}

Или вообще во что-то странное, например, в организацию браузера:
private Element GetStorageElement(Document doc)
        {
BrowserOrganization bo = new FilteredElementCollector(doc)
.OfClass(typeof(BrowserOrganization))
.Cast<BrowserOrganization>()
.First();
return bo as Element;
}


Дальнейшая работа не зависит от того, в каком именно элементе мы сохраняем информацию.

 

3. Записываем хранилище в элемент

Так как при записи информации мы фактически изменяем документ — потребуется открытая транзакция:
using(Transaction t = new Transaction(doc))
{
t.Start("Create storage");
        //здесь будет наш код
        t.Commit();
}
Соответственно, если мы находимся в режиме совместной работы — необходимо, чтобы был доступ на редактирование элемента.
Для начала получаем элемент модели, с которым будем работать:
Element elem = this.GetStorageElement(doc);
Из ранее созданной "Schema" получаем "поля", которые чуть позже используем для считывания значений из элемента. Потребуются имена, под которым мы их создавали:
Field fieldName = sch.GetField("Username");
Field fieldAge = sch.GetField("UserAge");
Также создаем объект Entity, в который будем записывать значения полей:
Entity ent = new Entity(sch);
Значения записываются через метод Entity.Set:
ent.Set<string>(fieldName"Иван Иванов");
ent.Set<int>(fieldAge21);
Записываем Entity в элемент:
elem.SetEntity(ent);
Всё, создано хранилище и информация записана в элемент:

Считываем информацию

Для того, чтобы считать записанную информацию, нужно получить элемент модели, знать GUID хранилища и имена параметров.
Получаем Schema:
Schema sch = Schema.Lookup(new Guid(schemaGuid));
Это достаточно любопытный момент — чтобы получить Schema, мне даже не нужен элемент модели. Судя по всему, все Schema, созданные в документе, хранятся где-то вместе, и для получения достаточно знать GUID.
Получаем элемент, из которого будем считывать информацию:
 
Element elem = this.GetStorageElement(doc);
 
Получаем Entity из элемента:
Entity ent = elem.GetEntity(sch);
Уже знакомым способом получаем "поля":
Field fieldName = sch.GetField("Username");
Field fieldAge = sch.GetField("UserAge");
 
Для считывания значений используем метод Entity.Get:
string userName = ent.Get<string>(fieldName);
int userAge = ent.Get<int>(fieldAge );
Для того, чтобы изменить значение поля, нужно воспользоваться методом Entity.Set, а затем Element.SetEntity. Нужно будет открыть транзакцию:
         private void UpdateAge()
{
Element elem = this.GetStorageElement(doc);
Schema sch = Schema.Lookup(new Guid(schemaGuid));
Entity ent = elem.GetEntity(sch);
Field f = sch.GetField("UserAge");
int age = ent.Get<int>(fr);
using(Transaction t = new Transaction(doc))
{
t.Start("Revision update");
ent.Set<int>(fr, famRev + 1);
elem.SetEntity(ent);
t.Commit();
}
}
Обновлять можно как одно поле, так и несколько.

Проверяем наличие Extensible Storage

В случае, если вы планируете хранить с помощью Extensible Storage какие-то настройки — есть задача проверить, есть ли уже настройки в элементе. Задача усложняется тем, что:

  • если в этом файле еще ни разу нчиего не записывалось под нужным GUID — вы получите исключение при попытке вызвать Schema.Lookup;
  • Если GUID уже использовался для других элементов — вы сможете получить Schema и Entity, но Entity будет "пустой".

Предлагаю использовать такую конструкцию:

private bool CheckStorageExists(Element elem, string sGuid)
        {
            try
            {
                Schema sch = Schema.Lookup(new Guid(sGuid));
                Entity ent = elem.GetEntity(sch);
                if (ent.Schema != null) return true;
            }
            catch { }
            return false;
        }

В этом случае:

  • Если Schema еще ни разу не создавалась — получим исключение, выйдем из блока try и код вернет false — "Хранилище отсутствует";
  • Если Schema уже создавался, но для других элементов — Entity будет создан, но свойство Entity.Schema будет равно null. Код выйдет из блока try и вернет false;
  • Если Schema создавался и был записан в элемент — свойство Entity.Schema не будет равно null. Код вернет true — "Хранилище присутствует".

 

Больше почитать можно на блоге The Building Coder.
Надеюсь, статья будет полезна начинающим, да и мне тоже, когда в следующий раз опять забуду, как оно правильно работает :)
Удачи в изучении Revit API!