Recently, I have been asked by friend to develope a Revit plugin to help improve his workflow in terms of productivity. I have come across a proble that I need to store a snippet of data in the Revit file for purpose of cache. According to the develper guide in the official website, you can achieve it via two ways: Shared parameters and Extensible storage. But neither of them are not perfectly suiting my requirements.

For Shared parameters, the data can be viewed by end user if set, which is not what I want. I just want a backend storage that silently contains this data and it’s only programatically accessible. In addition, the name implies this parameter is supposed to be a piece of shared data whereas I simply just want a private data. As a result, this has been crossed from my checklist.

I then started looking at next recommended API: Extensible storage. Its functionality seems very promissing and very flexible on storing data in the file. It provides various of common data types that should be enough for most of use cases such as int, string, bool and so on. The only complaint I have is that the data must be attached to an element in order to store it. This has concerned me that the data will be gone if the user has removed the element from Revit. I want something globally storing the data.

I did a few research on internet and an article advised to use ProjectInformation entity as the carrier of the data as it’s singleton and it always exists. Sounds like a good idea. But I still want to attach the data into the root Document object rather than an element’s document. Finally, I found that from the same site there is another article mentioned using DataStorage (an interface introduced in Revit 2013 APIs) to not bother storing data in an element of Revit model. I started writting some proof of concept code and surprisingly, it is quite easy to use. But there are still few tricks that need to be aware whilst in development, which I’ll cover in the following examples.

Create a storage

To create a storage, you just need to retrieve the root Document object from the ExternalCommandData and then pass it to the method: Create in DataStorage. Next step, you need to define a schema for the data that you want to store. Within the schema, you define the name and type for each field that your data snipe wants to be. After that, pass it to the constructor of an Entity class and then you can use the Set method to store the data. Finally, link the entity in the storage. Also, the entire process needs to be wrapped in a Transaction scope. Otherwise, your change won’t be applied.

var document = commandData.Application.ActiveUIDocument.Document;
using (var transaction = new Transaction(document, TXN_NAME)) {
    if (transaction.Start() == TransactionStatus.Started) {

        var storage = DataStorage.Create(document);

        var entity = new Entity(getSchema());
        entity.Set("field1", "I am a string");
        entity.Set("field2", 123.4);

        storage.SetEntity(entity);

        if (transaction.Commit() != TransactionStatus.Committed) {
            TaskDialog.Show(TXN_NAME, "Save StorageTransaction failed to commit");
        }

    } else {
        TaskDialog.Show(TXN_NAME, "Save Storage Transaction cant' start");
    }
}

private static Schema getSchema() {
    var existingSchema = Schema.Lookup(SchemaId);

    if (existingSchema != null) {
        return existingSchema;
    }

    SchemaBuilder schemaBuilder = new SchemaBuilder(Guid.newGuid());
    schemaBuilder.SetReadAccessLevel(AccessLevel.Vendor);
    schemaBuilder.SetWriteAccessLevel(AccessLevel.Vendor);
    schemaBuilder.SetVendorId("vendorid"); // restrict access to only a particular vendor.
    schemaBuilder.SetSchemaName("schema_name);

    var versionField = schemaBuilder.AddSimpleField("field1", typeof(string));
    versionField.SetDocumentation("An example field");

    var customDataStorageField = schemaBuilder.AddSimpleField("field2", typeof(double));
    customDataStorageField.SetDocumentation("store a double type");

    Schema newSchema = schemaBuilder.Finish();

    return newSchema;
}

If your command succeeds (it returns Result.Success), then your data should be successfully stored. Keep this in mind because I spent a few hours on troubleshooting why it’s not working and then realise my PoC code returned a Result.Failure that reset my session data T_T. A true story…

Find the data storage

Once you have created the data storage, you will need to look up it for later usage. With the ElementFilter, you can do that easily.

private Element search() {
    var document = commandData.Application.ActiveUIDocument.Document;
    var filter = new FilteredElementCollector(document);

    // i am being lazy here. If you have multiple storage, you need to loop it and find the one with the data you want
    var dataStorage = filter.OfClass(typeof(DataStorage)).FirstElement();

    return dataStorage;
}

Update Data Storage

Now you are able to create and search data storage. What if you want to update the data in it? You will find out the same piece of code that creates the storage that won’t work once the storage already exists. It won’t overwrite the existing storage. Hence, you need to look up the existing storage first and then do the update. It will be something like this:

var storage = search();

using (var transaction = new Transaction(document, TXN_NAME)) {
    if (transaction.Start() == TransactionStatus.Started) {

        if (storage == null) {
            storage = DataStorage.Create(document);
        }

        var entity = new Entity(getSchema());
        entity.Set("field1", "I am a new string");
        entity.Set("field2", 512.4);

        storage.SetEntity(entity);

        if (transaction.Commit() != TransactionStatus.Committed) {
            TaskDialog.Show(TXN_NAME, "Save StorageTransaction failed to commit");
        }

    } else {
        TaskDialog.Show(TXN_NAME, "Save Storage Transaction cant' start");
    }
}

This way will update the data storage correctly.