Implementing persistence


Summary
This topic reviews the technique of persistence of custom ArcObjects implemented by using .NET.


About implementing persistence

Persistence is a general term that refers to the process by which information indicating the current state of an object is written to a persistent storage medium, such as a file on disk.
Persistence is used in ArcGIS to save the current state of documents and templates. By interacting with the ArcGIS user interface (UI), you can change the properties of many of the objects that belong to a map document, for example, a renderer. When the map document is saved and closed, the instance of the renderer class is terminated. When the document is reopened, you can see that the state of the renderer object has been preserved.

Structured storage, compound files, documents, and streams

Map documents and their contents are saved using a technique known as structured storage. Structured storage is one implementation of persistence defined by a number of standard Component Object Model (COM) interfaces. Before structured storage, only a single file pointer was used to access a file. However, in structured storage, a compound file model is used, whereby each file contains storage objects and streams. Storage objects provide structure (for example, folders on your operating system) and can contain other storage and stream objects. Stream objects provide storage (for example, traditional files) and can contain any type of data in any internal structure. When the stream is later reopened, a new object can be initialized and its state set from the information in the stream, re-creating the state of the previous object.
In this way, a single compound file can act as a mini-file system; it can be accessed by many file pointers. Benefits of structured storage include incremental file read/write and a standardization of file structure, although larger file sizes can also result.
ArcGIS uses structured storage to persist the current state of all the objects used by an application, although other persistence techniques are also used. Structured storage is only used for non-geographic information system (GIS) data.

Persistence in ArcGIS

Structured storage interfaces specified by COM are implemented extensively throughout the ArcGIS framework. Understanding when persistence is used in the ArcGIS framework helps you to implement correct persistence behavior in classes you create. The following sections explain when to implement persistence and which interfaces to implement, and also reviews a number of issues that you can encounter when persisting objects.
Although persistence is used throughout the ArcGIS framework, it is not ubiquitous; not every object will always be given the opportunity to persist itself.

Compound document structure

ArcGIS applications use the compound document structure to store documents, such as map documents, globe documents, map and globe templates, normal templates, and so on. All the objects currently running in a document or template are persisted to streams in the compound file when the document is saved.
For example, in a map document, when a user clicks Save in ArcMap, the MxApplication creates streams as required, associates them with the existing .mxd file (if the document has previously been saved), then requests the document to persist itself to these streams. If there are changes to the normal template or map template, this process is repeated for the appropriate .mxt file. This process allows the current state of a document to be re-created when the file is reopened.
ArcMap, for example, persists many items. Notable areas that can include custom objects are described as follows:
  • Map collection—Each map persists its layers, symbology, graphics, current extent, spatial reference, and so on. This can include custom layers, renderers, symbols, elements, or other map items.
  • Page layout, map frames, map surrounds, layout of items, and so on—This can include custom map surrounds or frames.
  • Visible table of contents (TOC) views and their state—This can include a custom TOC view.
  • Toolbars currently visible, their members and position, if floating, including standard and custom toolbars and commands, and UIControls.
  • Registered extensions and their state—This can include custom extensions.
  • Current DataWindows, their type, location, and contents—This can include a custom DataWindow.
  • List of styles currently referenced by the StyleGallery; items are stored in a style by using persistence—This can include a custom StyleGalleryItem or StyleGalleryClass.
Starting in ArcGIS 9.1, you can save map documents so you can open and work with them in previous versions of ArcGIS. See the Version compatibility and Version compatibility consistency sections in this topic for more information on handling this kind of persistence in your custom components.
If any object referenced by the map document is expected to support persistence and does not, errors might be raised to a user and the completion of the save might be prevented, rendering the document unusable; therefore, you should always be clear whether your class needs to implement persistence and implement correct persistence behavior if required.

Persistent classes

When an object is requested to persist itself, it writes the current value of its member variables to the stream. If one of the members references another object and that object is also persistable, it is most likely to delegate the persistence work by requesting the member object to persist itself. This cascading effect ensures that all the referenced objects are given a chance to persist. This can include your custom objects if they are referenced by an object that is persisted. 
A persistence event cascades through the document as each object requests its members to persist themselves in turn.
See the following illustration:
As seen previously in document persistence, each class decides what defines its own state and persists only this data (in most cases, the values of its private member variables).
If for some reason you decide your custom class does not need to save any information about its state to the stream but is expected to support persistence, then you still must implement persistence, although you don't necessarily need to write any data to the stream.
For most custom classes you create, objects are persisted to one of the streams created by the framework—for example, ArcMap in the case of ArcGIS for Desktop and the MapDocument class in the case of ArcGIS Engine—it is unlikely you need to create a new storage or stream.

Loaded extensions

During the save process, the application checks all currently loaded extensions to verify if they implement persistence. If so, each extension is requested to persist itself. An extension, therefore, does not necessarily have to support persistence—no errors are raised if it does not—it depends on whether the extension needs to persist the state when a document is closed. Extensions are persisted in the order they are referenced, which is the order of their class identifiers (CLSIDs).
The application object creates a separate stream for the persistence of each extension and the new streams are stored in the same compound file as the other document streams. A separate ObjectStream is also created for the extension. See the following ObjectStreams section for more information about ObjectStreams.

ObjectStreams

An object's state is not always defined by value types—you have already seen how a map document persists itself by calling other objects to persist themselves.
Often, multiple references are held to the same object, for example, the same layer in a map might be referenced by IMap.Layer and ILegendItem.Layer. If each of these properties is called to persist, two separate copies of the layer are persisted in different sections of the stream. This bloats the file size and also corrupts object references. To avoid this problem, ObjectStreams are used in ArcObjects to persist objects and maintain object references correctly when persisted.
When an ArcObjects object initiates a persist, that object creates a stream for the persistence. It also creates an ObjectStream and associates it with the stream; one ObjectStream can be associated with one or more streams. The ObjectStream maintains a list of objects that have been persisted to that stream.
The first time an object is encountered, it is persisted in the usual manner. If the same object is encountered again, the ObjectStream ensures that instead of persisting the object a second time, a reference to the existing saved object is stored. See the following illustration:
   
In addition to ensuring the integrity of object references, this helps to keep file sizes to a minimum. Only COM objects supporting IUnknown and IPersist can be stored in this way.

Implementing a persistence class

To create a persistable class, implement either IPersist and IPersistStream, or IPersistVariant. IPersistStream and IPersistVariant specify the following three basic pieces of functionality:
Implementing IPersistStream in .NET is an advanced technique and is not discussed in this topic. Instead, this topic discusses the implementation of IPersistVariant. In any case, you do not need to implement both interfaces.
When a document is persisted, the client writes the identity of the class to the stream (using the ID). Then it calls the Save method to write the actual class data to the stream. When a document is loaded, the identity is read first, allowing an instance of the correct class to be created. At this point, the rest of the persisted data can be loaded into the new instance of the class.
If you want to implement version-specific persistence code, see Version compatibility for more information.
All code sections in this topic are from the Sample: Triangle graphic element.

What you need to save

When you implement a persistent class, the decision of what constitutes the persistent state for your class is yours to make. The exact data you choose to write to a stream is up to you.
Ensuring that your code can re-create the state of an instance can include storing data about public properties and any internal members of the class.
You might decide that certain items of state are not persisted. For example, a map does not persist the IMap.SelectedLayer property. When opening a map document, the SelectedLayer property is null. Decide how a newly instantiated instance of the class is initialized from the data stored in the stream.

Implementing IPersistVariant

The IPersistVariant interface is for use by non C++/VC++ programmers. See the following code example:
[C#]
public sealed class TriangleElementClass: � � � IPersistVariant � � �
[VB.NET]
Public NotInheritable Class TriangleElementClass
Implements …, IPersistVariant…
In the ID property, create a unique identifier (UID) and set the object to the fully qualified class name of your class, or alternatively, use your component's class identifier (CLASSID/globally unique identifier [GUID]). See the following code example:
[C#]
public UID ID
{
    get
    {
        UID uid = new UIDClass();
        uid.Value = "{" + TriangleElementClass.CLASSGUID + "}";
        return uid;
    }
}
[VB.NET]
Public ReadOnly Property ID() As UID Implements IPersistVariant.ID
Get
Dim uid As UID = New UIDClass()
uid.Value = "{" & TriangleElementClass.CLASSGUID & "}"
Return uid
End Get
End Property
A basic implementation of Save and Load is shown in the following code example:
[C#]
public void Save(IVariantStream Stream)
{
    Stream.Write(m_size);
    Stream.Write(m_elementType);
    Stream.Write(m_pointGeometry);
    ...
}

public void Load(IVariantStream Stream)
{
    m_size = (double)Stream.Read();

    m_elementType = (string)Stream.Read();
    m_pointGeometry = Stream.Read()as IPoint;
    ...
}
[VB.NET]
Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(m_size)
    Stream.Write(m_elementType)
    Stream.Write(m_pointGeometry)
    …
End Sub

Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    m_size = CDbl(Stream.Read())
    m_elementType = CStr(Stream.Read())
    m_pointGeometry = TryCast(Stream.Read(), IPoint)
    …
End Sub
  • In the previous code example, one double, one string, and one point are persisted (m_size, m_elementType, m_pointGeometry). 
Streams are sequential. The Load method must read the data from the stream in the same order the data was written to the stream in the Save method.
Ensure that your data is saved and loaded in the correct order so that the correct data is written to the correct member. Coding the Save and Load methods might be considerably more complex if you have a large complex class.
The stream passed to the IPersistVariant interface is a specialist stream class that implements IVariantStream. Using this interface, any value type or COM object can be written to a stream. This stream class is internal to ArcObjects.
The IVariantStream interface allows you to write COM objects and value data types to a stream using the same semantics.

Identifying the document version

If your object can be saved to a previous version of ArcGIS but you need to account for this in your persistence code by having different persistence code for different ArcGIS versions, adapt your implementation of IPersistVariant to identify the document version that your component is being persisted to.
Within a call to load or save, you can determine the version of the document by casting to the IDocumentVersion interface on the stream object as shown in the following code example:
[C#]
public void Save(IVariantStream Stream)
{
    if (Stream is IDocumentVersion)
    {
        IDocumentVersion docVersion = (IDocumentVersion)Stream 'if 
            (docVersion.DocumentVersion == esriArcGISVersion.esriArcGISVersion83)
        {
            //Save object as 8.3 version of itself.
        }
        else
        {
            //Save object.
        }
    }
}
[VB.NET]
Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    If TypeOf Stream Is IDocumentVersion Then
        Dim docVersion As IDocumentVersion
        docVersion = CType(Stream, IDocumentVersion)
        If docVersion.DocumentVersion = esriArcGISVersion.esriArcGISVersion83 Then
            'Load object as 8.3 version of itself.
        Else
            'Load object.
        End If
    End If
If your code is installed on machines with an installation of ArcGIS before version 9.1, you cannot guarantee that the stream passed to the persistence methods supports IDocumentVersion. As previously shown, always try to cast for this interface and take appropriate action if this interface is not found. You might want to provide your functions to discover the installed version of ArcGIS or you might want to rely on your internal persistence version number. For more information, see Coding backward compatibility in persistence.

Techniques for persisting different data

The following sections give advice on persisting certain types of data to a stream for implementers of IPersistVariant.

Persisting objects

If you are using IPersistVariant, coding the persistence of an object is syntactically the same as coding the persistence of a value type. When you pass an object reference like this, the stream uses the ObjectStream associated internally with the stream to persist the object. See the following code example:
[C#]
Stream.Write(m_pointGeometry);
[VB.NET]
Stream.Write(m_pointGeometry)
The object is reloaded in a similar way. See the following code example:
[C#]
m_pointGeometry = Stream.Read()as IPoint;
[VB.NET]
m_pointGeometry = TryCast(Stream.Read(), IPoint)

Persisting arrays

Often, a class member might be a dynamic array having a variable number of members. In this case, write the value of the member directly to a stream in its entirety, as it is not a COM object.
You can write each array member in turn to the stream as long as you include extra information about the size of the array, since the Load method needs to size the array, and read the correct number of members from the stream to assign to the array.
The following code example shows how this technique can be used, where m_array is a member of the class:
[C#]
int count = m_array.Count;
Stream.Write(count);
for (int i = 0; i < count; i++)
{
    Stream.Write(m_array[i]);
}
[VB.NET]
Dim Count As Integer = m_array.Count
Stream.Write(Count)
Dim i As Integer
For i = 0 To Count - 1
    Stream.Write(m_array(i))
Next
The array can now be initialized correctly in the Load method. See the following code example:
[C#]
int count = (int)Stream.Read();
for (int i = 0; i < count; i++)
{
    m_array.Add(Stream.Read());
}
[VB.NET]
Dim Count As Integer = CInt(Stream.Read())
Dim i As Integer
For i = 0 To Count - 1
    m_array.Add(Stream.Read())
Next
Instead of using a standard dynamic array, you can store object references in an Esri Array class and persist each of these references in the same way (the Array class is not persistable).

Persisting a PropertySet

You can use the PropertySet class to persist a class's member data, as this class is persistable. Maximum efficiency is gained during a save if you already use the PropertySet to internally store your class data.

Document versions and ObjectStreams

The Version compatibility and Coding save a copy functionality sections describe how to deal with the persistence of your object at different versions of ArcGIS. If during your component's persistence code you persist object references, consider that those objects also need to deal with the document version correctly.
All core ArcObjects components deal correctly with document version persistence—they do not implement the IDocumentVersionSupportGEN interface but instead, deal with this issue internally. If you are persisting an object to an ObjectStream, all core ArcObjects components can therefore be relied on to either persist correctly regardless of version or convert themselves to suitable replacement objects using methods similar to the IDocumentVersionSupportGEN.ConvertToSupportedObject method.

Error handling when loading

If you encounter an error when you attempt to read a stream, you must propagate the error to the client. Because streams are sequential, your code should not attempt to continue reading, as the stream pointer is not positioned correctly, and therefore, the next value cannot be read correctly.
For this reason, you should always be particularly careful when writing and testing persistence code. Review the following Version compatibility section. You can avoid many errors in your persistence code if you correctly create backward-compatible components.

Safe loading

In some cases, ArcGIS might be able to continue loading a document despite an error in your code, through the use of safe loading techniques.
The effects of the error might vary according to the type of component. For example, if ArcGIS attempts to load a layer from a document and fails, ArcMap continues to load the remainder of the document but without the failed layer. Code your component regardless of this functionality, and raise an error to the calling function if you cannot complete the load before exiting the Load function.

Unregistered classes

You are responsible for ensuring that your component is registered on a machine that can open a document with a persisted version of your component.

Version compatibility

If you develop a new version of a persistable component, it is likely that you need to persist additional state information; this means you need to change the persistence signature of your class. However, your component can still maintain binary compatibility and have the same ClassID.
By coding your persistence methods to be adaptable from the beginning of your development cycle, you can ensure your component is compatible with other versions of itself when persisted. This allows you to fully utilize the ability when using COM to upgrade a component without needing to recompile the component's clients.

Compatibility in ArcGIS

Code custom components with the following version compatibility model of ArcGIS in mind:
  • Backward compatibility—ArcGIS document files work on the principle of backward compatibility, probably the most common form of persistence version compatibility. This means that ArcGIS clients can open documents created with an earlier version of ArcGIS.
  • Forward compatibility—It is possible to write forward-compatible components, for example, a client can load and save a component with a more recent version than that with which it was originally compiled. Implementing forward compatibility requires much care and can give rise to long, complex persistence code.
Although ArcGIS does not implement general forward compatibility (this is not generally a requirement for your components), from ArcGIS 9.1 onward it is possible for users to save their documents as specific previous ArcGIS versions using the Save a Copy command. The saved documents can then be opened with a version of ArcGIS before that with which the document was created. At ArcGIS 9.2, you can save to ArcGIS 8.3, or ArcGIS 9 and 9.1. ArcGIS 9.1 map documents are directly compatible with ArcGIS 9; therefore, there is no option to save them to version 9 specifically. See the following illustration:
If your component works without recompilation with both the current and previous ArcGIS versions, you do not need to adapt your component to ensure Save a Copy functionality.
However, if your object cannot be persisted to a previous version of ArcGIS, implement IDocumentVersionSupportGEN. This interface allows you to provide an alternative object. For more information, see Coding save a copy functionality.
If your object can be saved to a previous version of ArcGIS but you might need to account for this in your persistence code, adapt your implementation of IPersistVariant to identify the version being persisted to and make any necessary changes. For more information, see Identifying the document version.

Coding backward compatibility in persistence

You will now look at an example of creating a backward compatible class by creating two different versions of the class. The following code example is built step by step, showing how to code the persistence methods each time. You will create a custom graphic element that draws an equilateral triangle element on the map. For more information, see Sample: Triangle graphic element.

Version 1

For the first version of your triangle element, store the member variables in the following code example:
[C#]
private double m_size = 20.0;
private double m_scaleRef = 0.0;
private esriAnchorPointEnum m_anchorPointType = esriAnchorPointEnum.esriCenterPoint;
private bool m_autoTrans = true;
private string m_elementType = "TriangleElement";
private string m_elementName = string.Empty;
private ISpatialReference m_nativeSR = null;
private ISimpleFillSymbol m_fillSymbol = null;
private IPoint m_pointGeometry = null;
private IPolygon m_triangle = null;
[VB.NET]
Private m_size As Double = 20.0
Private m_scaleRef As Double = 0.0
Private m_anchorPointType As esriAnchorPointEnum = esriAnchorPointEnum.esriCenterPoint
Private m_autoTrans As Boolean = True
Private m_elementType As String = "TriangleElement"
Private m_elementName As String = String.Empty
Private m_nativeSR As ISpatialReference = Nothing
Private m_fillSymbol As ISimpleFillSymbol = Nothing
Private m_pointGeometry As IPoint = Nothing
Private m_triangle As IPolygon = Nothing
  • Implement IPersistVariant, as a graphic element, this must be persistable. Before coding the persistence members of the TriangleElement class, add the c_Version private constant. Use this constant throughout the persistence code to store the version of the class. Set the constant to 1, as this is the first version of the class. See the following code example:
[C#]
private const int c_Version = 1;
[VB.NET]
Private Const c_Version As Integer = 1
  • The use of this value is the key to version compatibility. Code the Save and Load methods dependent on this number. The first thing written to the stream in the Save method is this persistence version value. See the following code example:
[C#]
public void Save(IVariantStream Stream)
{
    Stream.Write(c_Version);
[VB.NET]
Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(c_Version)
  • Write the class state to the stream. See the following code example:
[C#]
Stream.Write(m_size);
Stream.Write(m_scaleRef);
Stream.Write(m_anchorPointType);
Stream.Write(m_autoTrans);
Stream.Write(m_elementType);
Stream.Write(m_elementName);
Stream.Write(m_nativeSR);
Stream.Write(m_fillSymbol);
Stream.Write(m_pointGeometry);
Stream.Write(m_triangle);
}
[VB.NET]
Stream.Write(m_size)
Stream.Write(m_scaleRef)
Stream.Write(m_anchorPointType)
Stream.Write(m_autoTrans)
Stream.Write(m_elementType)
Stream.Write(m_elementName)
Stream.Write(m_nativeSR)
Stream.Write(m_fillSymbol)
Stream.Write(m_pointGeometry)
Stream.Write(m_triangle)
End Sub
  • In the Load method, read the version number of the persisted class and store this value in the ver local variable. If this version number indicates a version of the persisted class that is newer than the current version of the class (c_Version), the class does not know how to load the persisted information correctly. If ver = 0, there is an error somewhere, as the minimum expected value is 1. Both cases are errors and might cause a corrupt stream. In these cases, raise an exception back to the calling object. See the following code example:
[C#]
public void Load(IVariantStream Stream)
{
    int ver = (int)Stream.Read();
    if (ver > c_Version || ver <= 0)
        throw new Exception("Wrong version!");
[VB.NET]
Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    Dim ver As Integer = CInt(Fix(Stream.Read()))
    If ver > c_Version OrElse ver <= 0 Then
        Throw New Exception("Wrong version!")
    End If
  • As Load might be called sometime after an object has been instantiated, ensure initialize default values for the class at the start of the Load. See the following code example:
[C#]
InitMembers();
[VB.NET]
InitMembers()
  • Read the persisted class state and set the members of the current object after checking that the saved persistence version is the version you expect (this verification is useful later when you produce a new version of your component). See the following code example:
[C#]
if (ver == 1)
{
    m_size = (double)Stream.Read();
    m_scaleRef = (double)Stream.Read();
    m_anchorPointType = (esriAnchorPointEnum)Stream.Read();
    m_autoTrans = (bool)Stream.Read();
    m_elementType = (string)Stream.Read();
    m_elementName = (string)Stream.Read();
    m_nativeSR = Stream.Read()as ISpatialReference;
    m_fillSymbol = Stream.Read()as ISimpleFillSymbol;
    m_pointGeometry = Stream.Read()as IPoint;
    m_triangle = Stream.Read()as IPolygon;
}
[VB.NET]
If ver = 1 Then
    m_size = CDbl(Stream.Read())
    m_scaleRef = CDbl(Stream.Read())
    m_anchorPointType = CType(Stream.Read(), esriAnchorPointEnum)
    m_autoTrans = CBool(Stream.Read())
    m_elementType = CStr(Stream.Read())
    m_elementName = CStr(Stream.Read())
    m_nativeSR = TryCast(Stream.Read(), ISpatialReference)
    m_fillSymbol = TryCast(Stream.Read(), ISimpleFillSymbol)
    m_pointGeometry = TryCast(Stream.Read(), IPoint)
    m_triangle = TryCast(Stream.Read(), IPolygon)
End If
  • Now that you have the first version of your class, you can compile and deploy the component. At this point, users might have map documents that contain persisted TriangleElement graphic elements.

    You can use the TriangleElementTool included in the project to add new triangle elements to a document.

Version 2

You are now asked to add functionality to allow rotation of the triangle elements. To achieve these requirements, adapt your component. Add a class member to keep the element's rotation and apply the rotation to the element when building the triangle, as well as in the implementation of the ITransform2D interface. See the following code example:
[C#]
private double m_rotation = 0.0;
[VB.NET]
Private m_rotation As Double = 0.0
  • As the data that needs to be persisted has now changed, increment the persist version number for the class by one. See the following code example:
[C#]
private const int c_Version = 2;
[VB.NET]
Private Const c_Version As Integer = 2
When you change the persistence signature of a component, the new component should still read the original data from the first persistence version if the old version is encountered in the Load method. See the following code example:
[C#]
public void Save(IVariantStream Stream)
{
    Stream.Write(c_Version);
    � � � 
    //New addition in version 2.
    Stream.Write(m_rotation);
}
[VB.NET]
Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(c_Version)
    …
    'New addition in version 2.
    Stream.Write(m_rotation)
End Sub
  • Adapt the Load method to always read from the stream the data saved by both the new and old versions of the component. 
Method InitMembers() sets a default rotation value in cases where the object has been read as version 1.
See the following code example:
[C#]
public void Load(IVariantStream Stream)
{
    int ver = (int)Stream.Read();
    if (ver > c_Version || ver <= 0)
        throw new Exception("Wrong version!");

    InitMembers();
    � � � 

    if (ver == 2)
    {
        m_rotation = (double)Stream.Read();
    }
}
[VB.NET]
Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    Dim ver As Integer = CInt(Fix(Stream.Read()))
    If ver > c_Version OrElse ver <= 0 Then
        Throw New Exception("Wrong version!")
    End If
    
    InitMembers()
    …
    
    If ver = 2 Then
        m_rotation = CDbl(Stream.Read())
    End If
End Sub
If you have loaded the first version of the persistence pattern, set the second version member variables to default values. If you have loaded the second version of the persistence pattern, read the additional members.
  • Compile and deploy version 2 of the TriangleElement class. At this point, if the new version of the component encounters an older persisted version, it can load from the persisted data.

    When the document is saved again (by the version 2 component), the persisted version will be version 2.

Obsolete persisted data

In many cases, a new component version requires adding data to a persistence pattern. Sometimes a new version of a component no longer needs to save certain data to the stream.
In these cases, if your new component encounters the older version persisted to a stream, read the obsolete data values; otherwise, the stream pointer is left at the wrong location and the next value is not read correctly. You can discard the obsolete values once read, and save only the required data in the new Save method. See the following illustration:
 
Another possibility is to create a class that does not update to the new persistence pattern if saved by a new version of the component. This enables old components to load the persisted object.
The persistence version number written at the beginning of the Save method should account for the persistence pattern used.
To make the implementation of persistence versions more straightforward, you might want to consider the use of a PropertySet. Each version of your component can add more or different properties as required, then the Save and Load events only need to persist the current PropertySet. If you choose this approach, make sure that all your class members are set to their default values at the beginning of a Load event in case the values of certain class members cannot be found in the current PropertySet.

Coding save a copy functionality

To allow your component to persist to a document at a particular version of ArcGIS, for example, when a user chooses the Save a Copy command in ArcMap, you might want to implement IDocumentVersionSupportGEN. This interface specifies the following pieces of functionality:
  • Allows a component to indicate whether it can be persisted to a particular version of an ArcGIS document
  • Allows a component to provide a suitable alternative object instead of itself if that component cannot be persisted to the specified version
If ArcGIS cannot cast to IDocumentVersionSupportGEN for a given persistable object, it assumes that the object can be persisted and unchanged to any version of an ArcGIS document.
For example, in a situation where a custom symbol is applied to a layer in a document at ArcGIS 9.2 and the user chooses to save a copy of the document to an ArcGIS 8.3 document, the persistence process for the symbol follows these general steps to create the version-specific document:
  1. When ArcMap attempts to persist the symbol object, it attempts to cast to IDocumentVersionSupportGEN to determine if the symbol can be saved to an 8.3 document.
  2. If the cast fails, ArcMap calls the persistence methods of the symbol as normal, assuming the symbol can be persisted to any version of ArcGIS.
  3. If the cast succeeds, ArcMap then calls the IDocumentVersionSupportGEN.IsSupportedAtVersion method, passing in a value indicating the ArcGIS version required.
    • If IsSupportedAtVersion returns true, ArcGIS calls the persistence methods of the symbol as normal.
    • If IsSupportedAtVersion returns false, ArcGIS calls the IDocumentVersionSupportGEN.ConvertToSupportedObject method on the symbol. The symbol then creates a suitable alternative symbol object that can be persisted to the ArcGIS document version specified. This alternative symbol object is then returned from ConvertToSupportedObject, and ArcGIS replaces object references to the original symbol with references to this alternative symbol. The following illustration shows this last situation:

Implementing IDocumentVersionSupportGEN

The IsSupportedAtVersion method is where you determine to which ArcGIS document versions your component can be persisted. Return true or false from this method depending on the document version indicated by the parameter passed in to this method. The parameter is an esriArcGISVersion enumeration value.
If, for example, your component can be used equally well at all versions of ArcGIS, you can return true from IsSupportedAtVersion, although in this case, you do not need to implement the interface at all.
If, however, your component relies on the presence of core objects or functionality that exist only from a certain version onward, return false to indicate that the object cannot be used in a previous version's document (for example, the triangle element example previously explained). You can prevent the element from being saved as it is to an 8.3 document using code like that shown in the following code example:
[C#]
public bool IsSupportedAtVersion(esriArcGISVersion docVersion)
{
    //Support all versions except 8.3.
    if (esriArcGISVersion.esriArcGISVersion83 == docVersion)
        return false;
    else
        return true;
}
[VB.NET]
Public Function IsSupportedAtVersion(ByVal docVersion As esriArcGISVersion) As Boolean Implements IDocumentVersionSupportGEN.IsSupportedAtVersion
    'Support all versions except 8.3.
    If esriArcGISVersion.esriArcGISVersion83 = docVersion Then
        Return False
    Else
        Return True
    End If
End Function
To allow users to save a copy of a document with the custom triangle element as an 8.3 document, you can use code like that shown in the following example to create a basic marker element that uses a triangular character marker symbol as an alternative to the custom triangle element and return this from ConvertToSupportedObject. (This might not be an appropriate solution for every custom graphics element, but the principle can be applied to similar code to create an object appropriate to your own customizations).
[C#]
public object ConvertToSupportedObject(esriArcGISVersion docVersion)
{
    //In the case of 8.3, create a character marker element and use a triangle marker.
    ICharacterMarkerSymbol charMarkerSymbol = new CharacterMarkerSymbolClass();
    charMarkerSymbol.Color = m_fillSymbol.Color;
    charMarkerSymbol.Angle = m_rotation;
    charMarkerSymbol.Size = m_size;
    charMarkerSymbol.Font = ESRI.ArcGIS.ADF.Converter.ToStdFont(new Font(
        "ESRI Default Marker", (float)m_size, FontStyle.Regular));
    charMarkerSymbol.CharacterIndex = 184;

    IMarkerElement markerElement = new MarkerElementClass();
    markerElement.Symbol = (IMarkerSymbol)charMarkerSymbol;

    IPoint point = ((IClone)m_pointGeometry).Clone()as IPoint;
    IElement element = (IElement)markerElement;
    element.Geometry = (IGeometry)point;

    return element;
}
[VB.NET]
Public Function ConvertToSupportedObject(ByVal docVersion As esriArcGISVersion) As Object Implements IDocumentVersionSupportGEN.ConvertToSupportedObject
    'In the case of 8.3, create a character marker element and use a triangle marker.
    Dim charMarkerSymbol As ICharacterMarkerSymbol = New CharacterMarkerSymbolClass()
    charMarkerSymbol.Color = m_fillSymbol.Color
    charMarkerSymbol.Angle = m_rotation
    charMarkerSymbol.Size = m_size
    charMarkerSymbol.Font = ESRI.ArcGIS.ADF.Converter.ToStdFont(New Font("ESRI Default Marker", CSng(m_size), FontStyle.Regular))
    charMarkerSymbol.CharacterIndex = 184
    
    Dim markerElement As IMarkerElement = New MarkerElementClass()
    markerElement.Symbol = CType(charMarkerSymbol, IMarkerSymbol)
    
    Dim point As IPoint = TryCast((CType(m_pointGeometry, IClone)).Clone(), IPoint)
    Dim element As IElement = CType(markerElement, IElement)
    element.Geometry = CType(point, IGeometry)
    
    Return element
End Function
For every esriArcGISVersion for which you return false from IsSupportedAtVersion, implement a suitable alternative in ConvertToSupportedObject. If you do not do this, you prevent users from saving a copy of a document to that version, and the user might not receive any information about why their attempt to save a copy failed. Do not return a null reference either, as ArcGIS might attempt to apply this null reference to a property, which can cause the saved document to become corrupted.

Responsibilities when implementing persistence

Review the following for a recap of your responsibilities when creating persistable components.

Controlling the data to persist

Remember that you control what is written to the stream. If your class needs to persist a reference to another custom object, you have the following two options:
  • Implement persistence on the other custom class and persist that class as you would any other object.
  • Alternatively, write the members of the secondary class to your persist stream for the primary class. Although this option might be simpler, use the first method, as it is more maintainable and scalable.

Error handling and file integrity

If you raise errors in a stream Load event, this can cause the current structured storage, for example, the current .mxd file, to be unreadable. Be careful when writing persistence code to ensure you preserve the integrity of storage files.

ObjectStreams in extensions

As previously mentioned, a separate ObjectStream is created for each extension. This situation can lead to problems for components that do not account for this difference.
If you persist an object reference that is already persisted elsewhere in the document and, therefore, to a separate ObjectStream, this results in two separate objects being persisted. When the document is reloaded, the object you persisted is no longer the same object that was persisted in the main document ObjectStream. You might want to initialize such objects in the extension startup rather than in persistence code.

Version compatibility consistency

Ensure that your components are consistent with the version compatibility used by ArcGIS. Ensure as a minimum that your components are backward compatible—that a client (for example, ArcMap) can open a document created with an earlier version of your component.
Consider implementing IDocumentVersionSupportGEN, if required, to ensure that your component can be opened in a document that is saved as a previous version of ArcGIS. For each ArcGIS version that you return false from IsSupportedAtVersion, ensure you return a valid alternative object from ConvertToSupportedObject.


See Also:

Sample: Triangle graphic element




Development licensing Deployment licensing
Engine Developer Kit Engine
ArcGIS for Desktop Basic ArcGIS for Desktop Basic
ArcGIS for Desktop Standard ArcGIS for Desktop Standard
ArcGIS for Desktop Advanced ArcGIS for Desktop Advanced