Extending ArcObjects  

Implementing Persistence

This topic is relevant for the following:
Product(s): ArcGIS Desktop: All
Version(s): 9.0, 9.1, 9.2
Language(s): VB6, VC++
Experience level(s): Intermediate to advanced



In this section:

  1. About Persistence
  2. Implementing Persistence
  3. Techniques for persisting different data
  4. Error handling when loading
  5. Version Compatibility
  6. Coding backward compatibility in persistence
  7. Coding 'Save A Copy' functionality
  8. Your responsibilities when implementing persistence

About Persistence

Persistence is a general term, referring 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, 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 COM interfaces. Prior to structured storage, only a single file pointer was used to access a file. In structured storage however, a compound file model is used, whereby each file contains storage objects and streams. Storage objects provide structure—like folders on your operating system, they can contain other storage and stream objects. Stream objects provide storage—like traditional files, they 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 may 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-GIS data.

Persistence in ArcGIS

The structured storage interfaces specified by COM are implemented extensively throughout the ArcGIS framework. Understanding when persistence is used within the ArcGIS framework will help you to implement correct persistence behavior in classes you create. The following sections explain when to implement persistence, which interfaces to implement, and also review a number of issues that you may 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.

Documents

ArcGIS applications use the compound document structure to store documents—map documents, map templates, normal templates, and scene documents. All the objects currently running within a document or template are persisted to streams in the compound file when the document is saved.

Take the example of a map document—when a user chooses Save in ArcMap, the MxApplication first creates streams as required, associates them with the existing .mxd file (if the document has previously been saved), then asks the document to persist itself to these streams. If there are changes to the normal template or map template, then this process is repeated for the appropriate .mxt file. This process allows the current state of a document to be recreated when the file is reopened.

From ArcGIS 9.1, you can save map documents so you can open and work with them in previous versions of ArcGIS. See the later sections of this topic on version compatibility 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 may be raised to a user and the completion of the save may be prevented, rendering the document unusable.

You should, therefore, always be clear whether or not your class needs to implement persistence, and implement correct persistence behavior if required.

Persistable classes

When an object is asked to persist itself, it will write 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 will most likely delegate the persistence work by asking the member object to persist itself. This 'cascading' effect ensures that all the referenced objects are given a chance to persist—this may include your own custom objects, if they are referenced by an object that is persisted.

A persistence event 'cascades' through the document, as each object asks its members to persist themselves in turn.

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 will create, objects will be persisted to one of the streams created by ArcMap; it is unlikely you will need to create a new storage or stream yourself.

Extensions

During the Save process, the application checks all currently loaded extensions to see if they implement persistence. If so, each extension is asked to persist itself. An extension, therefore, does not necessarily have to support persistence—no errors will be raised if it does not—it depends on whether or not 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 CLSIDs.

The Application object creates a separate stream for the persistence of each extension; 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 below for more information about ObjectStreams.

ObjectStreams

An object's state is not always defined by value types—you have already seen how an MxDocument 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 may be referenced by IMap::Layer and ILegendItem::Layer. If each of these properties were called to persist, two separate copies of the Layer would be persisted in different sections of the stream. This would bloat file size and would also corrupt 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 will create a stream for the persistence. It will also create an ObjectStream, and associate 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 a particular object is encountered, it is persisted in the usual manner. If the same object is encountered again, the ObjectStream will ensure that instead of persisting the object a second time, a reference to the existing saved object is stored instead.

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 Persistence

To create a persistable class, you should implement either IPersist and IPersistStream or IPersistVariant. Both interfaces specify three basic pieces of functionality.

The choice of IPersistStream or IPersistVariant depends on the development environment you are using—these interfaces are discussed in turn below. 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 ID or GetClassID). 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 coclass to be created. At this point, the rest of the persisted data can be loaded into the new instance of the class.

If you wish to implement version-specific persistence code, see the Version Compatibility section later in this topic for more information.

What needs to be saved?

When you implement a persistent class, the decision of what constitutes the persistent state for your class is yours to make—exactly what data you choose to write to a stream is up to you.

Ensuring that your code can re-create the state of an instance may include storing data about public properties and any internal members of the class.

You may decide that certain items of state are not persisted. For example, a Map does not persist the IMap::SelectedLayer property; upon opening a map document, the SelectedLayer property is null. You should also decide exactly how a newly instantiated instance of the class is initialized from the data stored in the stream.

Implementing IPersistVariant

The IPersistVariant interface should be implemented by Visual Basic classes that need to be persistable. This interface was specifically designed for use by VB programmers.

[Visual Basic 6]
Implements esriSystem.IPersistVariant

In the ID property, create a UID and set the object to the fully qualified class name of your class.

[Visual Basic 6]
Private Property Get IPersistVariant_ID() As esriSystem.IUID
  Dim pUID As New esriSystem.UID
  pUID.Value = "<LibraryName>.<ClassName>" ' or a GUID
  Set IPersistVariant_ID = pUID
End Property

A basic implementation of Save and Load is shown below.

[Visual Basic 6]
Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  Stream.Write m_sValue1
  Stream.Write m_sValue2
End Sub
Private Sub IPersistVariant_Load(ByVal Stream As esriSystem.IVariantStream)
  m_sValue1 = Stream.Read
  m_sValue2 = Stream.Read
End Sub

In the example, the two string member variables, m_sValue1 and m_sValue2, are persisted. Note that 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 may be considerably more complex if you have a large, complex class.

The stream passed to the IPersistVariant interface is a specialist stream class which implements IVariantStream. Using this interface, any value type, or any 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.

Implementing IPersist and IPersistStream

If you are developing in VC++, you may already be familiar with the IPersistStream interface, an interface defined as part of COM which inherits the IPersist base interface. You would generally implement this interface in preference to IPersistVariant, as it is more flexible, and the VC++ environment offers better support for this interface.

IPersistStream inherits from IPersist, and therefore, cannot be implemented in VB. Neither is it suitable for use by VB clients, as it also uses data types not supported by VB.

IPersist and IPersistStream are standard COM persistence interfaces and can be implemented in VC++.

A simple implementation of Save and Load, shown below, persists a string (m_bstrValue) and a long (m_lNum). The code makes use of the WriteToStream and ReadFromStream methods that are available on the CComBSTR smart type. These methods are also available on CComVariant.

[Visual C++]
STDMETHODIMP CPersistClass::Save(IStream * pStm, BOOL fClearDirty)
{
  if (m_bDirty)
  {
    m_bstrValue.WriteToStream(pStm);
    pStm->Write(&m_longValue, sizeof(m_longValue), 0);
  }
  // reset dirty flag
  m_bDirty = false;
  return S_OK;
}
[Visual C++]
STDMETHODIMP CPersistClass::Load(IStream * pStm)
{
  m_bstrValue1.m_str = NULL;
  m_bstrValue1.ReadFromStream(pStm);
  pStm->Read(&m_longValue, sizeof(m_longValue), 0);
  
  m_bLoadedSettings = true;
  return S_OK;
}

The Save method only writes to the stream if the parameter m_bDirty indicates that the object has changed since the last save.

The IPersistStream interface includes the IsDirty method, indicating whether or not an object has changed since it was last saved. The private member, m_bDirty, is used by this class to indicate the state of the object and is set to True when any change is made to the class and reset as False at the end of the Save method.

The GetSizeMax method should return the maximum size of the persisted data. In many cases it is not possible to calculate this in advance, for example, when persisting a variable size array or collection. In this case, return E_NOTIMPL.

[Visual C++]
STDMETHODIMP MyClass::GetSizeMax(_ULARGE_INTEGER * pcbSize)
{
  if (pcbSize == NULL)
    return E_POINTER;
  
  return E_NOTIMPL;
}

IPersistStreamInit

The IPersistStreamInit interface is an alternative to IPersistStream. It provides one extra method, InitNew, which clients may call to initialize the object to default values. This interface is more relevant to persistence of ActiveX controls where there is a large range of possible clients—you will not need to implement this interface for a custom class for ArcGIS.

IPersistStorage

The IPersistStorage interface is implemented by objects that persist themselves directly to a structured storage container, rather than a stream. MXD documents are implemented with this kind of persistence. This method is essential for objects which are to be embedded in OLE containers such as Microsoft Word. You will not need to implement this interface to create a persistable custom ArcGIS class.

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, then you should adapt your implementation of IPersistVariant or IPersist/IPersistStream to identify the document version that your component is being persisted to.

Within a call to load or save, you can find out the version of the document by QIing to the IDocumentVersion interface on the stream object as shown below; this applies to both the variant stream reference passed to a VB6 component implementing IPersistVariant, and also to the object stream reference passed to a VC++ component implementing IPersist/IPersistStream.

[Visual Basic 6]
Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  If TypeOf Stream Is IDocumentVersion Then
    Dim pDocVersion As IDocumentVersion
    Set pDocVersion = Stream
    If pDocVersion.DocumentVersion = esriArcGISVersion83 Then
      ' Load object as 8.3 version of itself.
    Else
      ' Load object.
    End If
  Else
    ' Installed client must be 9.0 or previous version.
    ...
[Visual C++]
STDMETHODIMP CPersistClass::Load(IStream * pStm)
{
  IDocumentVersionPtr ipDocumentVersion = pStm;
  if (ipDocumentVersion!=0)
  {
    enum esriArcGISVersion docVer;
    ipDocumentVersion->get_DocumentVersion(&docVer);
    if (docVer == esriArcGISVersion83)
    {
      // Load object as 8.3 version of itself
    }
    else
    {
      // Load object.
    }
  else
  {
    // Installed client must be 9.0 or previous version.

If your code may be installed to machines with an installation of ArcGIS previous to 9.1, then you cannot guarantee that the stream passed to the persistence methods will support IDocumentVersion. As shown above, you should always QI for this interface and take appropriate action if this interface is not found. You may wish to provide your own functions to discover the installed version of ArcGIS, or may wish to rely on your own internal persistence version number - see the Coding Backward Compatibility section below for more details.


Techniques for persisting different data

The following sections give advice on persisting certain types of data to a stream for implementors of both IPersistVariant and IPersistStream.

Persisting objects

If you are using IPersistVariant and working in VB, 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.

[Visual Basic 6]
Stream.Write m_pMyColorObject

The object is reloaded in a similar way.

[Visual Basic 6]
Dim pColor as IColorSet pColor = Stream.Read

If using the IPersistStream interface in Visual C++, a few more lines of code are required. First, you should check if an ObjectStream is associated with the stream by performing a QI for IObjectStream. If the QI succeeds, you can call SaveObject on the IObjectStream pointer, passing the object you wish to persist directly to the ObjectStream.

[Visual C++]
IObjectStreamPtr ipObjectStream(pStream);
if (ipObjectStream !=0)
{
  HRESULT hr;
  hr = ipObjectStream->SaveObject(m_ipColor);
  if (FAILED(hr)) return hr;
}

Always check the return value, as if the save fails, this can produce a corrupt stream. If you are working within the ArcGIS framework, an ObjectStream will already be associated with the stream you receive. Again, be sure to check the return value when you reload the object.

[Visual C++]
IObjectStreamPtr ipObjectStream(CLSID_ObjectStream);
ipObjectStream->putref_Stream(pStm);
hr = ipObjectStream->LoadObject((GUID*) &IID_IColor, 0, &pUnk);
if (FAILED(hr))
  return hr;

Persisting arrays

Often, a class member may 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 be able to size the array and read the correct number of members from the stream to assign to the array.

The example code below demonstrates how this technique can be used in VB, where the variable m_pArrayMember is a member of the class and also a dynamic array.

[Visual Basic 6]
Dim i As Long, lCount As Long
lCount = UBound(m_pArrayMember) + 1
Stream.Write lCount
 
For i = 0 To lCount - 1
  Stream.Write m_pArrayMember(i)
Next i

The array can now be initialized correctly in the Load method.

[Visual Basic 6]
Dim i as Long, lCount as Long
lCount = Stream.Read
ReDim m_pArrayMember(lCount -1)
 
For i = 0 to lCount - 1
  m_pArrayMember(i) = Stream.Read
Next i

Instead of using a standard dynamic array, you could store object references in an ESRI Array class and persist each of these references in the same way (the Array class is not persistable itself).

Persisting a PropertySet

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

Some of the persistable examples in the ArcGIS Developer Help show examples of this technique.

Document Versions and Object Streams

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, you should also consider that those objects too need to deal with the document version correctly.

All core ArcObjects 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 object stream, all core ArcObjects therefore can be relied upon to either persist correctly regardless of version, or to 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. As streams are sequential, your code should not attempt to continue reading, as the stream pointer will not be 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.

Version compatibility

Review the following section on persistence version compatibility—you can avoid many errors in your persistence code if you correctly create backward-compatible components.

Safe loading

In some cases, ArcGIS may be able to continue loading a document despite an error in your code, due to the use of safe loading techniques.

The effects of the error may vary according to the type of component. For example, if ArcGIS attempts to load a Layer from a document and fails, ArcMap will continue to load the remainder of the document, but the failed layer will be missing. You should 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, which may open a document with a persisted version of your component.


Version Compatibility

If you develop a new version of a persistable component, it is quite likely that you will need to persist additional state information—this will mean you need to change the persistence signature of your class. However, your component may 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 will allow you to fully utilize the ability when using COM to upgrade a component without needing to recompile the component's clients.

Compatibility in ArcGIS

Custom components should be coded with the version compatibility model of ArcGIS in mind.

Although ArcGIS does not implement general forwards compatibility (and therefore this is not generally a requirement for your components), from ArcGIS 9.1 onwards 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 previous to that with which the document was created. At ArcGIS 9.1, you can only save to ArcGIS 8.3. ArcGIS 9.1 map documents are directly compatible with ArcGIS 9.0, so there is no option to save them to version 9.0 specifically.

If your component works, without recompilation, with both the current ArcGIS version and also to previous ArcGIS versions, then 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, you should implement IDocumentSupportVersionGEN—this interface will allow you to provide an alternative object instead. See the Coding Save A Copy Functionality section below.

If your object can be saved to a previous version of ArcGIS, but you may need to account for this in your persistence code, then you should adapt your implementation of IPersistVariant or IPersist/IPersistStream to identify the version being persisted to, and make any necessary changes. You can find out more information on this in the 'Identifying the document version' section under Implementing Persistence.


Coding backward compatibility in persistence

You will now look at an example of creating a backwardly compatible class, by creating three different versions of the class. The example code is built up step-by-step, showing you how to code the persistence methods each time. The code is shown here in VB, although you can use the same principles if you are developing in VC++.

You will create a custom watermark layer, which simply displays a faint picture over a map to indicate map copyright for exporting or printing purposes.

It can be added programmatically to a map and provides limited layer functionality. For more information on creating custom layers, see 'Creating Cartography'.

Version 1

For the first version of your layer class, WatermarkLayer, implement ILayer and ILayer2 to provide basic layer functionality. You will need to store the following member variables.

[Visual Basic 6]
Private m_sName As String        ' ILayer::Name
Private m_bCached As Boolean     ' ILayer::Cached
Private m_bVisible As Boolean    ' ILayer::Visible
Private m_pDisplayFilter As ITransparencyDisplayFilter  ' Used for drawing
Private m_dRatio As Double       ' Ratio of picture width to height

Now implement IPersistVariant, as a custom layer must be persistable. Before coding the persistence members of the WatermarkLayer class, add a private constant called m_iCurrPersistVers. You will use this constant throughout the persistence code to store the version of the class. As this is the first version of the class, set the constant to 1.

[Visual Basic 6]
Private Const m_iCurrPersistVers As Integer = 1  

The use of this value is the key to version compatibility—you should 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.

[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  Stream.Write m_iCurrPersistVers

Then you can write the class state to the stream.

[Visual Basic 6]
  Stream.Write m_sName
  Stream.Write m_bCached
  Stream.Write m_bVisible
  Stream.Write m_pDisplayFilter
  Stream.Write m_dRatio
End Sub

In the Load method, start by reading the version number of the persisted class—store this value in the iSavedVers local variable. If this version number indicates a version of the persisted class that is newer than the current version of the class (m_iCurrPersistVers), the class will not know how to load the persisted information correctly. If iSavedVers = 0, there is an error somewhere, as the minimum expected value is one. Both cases are errors and may cause a corrupt stream—in these cases, raise an error back to the calling function.

[Visual Basic 6]

Private Sub IPersistVariant_Load(ByVal Stream As esriSystem.IVariantStream)
  Dim iSavedVers As Long
  iSavedVers = Stream.Read
  If (iSavedVers > m_iCurrPersistVers) Or (iSavedVers <= 0) Then
    Err.Raise E_FAIL
    Exit Sub
  End If

As Load may be called sometime after an object has been instantiated, you should ensure initialize default values for the class at the start of the Load.

[Visual Basic 6]

  InitializeMembers     

Now you can read the persisted class state and set the members of the current object—after first checking that the saved persistence version is the version you expect (this check will come in useful later when you produce a new version of your component).

[Visual Basic 6]

  If iSavedVers = 1 Then
    m_sName = Stream.Read
    m_bCached = Stream.Read
    m_bVisible = Stream.Read
    Set m_pDisplayFilter = Stream.Read
    m_dRatio = Stream.Read
  End If
End Sub

Now that you have the first version of your class, you can compile and deploy the component. At this point, users may have map documents that contain persisted WatermarkLayer objects.

You can use the AddWMLayer command included in the project to add a new WatermarkLayer to a document—compile the project and register the AddWMLayer command to ESRI Mx Commands.

Version 2

You are now asked to add functionality to allow people to scale the size of the watermark image, change its location relative to the full extent, and also to change the level of transparency.

To achieve these requirements, you must adapt your component. Implement ILayerEffects and return True from the SupportsTransparency property. Create and implement a new interface, IWatermarkLayer, to add properties to scale the image and set its relative location. Again, see the sample code for full details of the implementation.

Add new member variables to your class to store ILayerEffects::Transparency and also the values of the members of IWatermarkLayer.

[Visual Basic 6]

Private m_iTransparency As Integer  ' ILayerEffects::Transparency
Private m_ePosition As watermarkPosition   ' IWatermarkLayer::Position
Private m_dScale As Double  ' IWatermarkLayer::SymbolScale

As the data that needs to be persisted has now changed, increment the persist version number for the class by one.

[Visual Basic 6]
Private Const m_iCurrPersistVers 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 Load.

The Save method is updated with the new data being written to the stream after the existing members.

[Visual Basic 6]
Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  ...
  Stream.Write m_dRatio
  
  Stream.Write m_iTransparency
  Stream.Write m_ePosition
  Stream.Write m_dScale
End Sub

Now adapt the Load method to always read from the stream the data saved by both the new and old versions of the component.

[Visual Basic 6]
If (iSavedVers > 0) And (iSavedVers <= 2) Then
  ... ' Load first persistence signature

If the data in the persist stream indicates there is new (second version only data), read these values as well.

[Visual Basic 6]
If iSavedVers = 2 Then
  m_iTransparency = Stream.Read
  m_ePosition = Stream.Read
  m_dScale = Stream.Read
End If

Note that if the loaded version does not have this second version data, these member variables (m_iTransparency, m_dScale, and m_ePosition) are set to default values in the InitializeData routine.

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.

Now compile and deploy version 2 of the WatermarkLayer class. At this point, if the new version of the component encounters an older persisted version, it can load from the persisted data.

Use the ChangeWMPosition and ChangeWMScale commands in the version 2 sample code to change the new IWaterMarklayer properties; use the ArcMap Effects toolbar Adjust Transparency command to change the transparency of the WatermarkLayer.

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

Version 3

Finally, your customers ask to be able to specify their own choice of Symbol, as they are not happy with the display of the WatermarkLayer. They no longer want to be able to turn off the WatermarkLayer.

Create and implement another interface, IWatermarkSymbol, with a Symbol property—this value must also be persisted.

[Visual Basic 6]
Private m_pMarker As esriDisplay.IMarkerSymbol
...
Private Property Set IWatermarkSymbol_Symbol(ByVal RHS As esriDisplay.IMarkerSymbol)
  Set m_pMarker = RHS
End Property

As this requirement changes the data that needs to be persisted, you should again update the m_iCurrPersistVersion constant to 3.

[Visual Basic 6]
Private Const m_iCurrPersistVers As Integer = 3

To ensure the WatermarkLayer is always Visible, you can remove the member variable m_bVisible and always return True from the Visible property. Don't forget to remove all references to m_bVisible from the rest of your code.

[Visual Basic 6]
Private Property Get ILayer_Visible() As Boolean
  ILayer_Visible = True
End Property

Next, change the Save method. You no longer need to save the Visible value, but you do need to persist the new Symbol.

[Visual Basic 6]
Private Sub IPersistVariant_Save(CODE id="k">ByVal Stream CODE id="k">As esriSystem.IVariantStream)
  Stream.Write m_iCurrPersistVers
  Stream.Write m_sName
  Stream.Write m_bCached
  ' Stream.Write m_bVisible
  ...
  Stream.Write m_pMarker
End Sub

Last, update your Load method. You still need to read all the items saved at versions 1 and 2; the boolean Visible value is no longer required at version 3, so you can discard the value once read; you must still read the value, however, to advance the stream pointer by the correct amount.

[Visual Basic 6]
If (iSavedVers > 0) And (iSavedVers <= 3) Then
  m_sName = Stream.Read
  m_bCached = Stream.Read
  
  If (iSavedVers <= 2) Then
    Dim bVisible As Boolean
    bVisible = Stream.Read
  End If
  
  Set m_pDisplayFilter = Stream.Read
  m_dRatio = Stream.Read
  If iSavedVers >= 2 Then
    m_iTransparency = Stream.Read
    m_ePosition = Stream.Read
    m_dScale = Stream.Read
  End If
End If

To complete the new Load method, if the persisted version is 3, read the stream again to set the value of the Symbol property of IWatermarkSymbol::Symbol.

[Visual Basic 6]
If (iSavedVers = 3) Then
  Set m_pMarker = Stream.Read
End If

The WatermarkLayer can now use any MarkerSymbol as its watermark. Attempting to change the visibility of the layer will not have any effect.

Obsolete persisted data

In many cases, a new component version will require adding new data to a persistence pattern. The WatermarkLayer example above demonstrates that sometimes a new version of a component may no longer need to save data to the stream.

In these cases, if your new component encounters the older version persisted to a stream, you should always read the obsolete data values—otherwise, the stream pointer will be left at the wrong location, and the next value will not be read correctly. You can discard the obsolete values once read and save only the required data in the new Save method.

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 would enable old components to Load the persisted object. Note that the persistence version number written at the beginning of the Save method should account for which persistence pattern is used.

To make the implementation of persistence versions more straightforward, you may want to consider the use of a PropertySet. Each version of your component can add more, or different properties as required. The Save and Load events then only need to persist the current PropertySet. If you choose this approach, you should 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 may wish to implement IDocumentVersionSupportGEN. This interface specifies two pieces of functionality:

If ArcGIS cannot QI to IDocumentSupportVersionGEN for a given persistable object, then it will assume that the object can be persisted, unchanged, to any version of ArcGIS document.

Take, for example, a situation where a custom symbol is applied to a layer in a document at ArcGIS 9.1 and the user chooses to save a copy of the document to an ArcGIS version 8.3 document. The persistence process for the symbol will follow these general steps to create the version-specific document:

  1. When ArcMap attempts to persist the symbol object, it will attempt to QI to IDocumentSupportVersionGEN to determine if the symbol can be saved to an 8.3 document.
  2. If the QI fails, then ArcMap will call the persistence methods of the symbol as normal, assuming the symbol can be persisted to any version of ArcGIS.
  3. If the QI succeeds, ArcMap will then call the IDocumentSupportVersionGEN::IsSupportedAtVersion method, passing in a value indicating the ArcGIS version required.

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 simply return True from IsSupportedAtVersion—although in this case you do not need to implement the interface at all.

If however your component relies upon the presence of core objects or functionality which exists only from 9.0 onwards, then you should return false to indicate that the object cannot be used in an ArcGIS 8.3 document. Take the custom logo layer example which is explained above; you can prevent the layer from being saved as it is to an 8.3 document using code like that below.

[Visual Basic 6]
Private Function IDocumentVersionSupportGEN_IsSupportedAtVersion(ByVal docVersion As esriSystem.esriArcGISVersion) As Boolean
  If docVersion = esriArcGISVersion83 Then
    IDocumentVersionSupportGEN_IsSupportedAtVersion = False
  Else
    IDocumentVersionSupportGEN_IsSupportedAtVersion = True
  End If
End Function

In order to allow users to save a copy of a document with the custom logo layer as an 8.3 document you can then use code like that below to create a basic RasterLayer as an alternative to the custom logo layer, and return this from ConvertToSupportedObject. (This may not be an appropriate solution for every custom layer of course, but the principle can be applied to similar code to create an object appropriate to your own customizations).

[Visual Basic 6]
Private Function IDocumentVersionSupportGEN_ConvertToSupportedObject(ByVal docVersion As esriSystem.esriArcGISVersion) As Variant
  If docVersion = esriSystem.esriArcGISVersion.esriArcGISVersion83 Then
    ' Note this code relies on the raster dataset file being available at the specified
    ' location and having an appropriate spatial reference applied.
    Dim pWsFact As IWorkspaceFactory, pWs As IRasterWorkspace
    Set pWsFact = New RasterWorkspaceFactory
    Set pWs = pWsFact.OpenFromFile("C:\MyWorkspace", 0)
    Dim pRasterDataset As IRasterDataset
    Set pRasterDataset = pWs.OpenRasterDataset("MyLogo.bmp")
    If Not pRasterDataset Is Nothing Then
      Dim pRasterLy As IRasterLayer
      Set pRasterLy = New RasterLayer
      pRasterLy.CreateFromDataset pRasterDataset
    Set IDocumentVersionSupportGEN_ConvertToSupportedObject = pRasterLy
    End If
  End If
End Function

Note that for every esriArcGISVersion for which you return false from IsSupportedAtVersion, you should implement a suitable alternative in ConvertToSupportedObject—if you do not do this then you will prevent users from saving a copy of a document to that version, and the user may not receive any information about why exactly their attempt to save a copy failed. Do not return a null reference either, as ArcGIS may attempt to apply this null reference to a property, which may cause the saved document to become corrupted.


Your responsibilities when implementing persistence

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

You control what data to persist

Remember that you control exactly what is written to the stream. If your class needs to persist a reference to another custom object, you have two options. First, you could implement persistence on the other custom class, and persist that class as you would any other object.

Alternatively, you could write the members of the secondary class to your persist stream for the primary class. Although the second option may be simpler, the first method is recommended, as it is more maintainable and scalable.

Error handling and file integrity

If you raise errors in a stream Load event, this may cause the current structured storage (for example, the current .Mxd file) to be unreadable. You must take care when writing persistence code to ensure you preserve the integrity of storage files.

ObjectsStreams in extensions

As mentioned previously, a separate ObjectStream is created for each extension. This situation can lead to problems for components which 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 will result in two separate objects being persisted. When the document is reloaded, the object you persisted will no longer be the same object that was persisted in the main document ObjectStream. You may want to initialize such objects in the extension startup, rather than in persistence code.

Version Compatibility

You must ensure your components are consistent with the version compatibility used by ArcGIS. Ensure as a minimum that your components are backwardly compatible—that a client (for example ArcMap) can open a document that was created with an earlier version of your component.

You should also consider implementing IDocumentVersionSupportGEN, if required, to ensure your component can be opened in a document which is saved as a previous version of ArcGIS. For each ArcGIS version that you return False from IsSupportedAtVersion, you should ensure you return a valid alternative object from ConvertToSupportedObject.


Back to top