Найти тему

Extensible Storage — что это и с чем это едят

Оглавление

Всем привет! Сегодня будет очень большая и подробная статья. Я задумывал её как обычное описание поведения классов в Revit API, но в процессе перешёл к созданию приложения по работе со Schema (далее для простоты — схема), написал очень много кода и сделал очень интересную штуку.

Сегодня вы узнаете, как создавать свои схемы, как записывать в них данные, чем отличаются Schema, Field и Entity и как работать с ArrayField и MapField. По пути я напомню вам про конвертёры в WPF и расскажу про DataTemplateSelector Поехали!

Введение

В этой статье мы подробно рассмотрим классы из пространства имён Autodesk.Revit.DB.ExtensibleStorage. Перед началом я дам их краткое описание.

При работе с внешним хранилищем мы имеем 3 основных класса: Schema, Entity, Field. Что это такое:

Schema — это контракт на хранение данных. Мы говорим, что в этой схеме мы будем хранить данные определённого типа в таких-то полях (Field), эти поля будут простыми (Simple), массивами (Array) или словарями (Map), при необходимости назначаем им единицы измерения. Информация о схеме существует в документе отдельно от всех элементов.

Entity — это реализация схемы в конкретном элементе. У нас есть схема, и мы можем создать Entity этой схемы в элементе, записав данные нужного типа в нужные нам поля Field. Информация об Entity хранится в самом элементе, это как бы часть элемента

Field — это поле схемы. Оно есть как у схемы (и тогда оно представляет собой описание поля), так и у Entity (тогда это записанное в поле значение).

Использование внешнего хранилища в плагинах

Внешнее хранилище используется, чтобы хранить информацию так, чтобы её не видел пользователь. При желании, мы можем вообще хранить её так, что кроме нашего плагина никто не сможет её увидеть, поставив ReadAccessLevel и WriteAcessLevel в значение Application. Но это не очень удобно, потому что тогда информация скрывается и от разработчика, и через Revit Lookup её не увидеть. Иногда сложно понять, успешно ли произошла запись в схему, или нет, поэтому я решил создать приложение для работы со схемами.

Задача для приложения

Я реализовал следующий функционал:

  1. Просмотр всех схем в документе.
  2. Редактирование значений SimpleField в существующих Entity (только для строк).
  3. Редактирование значений ArrayField в существующих Entity (только для строк).
  4. Редактирование значений MapField в существующих Entity (только для строк).
  5. Создание новых Entity по существующим схемам.
  6. Создание новых схем, в том числе:
  • Добавление простых (Simple) Field
  • Добавление массивов (Array) Field
  • Добавление словарей (Map) Field
  • Добавление в поля данных любого доступного типа
  • Назначение полям любых возможных единиц измерения

Вообще, согласно документации Revit API, мы можем добавлять в схему поля со следующим типом данных: Boolean, Byte, Int16, Int32, Float, Double, ElementId, GUID, String, XYZ, UV, Entity. Однако, при редактировании их в окне мы неизбежно переводим их в строки. Поэтому, чтобы не загромождать приложение ненужной валидацией, я оставил поддержку только для строк. При заполнении схемы из кода таких проблем не будет — мы сразу имеем нужный тип данных.

Просмотр схем

Разберёмся сначала с самой простой задачей: просмотр существующих схем. Я сделал это по такому алгоритму:

1. При нажатии на кнопку появляется окно. Если был выделен элемент, дальше работаем с ним. Если нет, то пользователь может выделить элемент, нажав на соответствующую кнопку

2. В окне мы видим ComboBox со списком всех схем. Если для данной схемы у элемента существует Entity с записанными полями, то выводим их значения в DataGrid. Если нет, то в DataGrid будут лишь имена полей.

Звучит просто? Кажется да, но на самом деле нет. Давайте разбираться:

Я создал класс SchemaViewerViewModel. При выборе элемента он собирает все схемы и для каждой создаёт класс EntityDescriptor. При его создание для каждого Field создаётся FieldDescriptor одного из 3 возможных типов. Так мы получаем список всех схем и список всех полей в схемах (со значениями).

Тут я немного нарушаю терминологию — если у элемента нет Entity от данной схемы, то я отображаю пустую схему, но всё равно создаю для неё EntityDescriptor, хотя по сути там нет Entity.

Что ж, давайте посмотрим код ViewModel, а именно собирающий все схемы и поля метод:

Соответственно, в классе EntityDescriptor определено 2 конструктора: он создаётся либо по схеме, либо по Entity. Посмотрим конструктор со схемой, там всё просто:

-2

Всё просто, потому что нам не надо считывать значения Fields — их просто ещё нет, и мы создаём пустые поля:

-3

А вот с Entity оказалось чуть интереснее. Сам конструктор, кажется, не сильно меняется:

-4

Но вот методы Describe уже поинтереснее:

-5

На практике значение будет получаться по другому, потому что мы будем знать тип поля. Допустим, мы знаем, что там хранится bool:

var value = entity.Get<bool>(field.FieldName);

Вместо написанного:

var info = typeof(Entity).GetMethod("Get", [typeof(string)]);
var getMethod = info!.MakeGenericMethod(valueType);
var value = getMethod.Invoke(entity, [field.FieldName]);

Дело в том, что в данном случае я не знаю, какой тип данных будет в поле. При этом, мне его надо как-то указать явно. Я не могу передать его как переменную valueType, хоть она и имеет тип Type, но нужен не тип, а его имя. Поэтому я применяю такой хитрый ход.

Однако, с полем-массивом или словарём ситуация ещё более интересная (а ещё и она не описана в документации к API): ArrayField хранит в себе IList<T>, а MapField хранит в себе IDictionary<Tkey, TValue>. Соответственно, нам нужно вызвать Generic метод, хранящий в себе Generic тип:

-6

Сначала мы создаём дженериковый тип IList, а затем вызываем метод. Со словарём примерно тоже самое:

-7

Когда же мы работаем с известными полями, созданными нашим кодом, получение значений чуть проще:

ArrayField

var value = entity.Get<IList<string>(field.FieldName);

MapField:

var value = entity.Get<IDictionary<string, string>(field.FieldName);

Итак, у нас получился инструмент для просмотра схем. для выбранного элемента. Выглядит он вот так:

Если у элемента есть Entity этой схемы, то строка будет зелёной, иначе — красной
Если у элемента есть Entity этой схемы, то строка будет зелёной, иначе — красной

Сейчас добавим в инструмент функцию создания новых схем.

Создание схем

За создание схем отвечают классы SchemaBuilder и FieldBuilder. Мы создаём объект SchemaBuilder, передаём в него базовые параметры: имя схемы, документацию, VendorId и Guid схемы, а потом добавляем поля. Поля можно добавлять просто: методами AddSimpleField, AddArrayField, AddMapField, а можно с помощью FieldBuilder. Главный смысл FieldBuilder в том, что мы можем проверить, нужны ли для поля единицы измерения или Guid вложенной схемы, и добавить их, если на то есть необходимость.

Для создания схемы я создал ещё одно окно:

-9

Там мы задаём параметры схемы и добавляем поля любого требуемого типа.

Вот результат:

-10

В принципе, создание схем в работе с ExtensibleStorage — самое простое. По этому код для сохранения схемы получился небольшой:

-11

Методы добавления полей довольно простые:

-12

Тут мы проверяем, нужны ли нам единицы измерения, и, если да, назначаем их.

В коде я бы советовал сделать так:

  • Хранить Guid схемы
  • При необходимость обращения к схеме вызывать Schema.Lookup с её Guid
  • Если она не найдена, то создавать новую схему
  • При создании схемы не нужны дескрипторы: просто напрямую добавляем нужные нам поля нужного нам типа. Не забывайте, что поля типа double и UV требуют назначения единиц измерения.

Всё, после вызова метода Finish схема готова.

Редактирование схем

Окей, мы создали схему, теперь мы хотим отредактировать её. Как нам это сделать?

Проще всего (удивительно) работать с простым полем, на то оно и Simple. Мы просто записываем в Entity новое значение:

var entity = _element.GetEntity(schema);
if (entity.Schema is null)
{
entity = new Entity(schema);
}
entity.Set(fieldName, value);
_element.SetEntity(entity);

Вам просто нужно знать имя поля и желаемое значение.

Для ArrayField чуть интереснее:

var entity = _element.GetEntity(schema);
if (entity.Schema is null)
{
entity = new Entity(schema);
}
var list = entity.Get<IList<string>>(fieldName);
// modify this list as you need
entity.Set(fieldName, list);
_element.SetEntity(entity);

Тут нам нужно обязательно получить значение в виде IList<T>, потому что мы не можем создать экземпляр интерфейса, а записывать нужно именно IList<T>.

Со словарём плюс-минус тоже самое:

var entity = _element.GetEntity(schema);
if (entity.Schema is null)
{
entity = new Entity(schema);
}
var dictionary = entity.Get<IDictionary<string, string>>(fieldName);
// modify this dictionary as you need
entity.Set(fieldName, dictionary);
_element.SetEntity(entity);

В общем-то, и всё. Таким нехитрым способом вы можете редактировать ваши схемы и поля через код. Но мне нужно сделать кое-что по интереснее: менять значения через пользовательский интерфейс.

DataTemplateSelector

А как это сделать? Ладно с SimpleField: у меня есть N SimpleField, я создаю таблицу в N строк, в первом столбце имя поля, во втором — значение. Меняем значение, считываем изменённое значение и перезаписываем. Звучит несложно, реализуется тоже. А как нам отобразить ArrayField в той же таблице? А словарь?

Тут на помощь и придёт DataTemplateSelector. Мы представим строки нашей таблицы в виде разных классов, унаследованных от базового класса или интерфейса, а затем будем менять их отображение, в зависимости от того, какой класс представляет данную строку:

-13

В данном проекте я не запаривался над дизайном, так что я не настаиваю, что надо делать именно такой внешний вид. Но я показываю, как им можно управлять

Итак, система классов у нас такая:

Базовый FieldDescriptor:

-14

И его наследники, которых вы уже видели:

Simple
Simple
Array
Array
Map
Map

В классе EntityDescriptor есть коллекция FieldDescriptor, являющаяся источником данных для DataGrid. Создадим же теперь DataTemplateSelector.

Шаг 1. Создание шаблонов для данных

Я просто создал xaml-файл Dictionary.xaml и определил там 3 шаблона данных:

-18

Обратите внимание на строку 33:

<views:DataGridCellSelector x:Key="DataGridCellSelector" />

С её помощью я получу возможность использовать созданный на втором шаге селектор как ресурc в xaml.

Шаг 2. Создание селектора:

Очень простой класс:

-19

Шаг 3: Подключение словаря:

Я добавил в ресурсы окна ссылку на этот словарь:

<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/ExtensibleStorageExample;component/Views/Dictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>

Шаг 4. Переопределение DataGridCell:

-20

Готово, теперь, если у нас есть Array или MapField, мы можем нажать на кнопку и в появившейся таблице изменить значения:

-21

Нужно ввести значения в строку и нажать Enter, тогда строка сохранится и появится новая строка.

Сохраняю данные, закрываю приложение и открываю снова. Entity появилась, теперь строка с именем схемы — зелёная:

-22

Разные цвета я сделал, конечно же, с помощью конвертёров (это кстати моя самая недооценённая статья, её почти не читают, хотя с помощью конвертёров можно делать крутые вещи. Если дочитали до сюда, обязательно загляните в неё, прочитайте, поставьте лайк и используйте в своей работе в будущем).

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

Классика, как обычно, реализация метода вынесена отдельно.
Классика, как обычно, реализация метода вынесена отдельно.

Отмечу, что тут я игнорирую поля, которые представлены не строками, так как я не могу легко их валидировать. В случае, если Entity ранее не существовало, я создаю новый Entity на строках 57-60.

Реализация же методов сохранения почти такая же, как в вышенаписанном коде-примере для 3 разных типов полей:

-24

Вот теперь почти всё. Я рассказал вам, как создавать схемы, читать их и перезаписывать их, а также рассказал, как работает моё приложение по их управлению.

Класс DataStorage

Но вообще в пространстве имён остался один класс, о котором я не сказал ни слова. Что это?
Часто бывает, что нам надо хранить информацию о документе в схеме. Очевидное решение: хранить её в объекте
ProjectInformation. Однако, мы можем создать объект DataStorage. Это невидимый элемент, смысл которого в том, чтобы хранить информацию для документа в схеме. Обычно используем так: ваш плагин ищет DataStorage с нужным именем, и, если не находит, то создаёт его и задают правильное имя. Далее ищет схему, и если не находит... ну, тут вы уже знаете

Заключение

Это моя одна из самых сложных работ для блога. Только на написание кода ушло несколько вечеров. И код всё равно получился не идеальным. Например, из минусов: можно создать схему с невалидным VendorId или именем, я проверяю только их наличие. Естественно, попытка создать такую схему вызовет исключение. Так что не кидайтесь в меня тряпками, а будьте аккуратны при создании схем этим приложением.

Также при записи данных в схему все поля не-строки просто игнорируются, сообщения об этом не выводятся.

Как обычно, если заметите что-то, о чём я забыл рассказать, найдёте ошибки в тексте или в коде, или захотите рассказать о своём опыте — обязательно пишите в комментарии и делитесь с сообществом, это важно и полезно.

Итоговый код на гитхабе:

GitHub - SergeyNefyodov/ManyCommandsApplication: Repository for Revit API Blog

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

-25