Extensible Storage of a Map

I already discussed the important new and powerful

extensible storage
mechanism,
and we also explained and demonstrated it in both the

DevHelp Online

and

Revit 2012 API webcast
presentations,
both of which were recorded.

I updated the sample code for the latter webcast.
Here is the mainline of the Execute method:


public Result Execute(
  ExternalCommandData commandData,
  ref string message,
  ElementSet elements )
{
  UIApplication uiapp = commandData.Application;
  UIDocument uidoc = uiapp.ActiveUIDocument;
  Document doc = uidoc.Document;
 
  try
  {
    // pick an element and define the XYZ 
    // data to store at the same time
 
    Reference r = uidoc.Selection.PickObject(
      ObjectType.Face,
      new WallFilter(),
      "Please pick a wall at a point on one of its faces" );
 
    Wall wall = doc.get_Element( r.ElementId ) as Wall;
    XYZ dataToStore = r.GlobalPoint;
 
    Transaction t = new Transaction( doc,
      "Create Extensible Storage Schemata and Store Data" );
 
    t.Start();
 
    // store the data, and also 
    // demonstrate reading it back
 
    StoreDataInWall( wall, dataToStore );
 
    t.Commit();
 
    // list all schemas in memory across all documents
 
    ListSchemas();
 
    return Result.Succeeded;
  }
  catch( Exception ex )
  {
    message = ex.Message;
    return Result.Failed;
  }
}

It prompts you to select a wall at a specific point.
The PickObject method can return both the selected wall and the picked point from one single user click.
It also takes a WallFilter instance, an implementation of the ISelectionFilter interface, to restrict the user selection to walls only:


class WallFilter : ISelectionFilter
{
  public bool AllowElement( Element e )
  {
    return e is Wall;
  }
 
  public bool AllowReference( Reference r, XYZ p )
  {
    return true;
  }
}

The selected point is stored on the wall in a newly created extensible storage schema by the following helper method:


/// <summary>
/// Create an extensible storage schema, 
/// attach it to a wall, populate it with data, 
/// and retrieve the data back from the wall.
/// </summary>
void StoreDataInWall(
  Wall wall,
  XYZ dataToStore )
{
  SchemaBuilder schemaBuilder = new SchemaBuilder(
    new Guid( "720080CB-DA99-40DC-9415-E53F280AA1F0" ) );
 
  // allow anyone to read the object
 
  schemaBuilder.SetReadAccessLevel(
    AccessLevel.Public );
 
  // restrict writing to this vendor only
 
  schemaBuilder.SetWriteAccessLevel(
    AccessLevel.Vendor );
 
  // required because of restricted write-access
 
  schemaBuilder.SetVendorId( "ADNP" );
 
  // create a field to store an XYZ
 
  FieldBuilder fieldBuilder = schemaBuilder
    .AddSimpleField( "WireSpliceLocation",
    typeof( XYZ ) );
 
  fieldBuilder.SetUnitType( UnitType.UT_Length );
 
  fieldBuilder.SetDocumentation( "A stored "
    + "location value representing a wiring "
    + "splice in a wall." );
 
  schemaBuilder.SetSchemaName( "WireSpliceLocation" );
 
  // register the schema
 
  Schema schema = schemaBuilder.Finish();
 
  // create an entity (object) for this schema (class)
 
  Entity entity = new Entity( schema );
 
  // get the field from the schema
 
  Field fieldSpliceLocation = schema.GetField(
    "WireSpliceLocation" );
 
  // set the value for this entity
 
  entity.Set<XYZ>( fieldSpliceLocation, dataToStore,
    DisplayUnitType.DUT_METERS );
 
  // store the entity on the element
 
  wall.SetEntity( entity );
 
  // read back the data from the wall
 
  Entity retrievedEntity = wall.GetEntity( schema );
 
  XYZ retrievedData = retrievedEntity.Get<XYZ>(
    schema.GetField( "WireSpliceLocation" ),
    DisplayUnitType.DUT_METERS );
}

The mainline also calls another little method ListSchemas which shows how all schemas currently loaded into Revit memory can be accessed and listed:


/// <summary>
/// List all schemas in Revit memory across all documents.
/// </summary>
void ListSchemas()
{
  IList<Schema> schemas = Schema.ListSchemas();
 
  int n = schemas.Count;
 
  Debug.Print(
    string.Format( "{0} schema{1} defined:",
      n, PluralSuffix( n ) ) );
 
  foreach( Schema s in schemas )
  {
    IList<Field> fields = s.ListFields();
 
    n = fields.Count;
 
    Debug.Print(
      string.Format( "Schema '{0}' has {1} field{2}:",
        s.SchemaName, n, PluralSuffix( n ) ) );
 
    foreach( Field f in fields )
    {
      Debug.Print(
        string.Format(
          "Field '{0}' has value type {1}"
          + " and unit type {2}", f.FieldName,
          f.ValueType, f.UnitType ) );
    }
  }
}

Storing a Dictionary Mapping in Extensible Storage

In the sample so far, only a simple XYZ data field is added and stored.

Here is a new question on making use of extensible storage to store a more complex data type, a dictionary mapping keys to values, in this case both represented by strings.

Question: I have added simple data to Revit elements using Extensible Storage.
Now I need a schema containing a field which is a map of strings.
I added a field by using


FieldBuilder.AddMapField( MyMappedField,
typeof( string ), typeof( string ) );

Now comes my problem: how can I set a mapped value to such a field by using Entity.Set<???>(???)?

Similarly, how can I retrieve a mapped value from such a field?

How can I remove a mapped value?

Please can you implement such a sample?

Answer: The key, no pun intended, is to use IDictionary<> as the type you pass to the Set<> method, even though your actual implementation concrete type is Dictionary<>.

This is document in the Revit API help file entry on the Entity.Set method, but I admit that it is tricky.

By the way, this is also demonstrated by the ExtensibleStorageManager SDK sample.
For instance, it includes the following code snippet:


  // Note that we use IDictionary<> for 
  // map types and IList<> for array types
 
  mySchemaWrapper
    .AddField<IDictionary<string, string>>(
      map0Name, UnitType.UT_Undefined, null );
 
  mySchemaWrapper
    .AddField<IList<bool>>(
      array0Name, UnitType.UT_Undefined, null );

Here is a new method StoreStringMapInElement that I added to the sample above to demonstrate this:


/// <summary>
/// Create an extensible storage schema specifying 
/// a dictionary mapping keys to values, both using 
/// strings,  populate it with data, attach it to the 
/// given element, and retrieve the data back again.
/// </summary>
void StoreStringMapInElement( Element e )
{
  SchemaBuilder schemaBuilder = new SchemaBuilder(
    new Guid( "F1697E22-9338-4A5C-8317-5B6EE088ECB4" ) );
 
  // allow anyone to read or write the object
 
  schemaBuilder.SetReadAccessLevel(
    AccessLevel.Public );
 
  schemaBuilder.SetWriteAccessLevel(
    AccessLevel.Public );
 
  // create a field to store a string map
 
  FieldBuilder fieldBuilder
    = schemaBuilder.AddMapField( "StringMap",
      typeof( string ), typeof( string ) );
 
  fieldBuilder.SetDocumentation(
    "A string map for Tobias." );
 
  schemaBuilder.SetSchemaName(
    "TobiasStringMap" );
 
  // register the schema
 
  Schema schema = schemaBuilder.Finish();
 
  // create an entity (object) for this schema (class)
 
  Entity entity = new Entity( schema );
 
  // get the field from the schema
 
  Field field = schema.GetField(
    "StringMap" );
 
  // set the value for this entity
 
  IDictionary<string, string> stringMap
    = new Dictionary<string, string>();
 
  stringMap.Add( "key1", "value1" );
  stringMap.Add( "key2", "value2" );
 
  entity.Set<IDictionary<string, string>>(
    field, stringMap );
 
  // store the entity on the element
 
  e.SetEntity( entity );
 
  // read back the data from the wall
 
  Entity retrievedEntity = e.GetEntity( schema );
 
  IDictionary<string, string> stringMap2
    = retrievedEntity
    .Get<IDictionary<string, string>>(
      schema.GetField( "StringMap" ) );
}

Here is the full updated sample application
ExtensibleStorage.zip including the Visual Studio solution and full source.

Many thanks to our local Revit extensible storage expert Steven Mycynek for help resolving this issue!


Comments

10 responses to “Extensible Storage of a Map”

  1. Hi Jeremy,
    I noticed this in the SDK documentation about the addmapfield function: “Natural comparison of contained types is used for sorting.” Is there a way to store a map field without the keys being sorted?

  2. Dear Paul,
    Of course it is not possible to affect the way the fields are stored internally. There is some kind of dictionary being used internally, and it has to use some kind of key comparison mechanism.
    Anyway, I do not see how that could be of any earthly interest to you. Nobody is going to see the contents of this container except through your own programmatic interface.
    Why are you asking this? What are you trying to achieve?
    Cheers, Jeremy.

  3. I’m creating a macro to work on door hardware and the order of the door hardware list is specified by the user to match the door schedule. I’ll just have to create a list that stores the order the hardware should be in.

  4. Dear Paul,
    Oh, I see. Yes, I guess you will have to store the order yourself somehow.
    Cheers, Jeremy.

  5. Hi Jerermy, Can you add new fields once a schema has been create? I am trying to add a new field but get “A different schema with the same identity already exists” Is the only way to add new fields to delete the existing schema and create a new one?

  6. Dear Christopher,
    Sorry, the answer is no. And the other answer is yes.
    You need to create an entirely new schema. That is the only safe way to implement such a system.
    In your add-in, you can simply support both schemata, and implement functionality to upgrade from one to the other, either automatically or on demand, and even to downgrade if you so wish.
    This is demonstrated by one of the samples in my Autodesk University 2011 class CP4451 – Extensible Storage in the Revit 2012 API:
    UpgradeSchema – Handle versioning issues when upgrading and extending an existing extensible storage schema.
    Cheers, Jeremy.

  7. I thought that might be the answer. Could become a problem with version control, each time you think of something else that could be stored in there, either control the versions or multiple schemas, which would be the best solution? Would have been nice and simple if I could just modify the schema.
    Cheers Chris

  8. Dear Christopher,
    I do not see the problem at all, whereas I definitely do see a whole bunch of possible problems if you try to solve it in any other manner.
    Cheers, Jeremy.

  9. Any chance that ES will support IDictionary (string, IList(string)) in the future? Thanks.

  10. Dear Mike,
    I would not wait if I were you.
    You can easily store a list of strings by simply concatenating them, using any delimiter you like that does not occur in any of the strings. If absolutely all characters may occur in the strings, you can use it anyway and escape the original occurrences.
    Cheers, Jeremy.

Leave a Reply to Jeremy TammikCancel reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading